@openwebf/webf 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/analyzer.js +183 -121
- package/dist/commands.js +54 -9
- package/dist/generator.js +39 -16
- package/package.json +1 -1
- package/src/analyzer.ts +186 -114
- package/src/commands.ts +65 -11
- package/src/generator.ts +32 -12
- package/templates/module.package.json.tpl +17 -6
- package/templates/{module.tsup.config.ts.tpl → module.tsdown.config.ts.tpl} +2 -7
- package/templates/react.component.tsx.tpl +18 -2
- package/templates/react.package.json.tpl +17 -5
- package/templates/{react.tsup.config.ts.tpl → react.tsdown.config.ts.tpl} +2 -4
- package/templates/vue.package.json.tpl +1 -1
- package/test/analyzer.test.ts +45 -1
- package/test/commands.test.ts +76 -4
- package/test/generator.test.ts +37 -0
- package/test/react.test.ts +5 -0
package/test/analyzer.test.ts
CHANGED
|
@@ -366,5 +366,49 @@ describe('Analyzer', () => {
|
|
|
366
366
|
expect(classObj.props[1].type.value).toBe(FunctionArgumentType.promise);
|
|
367
367
|
expect(classObj.props[2].type.value).toBe(FunctionArgumentType.int);
|
|
368
368
|
});
|
|
369
|
+
|
|
370
|
+
it('should preserve complex CustomEvent generic types', () => {
|
|
371
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
372
|
+
|
|
373
|
+
const blob = new IDLBlob('/test/source.d.ts', '/test/target', 'test', 'test');
|
|
374
|
+
blob.raw = `
|
|
375
|
+
interface VideoError {
|
|
376
|
+
code: int;
|
|
377
|
+
message: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface Events {
|
|
381
|
+
error: CustomEvent<VideoError>;
|
|
382
|
+
loadedmetadata: CustomEvent<{ duration: double; videoWidth: int; videoHeight: int }>;
|
|
383
|
+
volumechange: CustomEvent<{ volume: double; muted: boolean }>;
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
const propertyCollector = {
|
|
388
|
+
properties: new Set<string>(),
|
|
389
|
+
files: new Set<string>(),
|
|
390
|
+
interfaces: new Set<string>(),
|
|
391
|
+
};
|
|
392
|
+
const unionTypeCollector = {
|
|
393
|
+
types: new Set<ParameterType[]>(),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
analyzer(blob, propertyCollector, unionTypeCollector);
|
|
397
|
+
|
|
398
|
+
const eventsObj = blob.objects.find(o => (o as ClassObject).name === 'Events') as ClassObject;
|
|
399
|
+
expect(eventsObj).toBeDefined();
|
|
400
|
+
|
|
401
|
+
const errorProp = eventsObj.props.find(p => p.name === 'error');
|
|
402
|
+
expect(errorProp?.type.value).toBe('CustomEvent<VideoError>');
|
|
403
|
+
|
|
404
|
+
const loadedProp = eventsObj.props.find(p => p.name === 'loadedmetadata');
|
|
405
|
+
expect(loadedProp?.type.value).toBe('CustomEvent<{ duration: number; videoWidth: number; videoHeight: number }>');
|
|
406
|
+
|
|
407
|
+
const volumeProp = eventsObj.props.find(p => p.name === 'volumechange');
|
|
408
|
+
expect(volumeProp?.type.value).toBe('CustomEvent<{ volume: number; muted: boolean }>');
|
|
409
|
+
|
|
410
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
411
|
+
warnSpy.mockRestore();
|
|
412
|
+
});
|
|
369
413
|
});
|
|
370
|
-
});
|
|
414
|
+
});
|
package/test/commands.test.ts
CHANGED
|
@@ -24,7 +24,7 @@ mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
|
|
|
24
24
|
if (pathStr.includes('gitignore.tpl')) return 'gitignore template';
|
|
25
25
|
if (pathStr.includes('react.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
|
|
26
26
|
if (pathStr.includes('react.tsconfig.json.tpl')) return 'react tsconfig';
|
|
27
|
-
if (pathStr.includes('react.
|
|
27
|
+
if (pathStr.includes('react.tsdown.config.ts.tpl')) return 'tsdown config';
|
|
28
28
|
if (pathStr.includes('react.index.ts.tpl')) return 'index template';
|
|
29
29
|
if (pathStr.includes('vue.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
|
|
30
30
|
if (pathStr.includes('vue.tsconfig.json.tpl')) return 'vue tsconfig';
|
|
@@ -456,6 +456,78 @@ describe('Commands', () => {
|
|
|
456
456
|
}));
|
|
457
457
|
});
|
|
458
458
|
|
|
459
|
+
it('should copy Flutter README.md into generated React package root', async () => {
|
|
460
|
+
const options = {
|
|
461
|
+
flutterPackageSrc: '/flutter/src',
|
|
462
|
+
framework: 'react',
|
|
463
|
+
packageName: 'test-package'
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Mock TypeScript validation
|
|
467
|
+
mockTypeScriptValidation('/flutter/src');
|
|
468
|
+
|
|
469
|
+
const originalExistsSync = mockFs.existsSync as jest.Mock;
|
|
470
|
+
mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
|
|
471
|
+
const pathStr = filePath.toString();
|
|
472
|
+
if (pathStr === '/flutter/src/README.md') return true;
|
|
473
|
+
if (pathStr === path.join(path.resolve('/dist'), 'README.md')) return false;
|
|
474
|
+
return originalExistsSync(filePath);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const originalReadFileSync = mockFs.readFileSync as jest.Mock;
|
|
478
|
+
mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
|
|
479
|
+
const pathStr = filePath.toString();
|
|
480
|
+
if (pathStr === '/flutter/src/README.md') {
|
|
481
|
+
return '# Flutter README\n\nHello';
|
|
482
|
+
}
|
|
483
|
+
return originalReadFileSync(filePath, encoding);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await generateCommand('/dist', options);
|
|
487
|
+
|
|
488
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
489
|
+
path.join(path.resolve('/dist'), 'README.md'),
|
|
490
|
+
'# Flutter README\n\nHello',
|
|
491
|
+
'utf-8'
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should copy Flutter README.md into generated Vue package root', async () => {
|
|
496
|
+
const options = {
|
|
497
|
+
flutterPackageSrc: '/flutter/src',
|
|
498
|
+
framework: 'vue',
|
|
499
|
+
packageName: 'test-package'
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Mock TypeScript validation
|
|
503
|
+
mockTypeScriptValidation('/flutter/src');
|
|
504
|
+
|
|
505
|
+
const originalExistsSync = mockFs.existsSync as jest.Mock;
|
|
506
|
+
mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
|
|
507
|
+
const pathStr = filePath.toString();
|
|
508
|
+
if (pathStr === '/flutter/src/README.md') return true;
|
|
509
|
+
if (pathStr === path.join(path.resolve('/dist'), 'README.md')) return false;
|
|
510
|
+
return originalExistsSync(filePath);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const originalReadFileSync = mockFs.readFileSync as jest.Mock;
|
|
514
|
+
mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
|
|
515
|
+
const pathStr = filePath.toString();
|
|
516
|
+
if (pathStr === '/flutter/src/README.md') {
|
|
517
|
+
return '# Flutter README\n\nHello';
|
|
518
|
+
}
|
|
519
|
+
return originalReadFileSync(filePath, encoding);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await generateCommand('/dist', options);
|
|
523
|
+
|
|
524
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
525
|
+
path.join(path.resolve('/dist'), 'README.md'),
|
|
526
|
+
'# Flutter README\n\nHello',
|
|
527
|
+
'utf-8'
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
459
531
|
it('should generate an aggregated README in dist from markdown docs', async () => {
|
|
460
532
|
const options = {
|
|
461
533
|
flutterPackageSrc: '/flutter/src',
|
|
@@ -751,7 +823,7 @@ describe('Commands', () => {
|
|
|
751
823
|
name: 'test-package',
|
|
752
824
|
version: '1.0.0',
|
|
753
825
|
dependencies: { react: '^18.0.0' },
|
|
754
|
-
scripts: { build: '
|
|
826
|
+
scripts: { build: 'tsdown' }
|
|
755
827
|
});
|
|
756
828
|
}
|
|
757
829
|
return '';
|
|
@@ -1228,7 +1300,7 @@ describe('Commands', () => {
|
|
|
1228
1300
|
name: 'test-package',
|
|
1229
1301
|
version: '1.0.0',
|
|
1230
1302
|
scripts: {
|
|
1231
|
-
build: '
|
|
1303
|
+
build: 'tsdown'
|
|
1232
1304
|
},
|
|
1233
1305
|
dependencies: { react: '^18.0.0' }
|
|
1234
1306
|
});
|
|
@@ -1277,7 +1349,7 @@ describe('Commands', () => {
|
|
|
1277
1349
|
name: 'test-package',
|
|
1278
1350
|
version: '1.0.0',
|
|
1279
1351
|
scripts: {
|
|
1280
|
-
build: '
|
|
1352
|
+
build: 'tsdown'
|
|
1281
1353
|
},
|
|
1282
1354
|
dependencies: { react: '^18.0.0' }
|
|
1283
1355
|
});
|
package/test/generator.test.ts
CHANGED
|
@@ -395,6 +395,43 @@ describe('Generator', () => {
|
|
|
395
395
|
const indexWrite = mockFs.writeFileSync.mock.calls.find(call => call[0].toString().includes('index.ts'));
|
|
396
396
|
expect(indexWrite).toBeUndefined();
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
it('should merge type-only Element exports into existing index.ts', async () => {
|
|
400
|
+
mockAnalyzer.analyzer.mockImplementation((blob: any) => {
|
|
401
|
+
const props = new ClassObject();
|
|
402
|
+
props.name = blob.filename === 'test' ? 'TestProperties' : 'ComponentProperties';
|
|
403
|
+
const events = new ClassObject();
|
|
404
|
+
events.name = blob.filename === 'test' ? 'TestEvents' : 'ComponentEvents';
|
|
405
|
+
blob.objects = [props, events];
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
mockFs.existsSync.mockImplementation((p: any) => {
|
|
409
|
+
const s = p.toString();
|
|
410
|
+
if (s.includes(path.join('/test/target', 'src', 'index.ts'))) return true;
|
|
411
|
+
return true;
|
|
412
|
+
});
|
|
413
|
+
mockFs.readFileSync.mockImplementation((p: any) => {
|
|
414
|
+
const s = p.toString();
|
|
415
|
+
if (s.includes(path.join('/test/target', 'src', 'index.ts'))) {
|
|
416
|
+
return '/* empty index scaffold */\n';
|
|
417
|
+
}
|
|
418
|
+
return 'test content';
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await reactGen({
|
|
422
|
+
source: '/test/source',
|
|
423
|
+
target: '/test/target',
|
|
424
|
+
command: 'test command'
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const indexWrite = mockFs.writeFileSync.mock.calls.find(call => call[0].toString().includes('index.ts'));
|
|
428
|
+
expect(indexWrite).toBeDefined();
|
|
429
|
+
const content = String(indexWrite![1]);
|
|
430
|
+
expect(content).toContain('export { Test }');
|
|
431
|
+
expect(content).toContain('export type { TestElement }');
|
|
432
|
+
expect(content).toContain('export { Component }');
|
|
433
|
+
expect(content).toContain('export type { ComponentElement }');
|
|
434
|
+
});
|
|
398
435
|
});
|
|
399
436
|
|
|
400
437
|
describe('vueGen', () => {
|
package/test/react.test.ts
CHANGED
|
@@ -184,6 +184,11 @@ describe('React Generator', () => {
|
|
|
184
184
|
// Should include custom props with generated TS types
|
|
185
185
|
expect(result).toContain('title: __webfTypes.dom_string;');
|
|
186
186
|
expect(result).toContain('disabled?: __webfTypes.boolean;');
|
|
187
|
+
|
|
188
|
+
// Element interface should surface properties for refs
|
|
189
|
+
expect(result).toContain('export interface TestComponentElement extends WebFElementWithMethods<{');
|
|
190
|
+
expect(result).toContain('title: __webfTypes.dom_string;');
|
|
191
|
+
expect(result).toContain('disabled?: __webfTypes.boolean;');
|
|
187
192
|
|
|
188
193
|
// And still include standard HTML props
|
|
189
194
|
expect(result).toContain('id?: string;');
|