diff --git a/.gitignore b/.gitignore index a6b1f8b..434fae0 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,5 @@ dist # TernJS port file .tern-port + +.DS_Store \ No newline at end of file diff --git a/package.json b/package.json index 4c1f581..4a9497e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "custom-element-types", - "version": "0.0.2", + "version": "0.0.3", "description": "A generator to create Framework integrations and types for Custom Elements using the Custom Elements Schema format.", "packageManager": "pnpm@10.7.0", "engines": { diff --git a/src/angular.ts b/src/angular.ts index cfb0785..e998422 100644 --- a/src/angular.ts +++ b/src/angular.ts @@ -12,7 +12,7 @@ import { Directive, Input, Output, EventEmitter, ElementRef } from '@angular/cor ${elements.map(e => e.importType).join('\n')} ${elements.map(e => getDirective(e)).join('\n')}`.trim(); - return [{ src, path: 'custom-element-types.module.ts' }];; + return [{ src, path: 'custom-element-types.module.ts' }]; } // https://github.com/angular/angular/issues/14761 @@ -30,7 +30,7 @@ ${getOutputEvents(element)} } function getInputProperties(element: CustomElement) { - return element.propeties.map(prop => ` + return element.properties.map(prop => ` @Input() set ${prop.name}(value${prop.type === 'boolean' ? `: boolean | ''` : ''}) { this.element.${prop.name} = ${prop.type === 'boolean' ? `value === '' ? true : ` : ''}value; } get ${prop.name}() { return this.element.${prop.name}; }`).join('\n'); } diff --git a/src/blazor.ts b/src/blazor.ts index 0d6b9a0..fa62594 100644 --- a/src/blazor.ts +++ b/src/blazor.ts @@ -80,15 +80,4 @@ Object.keys(customEvents).map(event => { `; return [{ src: srcCS, path: 'CustomEvents.cs' }, { src: srcJS, path: 'custom-events.js' }]; -} - - -// export function afterStarted() { -// ${Object.keys(eventObject).map(name => ({ name, descriptions: eventObject[name] })).map(e => ` -// ${e.descriptions.map(d => ` // ${d.tagName}: ${d.description}`).join('\n')} -// Blazor.registerCustomEventType('${e.name}', { -// browserEventName: '${e.name}', -// createEventArgs: event => { -// return { detail: event.detail }; -// } -// });`)}`}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/reserved.ts b/src/reserved.ts index d020684..4fe3d4c 100644 --- a/src/reserved.ts +++ b/src/reserved.ts @@ -257,9 +257,8 @@ const reservedPublicProperties = new Set( [...ariaProperties, ...htmlElementProperties, ...elementProperties].map(p => p.toLowerCase()) ); -export function isReservedProperty(memberName) { - memberName = memberName.toLowerCase(); - return reservedPublicProperties.has(memberName); +export function isReservedProperty(memberName: string) { + return reservedPublicProperties.has(memberName.toLowerCase()); } const htmlElementEvents = [ @@ -342,7 +341,6 @@ const htmlElementEvents = [ const reservedPublicEvents = new Set([...htmlElementEvents].map(p => p.toLowerCase())); -export function isReservedEvent(memberName) { - memberName = memberName.toLowerCase(); - return reservedPublicEvents.has(memberName); +export function isReservedEvent(memberName: string) { + return reservedPublicEvents.has(memberName.toLowerCase()); } \ No newline at end of file diff --git a/src/typescript.ts b/src/typescript.ts index 5946c01..08adf79 100644 --- a/src/typescript.ts +++ b/src/typescript.ts @@ -15,5 +15,5 @@ ${elements.map(e => ` '${e.tagName}': ${e.name}`).join(';\n')} } }`.trim(); - return [{ src, path: 'types.d.ts' }];; + return [{ src, path: 'types.d.ts' }]; } diff --git a/src/utils.ts b/src/utils.ts index 60e5af8..512c6dc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,7 @@ export interface CustomElement { import: string; importType: string; description: string; - propeties: { name: string; type: string; }[]; + properties: { name: string; type: string; }[]; events: { name: string; }[]; cssProperties: any[]; slots: any[] @@ -19,7 +19,7 @@ export interface CustomElementMetadata { elements: CustomElement[]; } -export function createElementMetadata(customElementsManifest: Package, entrypoint): CustomElement[] { +export function createElementMetadata(customElementsManifest: Package, entrypoint: string): CustomElement[] { const modules = getCustomElementModules(customElementsManifest); const elements = modules.flatMap(m => { @@ -36,7 +36,7 @@ export function createElementMetadata(customElementsManifest: Package, entrypoin slots: d.slots ?? [], cssProperties: d.cssProperties ?? [], events: getCustomElementEvents(d) ?? [], - propeties: getPublicProperties(d) + properties: getPublicProperties(d) }; return element; @@ -52,7 +52,7 @@ function replaceTsExtentions(filePath: string) { function changeExt(filePath: string, ext: string) { const pos = filePath.includes('.') ? filePath.lastIndexOf('.') : filePath.length; - return `${filePath.substr(0, pos)}.${ext}`; + return `${filePath.substring(0, pos)}.${ext}`; } function getPublicProperties(element: any) { @@ -62,8 +62,6 @@ function getPublicProperties(element: any) { m.kind === 'field' && m.attribute !== undefined && m.privacy === undefined && - m.privacy !== 'private' && - m.privacy !== 'protected' && m.name !== 'accessor' && !isReservedProperty(m.name) ) ?? []).map(p => ({ name: p.name, type: p.type?.text })); @@ -73,7 +71,7 @@ function getCustomElementModules(customElementsManifest: any) { return customElementsManifest.modules.filter(m => m.declarations?.length && m.declarations.find(d => d.customElement === true)); } -function getCustomElementEvents(element): any[] { +function getCustomElementEvents(element: any): any[] { const memberEvents = element.members .filter(event => event.privacy === undefined) // public .filter(prop => prop.type && prop.type?.text && prop.type?.text.includes('EventEmitter') && !isReservedEvent(prop.name)) diff --git a/test/blazor.test.ts b/test/blazor.test.ts new file mode 100644 index 0000000..ea82bad --- /dev/null +++ b/test/blazor.test.ts @@ -0,0 +1,98 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { generate } from '../src/blazor.js'; +import type { Package } from 'custom-elements-manifest/schema'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('blazor.ts', () => { + const manifestPath = join(__dirname, 'fixtures', 'sample-manifest.json'); + const manifest: Package = JSON.parse(readFileSync(manifestPath, 'utf-8')); + + describe('generate', () => { + it('should generate two output files', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].path, 'CustomEvents.cs'); + assert.strictEqual(result[1].path, 'custom-events.js'); + }); + + it('should generate C# EventHandlers with correct namespace', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('namespace BlazorApp;')); + assert.ok(result[0].src.includes('public static class EventHandlers')); + }); + + it('should include C# CustomEventArgs class', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('public class CustomEventArgs : EventArgs')); + assert.ok(result[0].src.includes('public dynamic? Detail { get; set; }')); + assert.ok(result[0].src.includes('public T GetDetail()')); + }); + + it('should include EventHandler attributes for custom events', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('[EventHandler("on')); + assert.ok(result[0].src.includes('typeof(CustomEventArgs)')); + }); + + it('should generate JavaScript custom events registration', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[1].src.includes('const customEvents')); + assert.ok(result[1].src.includes('Blazor.registerCustomEventType')); + }); + + it('should include CustomEvent bubbling workaround in JS', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[1].src.includes('class Bubbled extends CustomEvent')); + }); + + it('should include C# using statements', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('using Microsoft.AspNetCore.Components;')); + assert.ok(result[0].src.includes('using System.Text.Json;')); + }); + + it('should include generated message comment in both files', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('Generated with https://github.com/blueprintui/custom-element-types')); + assert.ok(result[1].src.includes('Generated with https://github.com/blueprintui/custom-element-types')); + }); + }); +}); diff --git a/test/jsx.test.ts b/test/jsx.test.ts new file mode 100644 index 0000000..f5c68cc --- /dev/null +++ b/test/jsx.test.ts @@ -0,0 +1,113 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { generate } from '../src/jsx.js'; +import type { Package } from 'custom-elements-manifest/schema'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('jsx.ts', () => { + const manifestPath = join(__dirname, 'fixtures', 'sample-manifest.json'); + const manifest: Package = JSON.parse(readFileSync(manifestPath, 'utf-8')); + + describe('generate', () => { + it('should generate JSX type declarations', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].path, 'types.d.ts'); + assert.ok(result[0].src.length > 0); + }); + + it('should use type imports (not value imports)', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("import type { MyButton } from './/button/element.js'")); + assert.ok(result[0].src.includes("import type { MyInput } from './/input/element.js'")); + }); + + it('should export CustomElements interface', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('export interface CustomElements')); + }); + + it('should include tag names in CustomElements', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("['my-button']")); + assert.ok(result[0].src.includes("['my-input']")); + }); + + it('should include custom events in element type', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("CustomElement")); + }); + + it('should not include event types for elements without events', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('CustomElement')); + }); + + it('should define CustomEvents and CustomElement type helpers', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('type CustomEvents')); + assert.ok(result[0].src.includes('type CustomElement { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('Generated with https://github.com/blueprintui/custom-element-types')); + }); + + it('should mark as experimental', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('@experimental')); + }); + + it('should use entrypoint when provided', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: '@mylib/components' + }); + + assert.ok(result[0].src.includes("import type { MyButton } from '@mylib/components/button/element.js'")); + assert.ok(result[0].src.includes("import type { MyInput } from '@mylib/components/input/element.js'")); + }); + }); +}); diff --git a/test/preact.test.ts b/test/preact.test.ts new file mode 100644 index 0000000..a2587a1 --- /dev/null +++ b/test/preact.test.ts @@ -0,0 +1,106 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { generate } from '../src/preact.js'; +import type { Package } from 'custom-elements-manifest/schema'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('preact.ts', () => { + const manifestPath = join(__dirname, 'fixtures', 'sample-manifest.json'); + const manifest: Package = JSON.parse(readFileSync(manifestPath, 'utf-8')); + + describe('generate', () => { + it('should generate Preact type declarations', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].path, 'types.d.ts'); + assert.ok(result[0].src.length > 0); + }); + + it('should include element imports', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("import { MyButton } from './/button/element.js'")); + assert.ok(result[0].src.includes("import { MyInput } from './/input/element.js'")); + }); + + it('should declare global preact.JSX namespace', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('declare global')); + assert.ok(result[0].src.includes('namespace preact.JSX')); + assert.ok(result[0].src.includes('interface IntrinsicElements')); + }); + + it('should include tag names in IntrinsicElements', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("['my-button']")); + assert.ok(result[0].src.includes("['my-input']")); + }); + + it('should include custom events in element type', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes("CustomElement")); + }); + + it('should not include event types for elements without events', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('CustomElement')); + }); + + it('should define CustomEvents and CustomElement type helpers', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('type CustomEvents')); + assert.ok(result[0].src.includes('type CustomElement { + const result = generate({ + customElementsManifest: manifest, + entrypoint: undefined + }); + + assert.ok(result[0].src.includes('Generated with https://github.com/blueprintui/custom-element-types')); + }); + + it('should use entrypoint when provided', () => { + const result = generate({ + customElementsManifest: manifest, + entrypoint: '@mylib/components' + }); + + assert.ok(result[0].src.includes("import { MyButton } from '@mylib/components/button/element.js'")); + assert.ok(result[0].src.includes("import { MyInput } from '@mylib/components/input/element.js'")); + }); + }); +}); diff --git a/test/utils.test.ts b/test/utils.test.ts index 9ae8c0d..2e8eb9a 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -69,7 +69,7 @@ describe('utils.ts', () => { const elements = createElementMetadata(manifest, undefined); const button = elements[0]; - const readonlyProp = button.propeties.find(p => p.name === 'readonly'); + const readonlyProp = button.properties.find(p => p.name === 'readonly'); assert.strictEqual(readonlyProp, undefined, 'Should not include readonly properties'); }); @@ -77,7 +77,7 @@ describe('utils.ts', () => { const elements = createElementMetadata(manifest, undefined); const button = elements[0]; - const privateProp = button.propeties.find(p => p.name === 'privateField'); + const privateProp = button.properties.find(p => p.name === 'privateField'); assert.strictEqual(privateProp, undefined, 'Should not include private properties'); }); @@ -85,7 +85,7 @@ describe('utils.ts', () => { const elements = createElementMetadata(manifest, undefined); const button = elements[0]; - const staticProp = button.propeties.find(p => p.name === 'staticField'); + const staticProp = button.properties.find(p => p.name === 'staticField'); assert.strictEqual(staticProp, undefined, 'Should not include static properties'); }); @@ -93,7 +93,7 @@ describe('utils.ts', () => { const elements = createElementMetadata(manifest, undefined); const button = elements[0]; - const reservedProp = button.propeties.find(p => p.name === 'innerHTML'); + const reservedProp = button.properties.find(p => p.name === 'innerHTML'); assert.strictEqual(reservedProp, undefined, 'Should not include reserved properties'); }); @@ -101,13 +101,13 @@ describe('utils.ts', () => { const elements = createElementMetadata(manifest, undefined); const button = elements[0]; - assert.strictEqual(button.propeties.length, 2, 'Should have 2 valid properties'); + assert.strictEqual(button.properties.length, 2, 'Should have 2 valid properties'); - const disabled = button.propeties.find(p => p.name === 'disabled'); + const disabled = button.properties.find(p => p.name === 'disabled'); assert.ok(disabled, 'Should include disabled property'); assert.strictEqual(disabled.type, 'boolean'); - const label = button.propeties.find(p => p.name === 'label'); + const label = button.properties.find(p => p.name === 'label'); assert.ok(label, 'Should include label property'); assert.strictEqual(label.type, 'string'); }); diff --git a/tsconfig.json b/tsconfig.json index 23e8924..eeb55f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,8 +24,8 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "web-test-runner-performance": ["./dist/lib"], - "web-test-runner-performance/*": ["./dist/lib/*"] + "custom-element-types": ["./dist/lib"], + "custom-element-types/*": ["./dist/lib/*"] } }, "include": ["src/**/*.ts", "src/**/*.d.ts"],