@fragments-sdk/cli 0.14.3 → 0.15.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 +0 -3
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +4745 -3817
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/codebase-scanner-MQHUZC2G.js +21 -0
- package/dist/converter-7XM3Y6NJ.js +33 -0
- package/dist/converter-7XM3Y6NJ.js.map +1 -0
- package/dist/core/index.js +43 -2
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
- package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/govern-scan-OYFZYOQW.js +413 -0
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
- package/dist/init-SSGUSP7Z.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
- package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +5 -37
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/scan-PKSYSTRR.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/scanner-4KZNOXAK.js +12 -0
- package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
- package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
- package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
- package/dist/static-viewer-7QIBQZRC.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
- package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
- package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
- package/dist/tokens-generate-5JQSJ27E.js +85 -0
- package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +8 -6
- package/src/bin.ts +300 -48
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +189 -70
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +565 -0
- package/src/commands/govern.ts +67 -4
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +439 -51
- package/src/commands/scan.ts +14 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/tokens-push.ts +199 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +40 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/index.ts +3 -3
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/index.ts +8 -0
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-normalizer.ts +510 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js +0 -626
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js +0 -2453
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/build.ts +0 -612
- package/src/commands/dev.ts +0 -107
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1030
- package/src/core/token-resolver.ts +0 -155
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
package/src/commands/dev.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fragments dev - Start the development server with live component rendering
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import pc from 'picocolors';
|
|
6
|
-
import { BRAND } from '../core/index.js';
|
|
7
|
-
import { runSetup } from '../setup.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for dev command
|
|
11
|
-
*/
|
|
12
|
-
export interface DevOptions {
|
|
13
|
-
/** Port to run on */
|
|
14
|
-
port?: number | string;
|
|
15
|
-
/** Do not open browser */
|
|
16
|
-
open?: boolean;
|
|
17
|
-
/** Skip auto-setup (Storybook import, build, Figma link) */
|
|
18
|
-
skipSetup?: boolean;
|
|
19
|
-
/** Skip auto-importing from Storybook */
|
|
20
|
-
skipStorybook?: boolean;
|
|
21
|
-
/** Skip Figma link check */
|
|
22
|
-
skipFigma?: boolean;
|
|
23
|
-
/** Skip auto-building fragments.json */
|
|
24
|
-
skipBuild?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Result of dev command
|
|
29
|
-
*/
|
|
30
|
-
export interface DevResult {
|
|
31
|
-
success: boolean;
|
|
32
|
-
port?: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Run the dev command
|
|
37
|
-
*/
|
|
38
|
-
export async function dev(options: DevOptions = {}): Promise<DevResult> {
|
|
39
|
-
const {
|
|
40
|
-
port = 6006,
|
|
41
|
-
open = true,
|
|
42
|
-
skipSetup = false,
|
|
43
|
-
skipStorybook = false,
|
|
44
|
-
skipFigma = false,
|
|
45
|
-
skipBuild = false,
|
|
46
|
-
} = options;
|
|
47
|
-
|
|
48
|
-
console.log(pc.cyan(`\n${BRAND.name} Dev Server\n`));
|
|
49
|
-
|
|
50
|
-
// Run auto-setup unless skipped
|
|
51
|
-
if (!skipSetup) {
|
|
52
|
-
const setupResult = await runSetup({
|
|
53
|
-
skipStorybook,
|
|
54
|
-
skipFigma,
|
|
55
|
-
skipBuild,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Show errors if any
|
|
59
|
-
if (setupResult.errors.length > 0) {
|
|
60
|
-
console.log(pc.yellow('\n Setup completed with warnings:'));
|
|
61
|
-
for (const error of setupResult.errors) {
|
|
62
|
-
console.log(pc.dim(` ${error}`));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Import viewer directly (it's a dependency of CLI)
|
|
68
|
-
const { createDevServer } = await import('../viewer/index.js');
|
|
69
|
-
|
|
70
|
-
console.log(pc.dim('\nStarting dev server...'));
|
|
71
|
-
|
|
72
|
-
const parsedPort = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const server = await createDevServer({
|
|
76
|
-
port: parsedPort,
|
|
77
|
-
open,
|
|
78
|
-
projectRoot: process.cwd(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const address = server.httpServer?.address();
|
|
82
|
-
const actualPort =
|
|
83
|
-
typeof address === 'object' && address ? address.port : parsedPort;
|
|
84
|
-
|
|
85
|
-
console.log(pc.green(`\n Viewer running at http://localhost:${actualPort}/fragments/\n`));
|
|
86
|
-
console.log(pc.dim('Press Ctrl+C to stop\n'));
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
success: true,
|
|
90
|
-
port: typeof actualPort === 'number' ? actualPort : parseInt(String(actualPort), 10),
|
|
91
|
-
};
|
|
92
|
-
} catch (error) {
|
|
93
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
94
|
-
|
|
95
|
-
if (errMsg.includes('EADDRINUSE') || errMsg.includes('address already in use')) {
|
|
96
|
-
console.error(pc.red(`\n Port ${parsedPort} is already in use.`));
|
|
97
|
-
console.error(pc.dim(` Try a different port: ${BRAND.cliCommand} dev --port ${parsedPort + 1}\n`));
|
|
98
|
-
} else if (errMsg.includes('EACCES')) {
|
|
99
|
-
console.error(pc.red(`\n Permission denied for port ${parsedPort}.`));
|
|
100
|
-
console.error(pc.dim(` Try a port above 1024: ${BRAND.cliCommand} dev --port 6006\n`));
|
|
101
|
-
} else {
|
|
102
|
-
console.error(pc.red(`\n Failed to start dev server: ${errMsg}\n`));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { success: false };
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/core/auto-props.ts
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
3
|
-
import ts from "typescript";
|
|
4
|
-
import type { PropDefinition } from '@fragments-sdk/core';
|
|
5
|
-
|
|
6
|
-
export interface AutoDetectedPropDefinition {
|
|
7
|
-
type: PropDefinition["type"];
|
|
8
|
-
description: string;
|
|
9
|
-
required: boolean;
|
|
10
|
-
default?: unknown;
|
|
11
|
-
values?: readonly string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AutoPropsExtractionResult {
|
|
15
|
-
props: Record<string, AutoDetectedPropDefinition>;
|
|
16
|
-
warnings: string[];
|
|
17
|
-
resolved: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface ResolvedComponentSignature {
|
|
21
|
-
propsTypeNode: ts.TypeNode | null;
|
|
22
|
-
componentNode: ts.FunctionLikeDeclarationBase | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function toPosixPath(filePath: string): string {
|
|
26
|
-
return filePath.replace(/\\/g, "/");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function isFile(filePath: string): boolean {
|
|
30
|
-
if (!existsSync(filePath)) return false;
|
|
31
|
-
try {
|
|
32
|
-
return statSync(filePath).isFile();
|
|
33
|
-
} catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveModulePath(basePath: string): string | null {
|
|
39
|
-
const candidates: string[] = [];
|
|
40
|
-
const extension = extname(basePath);
|
|
41
|
-
|
|
42
|
-
if (extension) {
|
|
43
|
-
candidates.push(basePath);
|
|
44
|
-
} else {
|
|
45
|
-
candidates.push(
|
|
46
|
-
`${basePath}.tsx`,
|
|
47
|
-
`${basePath}.ts`,
|
|
48
|
-
`${basePath}.jsx`,
|
|
49
|
-
`${basePath}.js`,
|
|
50
|
-
join(basePath, "index.tsx"),
|
|
51
|
-
join(basePath, "index.ts"),
|
|
52
|
-
join(basePath, "index.jsx"),
|
|
53
|
-
join(basePath, "index.js")
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const candidate of candidates) {
|
|
58
|
-
if (isFile(candidate)) {
|
|
59
|
-
return resolve(candidate);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Resolve a component source file from the fragment file path + component import path.
|
|
68
|
-
* Supports relative imports like ".", "./Button", "../components/Button".
|
|
69
|
-
*/
|
|
70
|
-
export function resolveComponentSourcePath(
|
|
71
|
-
fragmentFileAbsolutePath: string,
|
|
72
|
-
componentImportPath: string | null
|
|
73
|
-
): string | null {
|
|
74
|
-
if (!componentImportPath) return null;
|
|
75
|
-
if (!componentImportPath.startsWith(".")) return null;
|
|
76
|
-
|
|
77
|
-
const fragmentDir = dirname(fragmentFileAbsolutePath);
|
|
78
|
-
const basePath = resolve(fragmentDir, componentImportPath);
|
|
79
|
-
return resolveModulePath(basePath);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function collectTopLevelDeclarations(sourceFile: ts.SourceFile): {
|
|
83
|
-
typeDeclarations: Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>;
|
|
84
|
-
functionDeclarations: Map<string, ts.FunctionDeclaration>;
|
|
85
|
-
variableDeclarations: Map<string, ts.VariableDeclaration>;
|
|
86
|
-
} {
|
|
87
|
-
const typeDeclarations = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
|
|
88
|
-
const functionDeclarations = new Map<string, ts.FunctionDeclaration>();
|
|
89
|
-
const variableDeclarations = new Map<string, ts.VariableDeclaration>();
|
|
90
|
-
|
|
91
|
-
for (const node of sourceFile.statements) {
|
|
92
|
-
if (ts.isInterfaceDeclaration(node)) {
|
|
93
|
-
typeDeclarations.set(node.name.text, node);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (ts.isTypeAliasDeclaration(node)) {
|
|
98
|
-
typeDeclarations.set(node.name.text, node);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
103
|
-
functionDeclarations.set(node.name.text, node);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (ts.isVariableStatement(node)) {
|
|
108
|
-
for (const declaration of node.declarationList.declarations) {
|
|
109
|
-
if (ts.isIdentifier(declaration.name)) {
|
|
110
|
-
variableDeclarations.set(declaration.name.text, declaration);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { typeDeclarations, functionDeclarations, variableDeclarations };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function readDefaultValue(expression: ts.Expression): unknown {
|
|
120
|
-
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
121
|
-
return expression.text;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (ts.isNumericLiteral(expression)) {
|
|
125
|
-
return Number(expression.text);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
129
|
-
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
130
|
-
if (expression.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
ts.isPrefixUnaryExpression(expression) &&
|
|
134
|
-
expression.operator === ts.SyntaxKind.MinusToken &&
|
|
135
|
-
ts.isNumericLiteral(expression.operand)
|
|
136
|
-
) {
|
|
137
|
-
return -Number(expression.operand.text);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function extractDefaultValues(
|
|
144
|
-
componentNode: ts.FunctionLikeDeclarationBase | null
|
|
145
|
-
): Record<string, unknown> {
|
|
146
|
-
const defaults: Record<string, unknown> = {};
|
|
147
|
-
if (!componentNode?.parameters?.length) return defaults;
|
|
148
|
-
|
|
149
|
-
const firstParam = componentNode.parameters[0];
|
|
150
|
-
if (!ts.isObjectBindingPattern(firstParam.name)) return defaults;
|
|
151
|
-
|
|
152
|
-
for (const element of firstParam.name.elements) {
|
|
153
|
-
let propName: string | null = null;
|
|
154
|
-
|
|
155
|
-
if (element.propertyName) {
|
|
156
|
-
if (ts.isIdentifier(element.propertyName) || ts.isStringLiteral(element.propertyName)) {
|
|
157
|
-
propName = element.propertyName.text;
|
|
158
|
-
}
|
|
159
|
-
} else if (ts.isIdentifier(element.name)) {
|
|
160
|
-
propName = element.name.text;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!propName || !element.initializer) continue;
|
|
164
|
-
|
|
165
|
-
const value = readDefaultValue(element.initializer);
|
|
166
|
-
if (value !== undefined) {
|
|
167
|
-
defaults[propName] = value;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return defaults;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function isNullishType(type: ts.Type): boolean {
|
|
175
|
-
return (
|
|
176
|
-
(type.flags & ts.TypeFlags.Null) !== 0 ||
|
|
177
|
-
(type.flags & ts.TypeFlags.Undefined) !== 0 ||
|
|
178
|
-
(type.flags & ts.TypeFlags.Void) !== 0
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isBooleanLikeType(type: ts.Type): boolean {
|
|
183
|
-
return (
|
|
184
|
-
(type.flags & ts.TypeFlags.BooleanLike) !== 0 ||
|
|
185
|
-
type.flags === ts.TypeFlags.BooleanLiteral
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function inferPropType(
|
|
190
|
-
type: ts.Type,
|
|
191
|
-
checker: ts.TypeChecker
|
|
192
|
-
): Pick<AutoDetectedPropDefinition, "type" | "values"> {
|
|
193
|
-
const typeText = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
194
|
-
|
|
195
|
-
if (typeText.includes("ReactNode")) {
|
|
196
|
-
return { type: "node" };
|
|
197
|
-
}
|
|
198
|
-
if (typeText.includes("ReactElement") || typeText.includes("JSX.Element")) {
|
|
199
|
-
return { type: "element" };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (type.getCallSignatures().length > 0) {
|
|
203
|
-
return { type: "function" };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
207
|
-
return { type: "array" };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (type.isUnion()) {
|
|
211
|
-
const nonNullableTypes = type.types.filter((unionType) => !isNullishType(unionType));
|
|
212
|
-
|
|
213
|
-
if (nonNullableTypes.length === 1) {
|
|
214
|
-
return inferPropType(nonNullableTypes[0], checker);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const stringLiteralValues = nonNullableTypes
|
|
218
|
-
.filter((unionType) => (unionType.flags & ts.TypeFlags.StringLiteral) !== 0)
|
|
219
|
-
.map((unionType) => (unionType as ts.StringLiteralType).value);
|
|
220
|
-
|
|
221
|
-
if (stringLiteralValues.length > 0 && stringLiteralValues.length === nonNullableTypes.length) {
|
|
222
|
-
return { type: "enum", values: stringLiteralValues };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (nonNullableTypes.every((unionType) => isBooleanLikeType(unionType))) {
|
|
226
|
-
return { type: "boolean" };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return { type: "union" };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
|
|
233
|
-
return { type: "string" };
|
|
234
|
-
}
|
|
235
|
-
if ((type.flags & ts.TypeFlags.NumberLike) !== 0) {
|
|
236
|
-
return { type: "number" };
|
|
237
|
-
}
|
|
238
|
-
if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) {
|
|
239
|
-
return { type: "boolean" };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if ((type.flags & ts.TypeFlags.Object) !== 0) {
|
|
243
|
-
return { type: "object" };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { type: "custom" };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function resolveComponentSignature(
|
|
250
|
-
exportName: string,
|
|
251
|
-
declarations: ReturnType<typeof collectTopLevelDeclarations>,
|
|
252
|
-
sourceFile: ts.SourceFile
|
|
253
|
-
): ResolvedComponentSignature {
|
|
254
|
-
const visitedNames = new Set<string>();
|
|
255
|
-
|
|
256
|
-
const typeNodeFromFunction = (
|
|
257
|
-
node: ts.FunctionLikeDeclarationBase
|
|
258
|
-
): ResolvedComponentSignature => ({
|
|
259
|
-
propsTypeNode: node.parameters[0]?.type ?? null,
|
|
260
|
-
componentNode: node,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const resolveFromExpression = (expression: ts.Expression): ResolvedComponentSignature => {
|
|
264
|
-
if (ts.isParenthesizedExpression(expression)) {
|
|
265
|
-
return resolveFromExpression(expression.expression);
|
|
266
|
-
}
|
|
267
|
-
if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
|
|
268
|
-
return resolveFromExpression(expression.expression);
|
|
269
|
-
}
|
|
270
|
-
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
271
|
-
return typeNodeFromFunction(expression);
|
|
272
|
-
}
|
|
273
|
-
if (ts.isIdentifier(expression)) {
|
|
274
|
-
return resolveFromIdentifier(expression.text);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (ts.isCallExpression(expression)) {
|
|
278
|
-
if (
|
|
279
|
-
ts.isPropertyAccessExpression(expression.expression) &&
|
|
280
|
-
expression.expression.name.text === "forwardRef"
|
|
281
|
-
) {
|
|
282
|
-
const forwardRefPropsType = expression.typeArguments?.[1] ?? null;
|
|
283
|
-
const innerArg = expression.arguments[0];
|
|
284
|
-
const inner = innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))
|
|
285
|
-
? typeNodeFromFunction(innerArg)
|
|
286
|
-
: innerArg && ts.isIdentifier(innerArg)
|
|
287
|
-
? resolveFromIdentifier(innerArg.text)
|
|
288
|
-
: { propsTypeNode: null, componentNode: null };
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
propsTypeNode: forwardRefPropsType ?? inner.propsTypeNode,
|
|
292
|
-
componentNode: inner.componentNode,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
ts.isPropertyAccessExpression(expression.expression) &&
|
|
298
|
-
expression.expression.name.text === "memo" &&
|
|
299
|
-
expression.arguments[0]
|
|
300
|
-
) {
|
|
301
|
-
return resolveFromExpression(expression.arguments[0]);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
ts.isPropertyAccessExpression(expression.expression) &&
|
|
306
|
-
expression.expression.expression.getText(sourceFile) === "Object" &&
|
|
307
|
-
expression.expression.name.text === "assign" &&
|
|
308
|
-
expression.arguments[0]
|
|
309
|
-
) {
|
|
310
|
-
return resolveFromExpression(expression.arguments[0]);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return { propsTypeNode: null, componentNode: null };
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const resolveFromVariable = (declaration: ts.VariableDeclaration): ResolvedComponentSignature => {
|
|
318
|
-
if (
|
|
319
|
-
declaration.type &&
|
|
320
|
-
ts.isTypeReferenceNode(declaration.type) &&
|
|
321
|
-
declaration.type.typeArguments?.length
|
|
322
|
-
) {
|
|
323
|
-
const typeName = declaration.type.typeName.getText(sourceFile);
|
|
324
|
-
if (typeName.includes("FC") || typeName.includes("FunctionComponent")) {
|
|
325
|
-
const componentNode =
|
|
326
|
-
declaration.initializer &&
|
|
327
|
-
(ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
|
|
328
|
-
? declaration.initializer
|
|
329
|
-
: null;
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
propsTypeNode: declaration.type.typeArguments[0] ?? null,
|
|
333
|
-
componentNode,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (declaration.initializer) {
|
|
339
|
-
return resolveFromExpression(declaration.initializer);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return { propsTypeNode: null, componentNode: null };
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const resolveFromIdentifier = (name: string): ResolvedComponentSignature => {
|
|
346
|
-
if (!name || visitedNames.has(name)) {
|
|
347
|
-
return { propsTypeNode: null, componentNode: null };
|
|
348
|
-
}
|
|
349
|
-
visitedNames.add(name);
|
|
350
|
-
|
|
351
|
-
const functionDeclaration = declarations.functionDeclarations.get(name);
|
|
352
|
-
if (functionDeclaration) {
|
|
353
|
-
return typeNodeFromFunction(functionDeclaration);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const variableDeclaration = declarations.variableDeclarations.get(name);
|
|
357
|
-
if (variableDeclaration) {
|
|
358
|
-
return resolveFromVariable(variableDeclaration);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return { propsTypeNode: null, componentNode: null };
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
return resolveFromIdentifier(exportName);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Extract custom component props from a source file.
|
|
369
|
-
* Custom props are identified as props whose declarations originate from the component's source file.
|
|
370
|
-
*/
|
|
371
|
-
export function extractCustomPropsFromComponentFile(
|
|
372
|
-
componentFilePath: string,
|
|
373
|
-
exportName: string
|
|
374
|
-
): AutoPropsExtractionResult {
|
|
375
|
-
const warnings: string[] = [];
|
|
376
|
-
const resolvedPath = resolve(componentFilePath);
|
|
377
|
-
|
|
378
|
-
if (!existsSync(resolvedPath)) {
|
|
379
|
-
return {
|
|
380
|
-
props: {},
|
|
381
|
-
warnings: [`Component file not found: ${resolvedPath}`],
|
|
382
|
-
resolved: false,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const compilerOptions: ts.CompilerOptions = {
|
|
387
|
-
target: ts.ScriptTarget.ESNext,
|
|
388
|
-
module: ts.ModuleKind.ESNext,
|
|
389
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
390
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
391
|
-
allowSyntheticDefaultImports: true,
|
|
392
|
-
esModuleInterop: true,
|
|
393
|
-
skipLibCheck: true,
|
|
394
|
-
strict: false,
|
|
395
|
-
noEmit: true,
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const program = ts.createProgram([resolvedPath], compilerOptions);
|
|
399
|
-
const sourceFile = program.getSourceFile(resolvedPath);
|
|
400
|
-
if (!sourceFile) {
|
|
401
|
-
return {
|
|
402
|
-
props: {},
|
|
403
|
-
warnings: [`Unable to parse component source: ${resolvedPath}`],
|
|
404
|
-
resolved: false,
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const checker = program.getTypeChecker();
|
|
409
|
-
const declarations = collectTopLevelDeclarations(sourceFile);
|
|
410
|
-
const signature = resolveComponentSignature(exportName, declarations, sourceFile);
|
|
411
|
-
|
|
412
|
-
if (!signature.propsTypeNode) {
|
|
413
|
-
return {
|
|
414
|
-
props: {},
|
|
415
|
-
warnings: [`Unable to resolve props type for export: ${exportName}`],
|
|
416
|
-
resolved: false,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const propsType = checker.getTypeFromTypeNode(signature.propsTypeNode);
|
|
421
|
-
const defaultValues = extractDefaultValues(signature.componentNode);
|
|
422
|
-
const sourceFilePath = toPosixPath(sourceFile.fileName);
|
|
423
|
-
|
|
424
|
-
const extractedProps: Record<string, AutoDetectedPropDefinition> = {};
|
|
425
|
-
for (const symbol of checker.getPropertiesOfType(propsType)) {
|
|
426
|
-
const propName = symbol.getName();
|
|
427
|
-
if (propName.startsWith("_") || propName.startsWith("$")) {
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const declarationsForSymbol = symbol.getDeclarations() ?? [];
|
|
432
|
-
const localDeclarations = declarationsForSymbol.filter(
|
|
433
|
-
(declaration) => toPosixPath(declaration.getSourceFile().fileName) === sourceFilePath
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
if (localDeclarations.length === 0) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const referenceNode = localDeclarations[0];
|
|
441
|
-
const inferredType = inferPropType(checker.getTypeOfSymbolAtLocation(symbol, referenceNode), checker);
|
|
442
|
-
const description = ts
|
|
443
|
-
.displayPartsToString(symbol.getDocumentationComment(checker))
|
|
444
|
-
.trim();
|
|
445
|
-
|
|
446
|
-
extractedProps[propName] = {
|
|
447
|
-
type: inferredType.type,
|
|
448
|
-
description,
|
|
449
|
-
required: (symbol.getFlags() & ts.SymbolFlags.Optional) === 0,
|
|
450
|
-
...(inferredType.values && { values: inferredType.values }),
|
|
451
|
-
...(defaultValues[propName] !== undefined && { default: defaultValues[propName] }),
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (Object.keys(extractedProps).length === 0) {
|
|
456
|
-
warnings.push(`Resolved props type for ${exportName}, but no local custom props were found`);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
props: extractedProps,
|
|
461
|
-
warnings,
|
|
462
|
-
resolved: true,
|
|
463
|
-
};
|
|
464
|
-
}
|