@openwebf/webf 0.24.0 → 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.
@@ -0,0 +1,80 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { agentsInitCommand } from '../src/agents';
5
+
6
+ function readText(filePath: string) {
7
+ return fs.readFileSync(filePath, 'utf-8');
8
+ }
9
+
10
+ describe('webf agents init', () => {
11
+ let tempDir: string;
12
+ let consoleSpy: jest.SpyInstance;
13
+
14
+ beforeEach(() => {
15
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webf-agents-init-'));
16
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
17
+ });
18
+
19
+ afterEach(() => {
20
+ consoleSpy.mockRestore();
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it('initializes a new project with skills and CLAUDE.md', async () => {
25
+ await agentsInitCommand(tempDir);
26
+
27
+ expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
28
+ expect(fs.existsSync(path.join(tempDir, '.claude', 'skills', 'webf-quickstart', 'SKILL.md'))).toBe(true);
29
+
30
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
31
+ expect(claude).toContain('<!-- webf-agents:init start -->');
32
+ expect(claude).toContain('Source: `@openwebf/claude-code-skills@');
33
+ expect(claude).toContain('### Skills');
34
+
35
+ const version = readText(path.join(tempDir, '.claude', 'webf-claude-code-skills.version'));
36
+ expect(version).toMatch(/^@openwebf\/claude-code-skills@/);
37
+ });
38
+
39
+ it('updates an existing CLAUDE.md without removing existing content', async () => {
40
+ fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), '# Existing\n\nHello\n', 'utf-8');
41
+
42
+ await agentsInitCommand(tempDir);
43
+
44
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
45
+ expect(claude).toContain('# Existing');
46
+ expect(claude).toContain('Hello');
47
+ expect(claude).toContain('<!-- webf-agents:init start -->');
48
+ });
49
+
50
+ it('is idempotent (does not duplicate the injected block)', async () => {
51
+ await agentsInitCommand(tempDir);
52
+ await agentsInitCommand(tempDir);
53
+
54
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
55
+ const occurrences = claude.split('<!-- webf-agents:init start -->').length - 1;
56
+ expect(occurrences).toBe(1);
57
+ });
58
+
59
+ it('backs up modified skill files before overwriting', async () => {
60
+ const skillDir = path.join(tempDir, '.claude', 'skills', 'webf-quickstart');
61
+ fs.mkdirSync(skillDir, { recursive: true });
62
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'local edits', 'utf-8');
63
+
64
+ await agentsInitCommand(tempDir);
65
+
66
+ const files = fs.readdirSync(skillDir);
67
+ expect(files.some(f => f.startsWith('SKILL.md.bak.'))).toBe(true);
68
+
69
+ const sourceSkill = fs.readFileSync(
70
+ path.join(
71
+ path.dirname(require.resolve('@openwebf/claude-code-skills/package.json')),
72
+ 'webf-quickstart',
73
+ 'SKILL.md'
74
+ ),
75
+ 'utf-8'
76
+ );
77
+ const installedSkill = readText(path.join(skillDir, 'SKILL.md'));
78
+ expect(installedSkill).toBe(sourceSkill);
79
+ });
80
+ });
@@ -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
+ });
@@ -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.tsup.config.ts.tpl')) return 'tsup config';
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';
@@ -823,7 +823,7 @@ describe('Commands', () => {
823
823
  name: 'test-package',
824
824
  version: '1.0.0',
825
825
  dependencies: { react: '^18.0.0' },
826
- scripts: { build: 'tsup' }
826
+ scripts: { build: 'tsdown' }
827
827
  });
828
828
  }
829
829
  return '';
@@ -1300,7 +1300,7 @@ describe('Commands', () => {
1300
1300
  name: 'test-package',
1301
1301
  version: '1.0.0',
1302
1302
  scripts: {
1303
- build: 'tsup'
1303
+ build: 'tsdown'
1304
1304
  },
1305
1305
  dependencies: { react: '^18.0.0' }
1306
1306
  });
@@ -1349,7 +1349,7 @@ describe('Commands', () => {
1349
1349
  name: 'test-package',
1350
1350
  version: '1.0.0',
1351
1351
  scripts: {
1352
- build: 'tsup'
1352
+ build: 'tsdown'
1353
1353
  },
1354
1354
  dependencies: { react: '^18.0.0' }
1355
1355
  });
@@ -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', () => {
@@ -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;');