@openwebf/webf 0.23.10 → 0.24.1
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 +8 -0
- package/bin/webf.js +9 -1
- package/dist/agents.js +245 -0
- package/dist/commands.js +101 -7
- package/dist/generator.js +37 -36
- package/dist/module.js +43 -14
- package/dist/peerDeps.js +27 -0
- package/dist/react.js +10 -18
- package/dist/vue.js +138 -132
- package/package.json +2 -1
- package/src/agents.ts +267 -0
- package/src/commands.ts +110 -8
- package/src/generator.ts +38 -37
- package/src/module.ts +53 -21
- package/src/peerDeps.ts +21 -0
- package/src/react.ts +10 -18
- package/src/vue.ts +157 -142
- package/templates/react.component.tsx.tpl +2 -2
- package/templates/react.index.ts.tpl +2 -1
- package/templates/react.package.json.tpl +3 -4
- package/templates/react.tsconfig.json.tpl +8 -1
- package/templates/react.tsup.config.ts.tpl +1 -1
- package/templates/vue.component.partial.tpl +4 -4
- package/templates/vue.components.d.ts.tpl +24 -9
- package/templates/vue.package.json.tpl +3 -1
- package/test/agents-init.test.ts +80 -0
- package/test/commands.test.ts +5 -12
- package/test/generator.test.ts +17 -10
- package/test/peerDeps.test.ts +30 -0
- package/test/react-consts.test.ts +9 -3
- package/test/standard-props.test.ts +14 -14
- package/test/templates.test.ts +17 -0
- package/test/vue.test.ts +36 -11
- package/dist/constants.js +0 -242
|
@@ -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
|
+
});
|
package/test/commands.test.ts
CHANGED
|
@@ -230,8 +230,8 @@ describe('Commands', () => {
|
|
|
230
230
|
|
|
231
231
|
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
232
232
|
expect.stringMatching(/npm(\.cmd)?/),
|
|
233
|
-
['install'],
|
|
234
|
-
{ cwd: target, stdio: 'inherit' }
|
|
233
|
+
['install', '--production=false'],
|
|
234
|
+
expect.objectContaining({ cwd: target, stdio: 'inherit' })
|
|
235
235
|
);
|
|
236
236
|
});
|
|
237
237
|
});
|
|
@@ -283,18 +283,11 @@ describe('Commands', () => {
|
|
|
283
283
|
|
|
284
284
|
await generateCommand(target, options);
|
|
285
285
|
|
|
286
|
-
// Should install
|
|
286
|
+
// Should install dependencies (including devDependencies) from package.json
|
|
287
287
|
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
288
288
|
expect.stringMatching(/npm(\.cmd)?/),
|
|
289
|
-
['install', '
|
|
290
|
-
{ cwd: target, stdio: 'inherit' }
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
// Should install Vue 3 as dev dependency
|
|
294
|
-
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
295
|
-
expect.stringMatching(/npm(\.cmd)?/),
|
|
296
|
-
['install', 'vue', '-D'],
|
|
297
|
-
{ cwd: target, stdio: 'inherit' }
|
|
289
|
+
['install', '--production=false'],
|
|
290
|
+
expect.objectContaining({ cwd: target, stdio: 'inherit' })
|
|
298
291
|
);
|
|
299
292
|
});
|
|
300
293
|
});
|
package/test/generator.test.ts
CHANGED
|
@@ -233,19 +233,13 @@ describe('Generator', () => {
|
|
|
233
233
|
command: 'test command'
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
//
|
|
236
|
+
// Dart codegen does not write a root index.d.ts; it copies .d.ts files alongside generated Dart bindings.
|
|
237
237
|
const writeFileCalls = mockFs.writeFileSync.mock.calls;
|
|
238
238
|
const indexDtsCall = writeFileCalls.find(call =>
|
|
239
239
|
call[0].toString().endsWith('index.d.ts')
|
|
240
240
|
);
|
|
241
241
|
|
|
242
|
-
expect(indexDtsCall).
|
|
243
|
-
expect(indexDtsCall![1]).toContain('/// <reference path="./global.d.ts" />');
|
|
244
|
-
expect(indexDtsCall![1]).toContain('/// <reference path="./components/button.d.ts" />');
|
|
245
|
-
expect(indexDtsCall![1]).toContain('/// <reference path="./widgets/card.d.ts" />');
|
|
246
|
-
expect(indexDtsCall![1]).toContain("export * from './components/button';");
|
|
247
|
-
expect(indexDtsCall![1]).toContain("export * from './widgets/card';");
|
|
248
|
-
expect(indexDtsCall![1]).toContain('TypeScript Definitions');
|
|
242
|
+
expect(indexDtsCall).toBeUndefined();
|
|
249
243
|
});
|
|
250
244
|
|
|
251
245
|
it('should copy original .d.ts files to output directory', async () => {
|
|
@@ -301,6 +295,19 @@ describe('Generator', () => {
|
|
|
301
295
|
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
302
296
|
});
|
|
303
297
|
|
|
298
|
+
it('should always generate src/types.ts for React output', async () => {
|
|
299
|
+
await reactGen({
|
|
300
|
+
source: '/test/source',
|
|
301
|
+
target: '/test/target',
|
|
302
|
+
command: 'test command'
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const writeCalls = mockFs.writeFileSync.mock.calls;
|
|
306
|
+
const typesCall = writeCalls.find(call => /[\\/]src[\\/]types\.ts$/.test(call[0].toString()));
|
|
307
|
+
expect(typesCall).toBeDefined();
|
|
308
|
+
expect(typesCall![1]).toContain('export {};');
|
|
309
|
+
});
|
|
310
|
+
|
|
304
311
|
it('should use the exact target directory specified', async () => {
|
|
305
312
|
await reactGen({
|
|
306
313
|
source: '/test/source',
|
|
@@ -478,8 +485,8 @@ describe('Generator', () => {
|
|
|
478
485
|
// First file fails (no writes), second file succeeds (1 dart + 1 .d.ts), plus index.d.ts = 3 total
|
|
479
486
|
// But since the error happens in dartGen, the .d.ts copy might not happen
|
|
480
487
|
const writeCalls = mockFs.writeFileSync.mock.calls;
|
|
481
|
-
// Should have at least written the successful
|
|
482
|
-
expect(writeCalls.length).toBeGreaterThanOrEqual(
|
|
488
|
+
// Should have at least written the successful Dart bindings file
|
|
489
|
+
expect(writeCalls.length).toBeGreaterThanOrEqual(1);
|
|
483
490
|
});
|
|
484
491
|
});
|
|
485
492
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { getPackageTypesFileFromDir, isPackageTypesReady } from '../src/peerDeps';
|
|
5
|
+
|
|
6
|
+
function makeTempDir(prefix: string): string {
|
|
7
|
+
return fs.mkdtempSync(path.join(process.cwd(), 'test', prefix));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('peerDeps', () => {
|
|
11
|
+
it('treats package as not ready when declared types file is missing', () => {
|
|
12
|
+
const dir = makeTempDir('peerDeps-');
|
|
13
|
+
const distDir = path.join(dir, 'dist');
|
|
14
|
+
try {
|
|
15
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
path.join(dir, 'package.json'),
|
|
18
|
+
JSON.stringify({ name: '@openwebf/react-core-ui', types: 'dist/index.d.ts' }, null, 2)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(getPackageTypesFileFromDir(dir)).toBe(path.join(dir, 'dist', 'index.d.ts'));
|
|
22
|
+
expect(isPackageTypesReady(dir)).toBe(false);
|
|
23
|
+
|
|
24
|
+
fs.writeFileSync(path.join(distDir, 'index.d.ts'), 'export {};');
|
|
25
|
+
expect(isPackageTypesReady(dir)).toBe(true);
|
|
26
|
+
} finally {
|
|
27
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { generateReactComponent } from '../src/react';
|
|
2
2
|
import { IDLBlob } from '../src/IDLBlob';
|
|
3
|
-
import { ConstObject, EnumObject, EnumMemberObject } from '../src/declaration';
|
|
3
|
+
import { ClassObject, ClassObjectKind, ConstObject, EnumObject, EnumMemberObject } from '../src/declaration';
|
|
4
4
|
|
|
5
5
|
describe('React generator - declare const support', () => {
|
|
6
6
|
it('emits export declare const for constants from typings', () => {
|
|
7
7
|
const blob = new IDLBlob('/test/source', '/test/target', 'ConstOnly', 'test', '');
|
|
8
|
+
const properties = new ClassObject();
|
|
9
|
+
properties.name = 'TestComponentProperties';
|
|
10
|
+
properties.kind = ClassObjectKind.interface;
|
|
8
11
|
const constObj = new ConstObject();
|
|
9
12
|
constObj.name = 'WEBF_CUPERTINO_SYMBOL';
|
|
10
13
|
constObj.type = 'unique symbol';
|
|
11
14
|
|
|
12
|
-
blob.objects = [constObj as any];
|
|
15
|
+
blob.objects = [properties, constObj as any];
|
|
13
16
|
|
|
14
17
|
const output = generateReactComponent(blob);
|
|
15
18
|
expect(output).toContain('export declare const WEBF_CUPERTINO_SYMBOL: unique symbol;');
|
|
@@ -17,12 +20,15 @@ describe('React generator - declare const support', () => {
|
|
|
17
20
|
|
|
18
21
|
it('emits export declare enum for enums from typings', () => {
|
|
19
22
|
const blob = new IDLBlob('/test/source', '/test/target', 'EnumOnly', 'test', '');
|
|
23
|
+
const properties = new ClassObject();
|
|
24
|
+
properties.name = 'TestComponentProperties';
|
|
25
|
+
properties.kind = ClassObjectKind.interface;
|
|
20
26
|
const eo = new EnumObject();
|
|
21
27
|
eo.name = 'CupertinoColors';
|
|
22
28
|
const m1 = new EnumMemberObject(); m1.name = "'red'"; m1.initializer = '0x0f';
|
|
23
29
|
const m2 = new EnumMemberObject(); m2.name = "'bbb'"; m2.initializer = '0x00';
|
|
24
30
|
eo.members = [m1, m2];
|
|
25
|
-
blob.objects = [eo as any];
|
|
31
|
+
blob.objects = [properties, eo as any];
|
|
26
32
|
|
|
27
33
|
const output = generateReactComponent(blob);
|
|
28
34
|
expect(output).toContain("export enum CupertinoColors { 'red' = 0x0f, 'bbb' = 0x00 }");
|
|
@@ -49,13 +49,13 @@ describe('Standard HTML Props Generation', () => {
|
|
|
49
49
|
const propsContent = result.substring(propsStart, propsEnd);
|
|
50
50
|
|
|
51
51
|
// Verify order: custom props, event handlers, then standard HTML props
|
|
52
|
-
const labelIndex = propsContent.
|
|
53
|
-
const variantIndex = propsContent.
|
|
54
|
-
const onClickIndex = propsContent.
|
|
55
|
-
const idIndex = propsContent.
|
|
56
|
-
const styleIndex = propsContent.
|
|
57
|
-
const childrenIndex = propsContent.
|
|
58
|
-
const classNameIndex = propsContent.
|
|
52
|
+
const labelIndex = propsContent.search(/\n\s*label\??:\s*/);
|
|
53
|
+
const variantIndex = propsContent.search(/\n\s*variant\??:\s*/);
|
|
54
|
+
const onClickIndex = propsContent.search(/\n\s*onClick\??:\s*/);
|
|
55
|
+
const idIndex = propsContent.search(/\n\s*id\??:\s*/);
|
|
56
|
+
const styleIndex = propsContent.search(/\n\s*style\??:\s*/);
|
|
57
|
+
const childrenIndex = propsContent.search(/\n\s*children\??:\s*/);
|
|
58
|
+
const classNameIndex = propsContent.search(/\n\s*className\??:\s*/);
|
|
59
59
|
|
|
60
60
|
// All props should exist
|
|
61
61
|
expect(labelIndex).toBeGreaterThan(-1);
|
|
@@ -131,8 +131,8 @@ describe('Standard HTML Props Generation', () => {
|
|
|
131
131
|
|
|
132
132
|
// Standard HTML props
|
|
133
133
|
expect(propsContent).toContain("'id'?: string;");
|
|
134
|
-
expect(propsContent).toContain("'class'?:
|
|
135
|
-
expect(propsContent).toContain("'style'?:
|
|
134
|
+
expect(propsContent).toContain("'class'?: ClassValue;");
|
|
135
|
+
expect(propsContent).toContain("'style'?: StyleValue;");
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
it('should handle Vue style prop with both string and object types', () => {
|
|
@@ -145,8 +145,8 @@ describe('Standard HTML Props Generation', () => {
|
|
|
145
145
|
|
|
146
146
|
const result = generateVueTypings([blob]);
|
|
147
147
|
|
|
148
|
-
// Vue style prop should accept
|
|
149
|
-
expect(result).toMatch(/'style'\?:
|
|
148
|
+
// Vue style prop should accept Vue's supported style value forms
|
|
149
|
+
expect(result).toMatch(/'style'\?: StyleValue;/);
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
@@ -176,15 +176,15 @@ describe('Standard HTML Props Generation', () => {
|
|
|
176
176
|
|
|
177
177
|
// Both should have style prop (with appropriate types)
|
|
178
178
|
expect(reactResult).toContain('style?: React.CSSProperties;');
|
|
179
|
-
expect(vueResult).toContain("'style'?:
|
|
179
|
+
expect(vueResult).toContain("'style'?: StyleValue;");
|
|
180
180
|
|
|
181
181
|
// React has className, Vue has class
|
|
182
182
|
expect(reactResult).toContain('className?: string;');
|
|
183
|
-
expect(vueResult).toContain("'class'?:
|
|
183
|
+
expect(vueResult).toContain("'class'?: ClassValue;");
|
|
184
184
|
|
|
185
185
|
// React has children, Vue uses slots (not in props)
|
|
186
186
|
expect(reactResult).toContain('children?: React.ReactNode;');
|
|
187
187
|
expect(vueResult).not.toContain('children');
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
|
-
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
describe('Templates', () => {
|
|
5
|
+
it('react tsconfig template pins React type resolution to root @types', () => {
|
|
6
|
+
const templatePath = path.resolve(__dirname, '../templates/react.tsconfig.json.tpl');
|
|
7
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
8
|
+
|
|
9
|
+
expect(template).toContain('"baseUrl": "."');
|
|
10
|
+
expect(template).toContain('"paths"');
|
|
11
|
+
expect(template).toContain('"react": ["./node_modules/@types/react/index.d.ts"]');
|
|
12
|
+
expect(template).toContain('"react-dom": ["./node_modules/@types/react-dom/index.d.ts"]');
|
|
13
|
+
expect(template).toContain('"react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"]');
|
|
14
|
+
expect(template).toContain('"react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"]');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
package/test/vue.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateVueTypings } from '../src/vue';
|
|
2
2
|
import { IDLBlob } from '../src/IDLBlob';
|
|
3
|
-
import { ClassObject, ClassObjectKind, PropsDeclaration, ConstObject } from '../src/declaration';
|
|
3
|
+
import { ClassObject, ClassObjectKind, PropsDeclaration, ConstObject, TypeAliasObject } from '../src/declaration';
|
|
4
4
|
|
|
5
5
|
describe('Vue Generator', () => {
|
|
6
6
|
describe('generateVueTypings', () => {
|
|
@@ -16,13 +16,16 @@ describe('Vue Generator', () => {
|
|
|
16
16
|
|
|
17
17
|
// Should include standard HTML props in Props type
|
|
18
18
|
expect(result).toContain("'id'?: string;");
|
|
19
|
-
expect(result).toContain("'class'?:
|
|
20
|
-
expect(result).toContain("'style'?:
|
|
19
|
+
expect(result).toContain("'class'?: ClassValue;");
|
|
20
|
+
expect(result).toContain("'style'?: StyleValue;");
|
|
21
21
|
|
|
22
22
|
// Should generate proper type exports
|
|
23
23
|
expect(result).toContain('export type TestComponentProps = {');
|
|
24
24
|
expect(result).toContain('export interface TestComponentElement {');
|
|
25
25
|
expect(result).toContain('export type TestComponentEvents = {');
|
|
26
|
+
|
|
27
|
+
// Should not rely on a non-existent internal __webfTypes import
|
|
28
|
+
expect(result).not.toContain('__webfTypes');
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
it('should include standard HTML props along with custom properties', () => {
|
|
@@ -58,8 +61,8 @@ describe('Vue Generator', () => {
|
|
|
58
61
|
|
|
59
62
|
// And still include standard HTML props
|
|
60
63
|
expect(result).toContain("'id'?: string;");
|
|
61
|
-
expect(result).toContain("'class'?:
|
|
62
|
-
expect(result).toContain("'style'?:
|
|
64
|
+
expect(result).toContain("'class'?: ClassValue;");
|
|
65
|
+
expect(result).toContain("'style'?: StyleValue;");
|
|
63
66
|
});
|
|
64
67
|
|
|
65
68
|
it('should generate proper Vue component declarations', () => {
|
|
@@ -73,11 +76,20 @@ describe('Vue Generator', () => {
|
|
|
73
76
|
const result = generateVueTypings([blob]);
|
|
74
77
|
|
|
75
78
|
// Should generate proper component declarations
|
|
76
|
-
expect(result).toContain("declare module 'vue' {");
|
|
79
|
+
expect(result).toContain("declare module '@vue/runtime-core' {");
|
|
80
|
+
expect(result).toContain("interface GlobalDirectives {");
|
|
81
|
+
expect(result).toContain('vFlutterAttached: typeof flutterAttached;');
|
|
77
82
|
expect(result).toContain("interface GlobalComponents {");
|
|
78
|
-
expect(result).toContain("'
|
|
83
|
+
expect(result).toContain("'webf-list-view': DefineCustomElement<");
|
|
84
|
+
expect(result).not.toContain("'web-f-list-view': DefineCustomElement<");
|
|
85
|
+
expect(result).toContain("WebFListViewElement,");
|
|
79
86
|
expect(result).toContain("WebFListViewProps,");
|
|
80
87
|
expect(result).toContain("WebFListViewEvents");
|
|
88
|
+
|
|
89
|
+
// Compatibility module for older tooling
|
|
90
|
+
expect(result).toContain("declare module 'vue' {");
|
|
91
|
+
expect(result).toContain("interface GlobalDirectives extends import('@vue/runtime-core').GlobalDirectives {}");
|
|
92
|
+
expect(result).toContain("interface GlobalComponents extends import('@vue/runtime-core').GlobalComponents {}");
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
it('should handle multiple components', () => {
|
|
@@ -145,13 +157,13 @@ describe('Vue Generator', () => {
|
|
|
145
157
|
|
|
146
158
|
// Should include event types
|
|
147
159
|
expect(result).toContain('export type TestComponentEvents = {');
|
|
148
|
-
expect(result).toContain('close
|
|
149
|
-
expect(result).toContain('refresh
|
|
160
|
+
expect(result).toContain('close: Event;');
|
|
161
|
+
expect(result).toContain('refresh: CustomEvent;');
|
|
150
162
|
|
|
151
163
|
// Props should still have standard HTML props
|
|
152
164
|
expect(result).toContain("'id'?: string;");
|
|
153
|
-
expect(result).toContain("'class'?:
|
|
154
|
-
expect(result).toContain("'style'?:
|
|
165
|
+
expect(result).toContain("'class'?: ClassValue;");
|
|
166
|
+
expect(result).toContain("'style'?: StyleValue;");
|
|
155
167
|
});
|
|
156
168
|
|
|
157
169
|
it('should include declare const variables as exported declarations', () => {
|
|
@@ -168,6 +180,19 @@ describe('Vue Generator', () => {
|
|
|
168
180
|
expect(result).toContain('export declare const WEBF_UNIQUE: unique symbol;');
|
|
169
181
|
});
|
|
170
182
|
|
|
183
|
+
it('should include type aliases as exported declarations', () => {
|
|
184
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'TypeAliasOnly', 'test', '');
|
|
185
|
+
|
|
186
|
+
const typeAlias = new TypeAliasObject();
|
|
187
|
+
typeAlias.name = 'MyAlias';
|
|
188
|
+
typeAlias.type = 'string | number';
|
|
189
|
+
|
|
190
|
+
blob.objects = [typeAlias as any];
|
|
191
|
+
|
|
192
|
+
const result = generateVueTypings([blob]);
|
|
193
|
+
expect(result).toContain('export type MyAlias = string | number;');
|
|
194
|
+
});
|
|
195
|
+
|
|
171
196
|
it('should include declare enum as exported declaration', () => {
|
|
172
197
|
const blob = new IDLBlob('/test/source', '/test/target', 'EnumOnly', 'test', '');
|
|
173
198
|
// Build a minimal faux EnumObject via analyzer by simulating ast is heavy; create a shape
|
package/dist/constants.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.generateUnionConstantsValuesTs = generateUnionConstantsValuesTs;
|
|
7
|
-
exports.generateUnionConstantsDts = generateUnionConstantsDts;
|
|
8
|
-
exports.generateDeclaredConstantsValuesTs = generateDeclaredConstantsValuesTs;
|
|
9
|
-
exports.generateDeclaredConstantsDts = generateDeclaredConstantsDts;
|
|
10
|
-
const lodash_1 = __importDefault(require("lodash"));
|
|
11
|
-
const typescript_1 = __importDefault(require("typescript"));
|
|
12
|
-
const dart_1 = require("./dart");
|
|
13
|
-
// Generate constant name from component (Properties/Bindings trimmed) and prop name
|
|
14
|
-
function getConstName(className, propName) {
|
|
15
|
-
const baseName = className.replace(/Properties$|Bindings$/, '');
|
|
16
|
-
return baseName + lodash_1.default.upperFirst(lodash_1.default.camelCase(propName));
|
|
17
|
-
}
|
|
18
|
-
function collectUnionStringProps(blobs) {
|
|
19
|
-
const results = [];
|
|
20
|
-
for (const blob of blobs) {
|
|
21
|
-
const classObjects = (blob.objects || []);
|
|
22
|
-
const properties = classObjects.filter(obj => obj.name && obj.name.endsWith('Properties'));
|
|
23
|
-
if (!properties.length)
|
|
24
|
-
continue;
|
|
25
|
-
const componentProps = properties[0];
|
|
26
|
-
const componentName = componentProps.name.replace(/Properties$/, '');
|
|
27
|
-
for (const prop of componentProps.props || []) {
|
|
28
|
-
if (!(0, dart_1.isStringUnionType)(prop.type))
|
|
29
|
-
continue;
|
|
30
|
-
const values = (0, dart_1.getUnionStringValues)(prop, blob);
|
|
31
|
-
if (!values || values.length === 0)
|
|
32
|
-
continue;
|
|
33
|
-
const constName = getConstName(componentProps.name, prop.name);
|
|
34
|
-
results.push({ constName, values, className: componentName, propName: prop.name });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return results;
|
|
38
|
-
}
|
|
39
|
-
function generateUnionConstantsValuesTs(blobs) {
|
|
40
|
-
const items = collectUnionStringProps(blobs);
|
|
41
|
-
if (!items.length)
|
|
42
|
-
return '';
|
|
43
|
-
const header = `// Auto-generated by WebF CLI\n// Constants for string-union properties extracted from .d.ts definitions\n`;
|
|
44
|
-
const blocks = items.map(item => {
|
|
45
|
-
const entries = item.values.map(v => ` '${v}': '${v}',`).join('\n');
|
|
46
|
-
return `// ${item.className}.${item.propName}\nexport const ${item.constName} = {\n${entries}\n} as const;`;
|
|
47
|
-
});
|
|
48
|
-
return [header, ...blocks].join('\n\n') + '\n';
|
|
49
|
-
}
|
|
50
|
-
function generateUnionConstantsDts(blobs) {
|
|
51
|
-
const items = collectUnionStringProps(blobs);
|
|
52
|
-
if (!items.length)
|
|
53
|
-
return '';
|
|
54
|
-
const header = `// Auto-generated by WebF CLI\n// Type declarations for constants representing string-union property values\n`;
|
|
55
|
-
const blocks = items.map(item => {
|
|
56
|
-
const entries = item.values.map(v => ` readonly '${v}': '${v}';`).join('\n');
|
|
57
|
-
return `// ${item.className}.${item.propName}\nexport declare const ${item.constName}: {\n${entries}\n};`;
|
|
58
|
-
});
|
|
59
|
-
return [header, ...blocks].join('\n\n') + '\n';
|
|
60
|
-
}
|
|
61
|
-
function parseLiteralFromType(node) {
|
|
62
|
-
if (typescript_1.default.isLiteralTypeNode(node)) {
|
|
63
|
-
const lit = node.literal;
|
|
64
|
-
if (typescript_1.default.isStringLiteral(lit))
|
|
65
|
-
return lit.text;
|
|
66
|
-
if (typescript_1.default.isNumericLiteral(lit))
|
|
67
|
-
return Number(lit.text);
|
|
68
|
-
if (lit.kind === typescript_1.default.SyntaxKind.TrueKeyword)
|
|
69
|
-
return true;
|
|
70
|
-
if (lit.kind === typescript_1.default.SyntaxKind.FalseKeyword)
|
|
71
|
-
return false;
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (typescript_1.default.isTypeQueryNode(node)) {
|
|
75
|
-
// typeof Identifier
|
|
76
|
-
if (typescript_1.default.isIdentifier(node.exprName)) {
|
|
77
|
-
return { typeofRef: node.exprName.text };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
function collectDeclaredConstsFromSource(content, fileName = 'index.d.ts') {
|
|
83
|
-
const source = typescript_1.default.createSourceFile(fileName, content, typescript_1.default.ScriptTarget.ES2020, true, typescript_1.default.ScriptKind.TS);
|
|
84
|
-
const results = [];
|
|
85
|
-
const handleVariableStatement = (stmt) => {
|
|
86
|
-
// Only consider const declarations
|
|
87
|
-
if ((stmt.declarationList.flags & typescript_1.default.NodeFlags.Const) === 0)
|
|
88
|
-
return;
|
|
89
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
90
|
-
if (!typescript_1.default.isIdentifier(decl.name) || !decl.type)
|
|
91
|
-
continue;
|
|
92
|
-
const name = decl.name.text;
|
|
93
|
-
const val = parseLiteralFromType(decl.type);
|
|
94
|
-
if (val == null)
|
|
95
|
-
continue;
|
|
96
|
-
results.push({ kind: 'const', name, value: val });
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
const handleClass = (cls) => {
|
|
100
|
-
var _a, _b, _c;
|
|
101
|
-
const container = (_a = cls.name) === null || _a === void 0 ? void 0 : _a.text;
|
|
102
|
-
if (!container)
|
|
103
|
-
return;
|
|
104
|
-
for (const m of cls.members) {
|
|
105
|
-
if (!typescript_1.default.isPropertyDeclaration(m))
|
|
106
|
-
continue;
|
|
107
|
-
const isStatic = (_b = m.modifiers) === null || _b === void 0 ? void 0 : _b.some(mod => mod.kind === typescript_1.default.SyntaxKind.StaticKeyword);
|
|
108
|
-
const isReadonly = (_c = m.modifiers) === null || _c === void 0 ? void 0 : _c.some(mod => mod.kind === typescript_1.default.SyntaxKind.ReadonlyKeyword);
|
|
109
|
-
if (!isStatic || !isReadonly || !m.type)
|
|
110
|
-
continue;
|
|
111
|
-
if (!typescript_1.default.isIdentifier(m.name))
|
|
112
|
-
continue;
|
|
113
|
-
const name = m.name.text;
|
|
114
|
-
const val = parseLiteralFromType(m.type);
|
|
115
|
-
if (val == null)
|
|
116
|
-
continue;
|
|
117
|
-
results.push({ kind: 'container', container, name, value: val });
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
const handleModule = (mod) => {
|
|
121
|
-
const container = mod.name.getText(source).replace(/['"]/g, '');
|
|
122
|
-
if (!mod.body || !typescript_1.default.isModuleBlock(mod.body))
|
|
123
|
-
return;
|
|
124
|
-
for (const stmt of mod.body.statements) {
|
|
125
|
-
if (typescript_1.default.isVariableStatement(stmt)) {
|
|
126
|
-
if ((stmt.declarationList.flags & typescript_1.default.NodeFlags.Const) === 0)
|
|
127
|
-
continue;
|
|
128
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
129
|
-
if (!typescript_1.default.isIdentifier(decl.name) || !decl.type)
|
|
130
|
-
continue;
|
|
131
|
-
const name = decl.name.text;
|
|
132
|
-
const val = parseLiteralFromType(decl.type);
|
|
133
|
-
if (val == null)
|
|
134
|
-
continue;
|
|
135
|
-
results.push({ kind: 'container', container, name, value: val });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
for (const stmt of source.statements) {
|
|
141
|
-
if (typescript_1.default.isVariableStatement(stmt))
|
|
142
|
-
handleVariableStatement(stmt);
|
|
143
|
-
else if (typescript_1.default.isClassDeclaration(stmt))
|
|
144
|
-
handleClass(stmt);
|
|
145
|
-
else if (typescript_1.default.isModuleDeclaration(stmt))
|
|
146
|
-
handleModule(stmt);
|
|
147
|
-
}
|
|
148
|
-
return results;
|
|
149
|
-
}
|
|
150
|
-
function collectDeclaredConsts(blobs) {
|
|
151
|
-
const all = [];
|
|
152
|
-
for (const blob of blobs) {
|
|
153
|
-
const raw = blob.raw || '';
|
|
154
|
-
if (!raw)
|
|
155
|
-
continue;
|
|
156
|
-
try {
|
|
157
|
-
const items = collectDeclaredConstsFromSource(raw, blob.filename + '.d.ts');
|
|
158
|
-
all.push(...items);
|
|
159
|
-
}
|
|
160
|
-
catch (_) {
|
|
161
|
-
// ignore parse errors per file
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return all;
|
|
165
|
-
}
|
|
166
|
-
function literalToTsValue(val) {
|
|
167
|
-
if (typeof val === 'string')
|
|
168
|
-
return `'${val.replace(/'/g, "\\'")}'`;
|
|
169
|
-
if (typeof val === 'number')
|
|
170
|
-
return String(val);
|
|
171
|
-
if (typeof val === 'boolean')
|
|
172
|
-
return val ? 'true' : 'false';
|
|
173
|
-
if (typeof val.typeofRef === 'string')
|
|
174
|
-
return val.typeofRef;
|
|
175
|
-
return 'undefined';
|
|
176
|
-
}
|
|
177
|
-
function generateDeclaredConstantsValuesTs(blobs) {
|
|
178
|
-
const items = collectDeclaredConsts(blobs);
|
|
179
|
-
if (!items.length)
|
|
180
|
-
return '';
|
|
181
|
-
const header = `// Auto-generated by WebF CLI\n// Runtime constants mirrored from .d.ts 'declare const' definitions\n`;
|
|
182
|
-
const topLevel = [];
|
|
183
|
-
const containers = new Map();
|
|
184
|
-
for (const it of items) {
|
|
185
|
-
if (it.kind === 'const') {
|
|
186
|
-
topLevel.push(`export const ${it.name} = ${literalToTsValue(it.value)} as const;`);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
if (!containers.has(it.container))
|
|
190
|
-
containers.set(it.container, []);
|
|
191
|
-
containers.get(it.container).push(it);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const containerBlocks = [];
|
|
195
|
-
for (const [container, arr] of containers) {
|
|
196
|
-
const lines = arr
|
|
197
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
198
|
-
.map(a => ` ${a.name}: ${literalToTsValue(a.value)},`) // keep plain object
|
|
199
|
-
.join('\n');
|
|
200
|
-
containerBlocks.push(`export const ${container} = {\n${lines}\n} as const;`);
|
|
201
|
-
}
|
|
202
|
-
return [header, ...topLevel, ...containerBlocks].filter(Boolean).join('\n\n') + '\n';
|
|
203
|
-
}
|
|
204
|
-
function generateDeclaredConstantsDts(blobs) {
|
|
205
|
-
const items = collectDeclaredConsts(blobs);
|
|
206
|
-
if (!items.length)
|
|
207
|
-
return '';
|
|
208
|
-
const header = `// Auto-generated by WebF CLI\n// Type declarations for 'declare const' values mirrored into JS runtime\n`;
|
|
209
|
-
const topLevel = [];
|
|
210
|
-
const containers = new Map();
|
|
211
|
-
for (const it of items) {
|
|
212
|
-
if (it.kind === 'const') {
|
|
213
|
-
const val = typeof it.value === 'object' && it.value.typeofRef
|
|
214
|
-
? `typeof ${it.value.typeofRef}`
|
|
215
|
-
: typeof it.value === 'string'
|
|
216
|
-
? `'${it.value.replace(/'/g, "\\'")}'`
|
|
217
|
-
: String(it.value);
|
|
218
|
-
topLevel.push(`export declare const ${it.name}: ${val};`);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
if (!containers.has(it.container))
|
|
222
|
-
containers.set(it.container, []);
|
|
223
|
-
containers.get(it.container).push(it);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const containerBlocks = [];
|
|
227
|
-
for (const [container, arr] of containers) {
|
|
228
|
-
const lines = arr
|
|
229
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
230
|
-
.map(a => {
|
|
231
|
-
const v = typeof a.value === 'object' && a.value.typeofRef
|
|
232
|
-
? `typeof ${a.value.typeofRef}`
|
|
233
|
-
: typeof a.value === 'string'
|
|
234
|
-
? `'${a.value.replace(/'/g, "\\'")}'`
|
|
235
|
-
: String(a.value);
|
|
236
|
-
return ` readonly ${a.name}: ${v};`;
|
|
237
|
-
})
|
|
238
|
-
.join('\n');
|
|
239
|
-
containerBlocks.push(`export declare const ${container}: {\n${lines}\n};`);
|
|
240
|
-
}
|
|
241
|
-
return [header, ...topLevel, ...containerBlocks].filter(Boolean).join('\n\n') + '\n';
|
|
242
|
-
}
|