@xyd-js/source-react-runtime 0.0.0-build-23166ce-20260423151359
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/package.json +8 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/src/UserCard.tsx +27 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/src/index.ts +1 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/tsconfig.json +12 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/vite.config.ts +22 -0
- package/__fixtures__/-1.vite-lib.custom-property/output.js +10 -0
- package/__fixtures__/1.vite-lib.user-card/input/package.json +8 -0
- package/__fixtures__/1.vite-lib.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/1.vite-lib.user-card/input/src/index.ts +1 -0
- package/__fixtures__/1.vite-lib.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/1.vite-lib.user-card/input/vite.config.ts +22 -0
- package/__fixtures__/1.vite-lib.user-card/output.js +10 -0
- package/__fixtures__/2.vite-lib.sample-app/input/package.json +8 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/components/UserProfile.tsx +42 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/contexts/UserContext.tsx +27 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/index.ts +3 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/types/user.ts +18 -0
- package/__fixtures__/2.vite-lib.sample-app/input/tsconfig.json +12 -0
- package/__fixtures__/2.vite-lib.sample-app/input/vite.config.ts +22 -0
- package/__fixtures__/2.vite-lib.sample-app/output.js +27 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/package.json +8 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/AddTodoForm.tsx +51 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/FilterBar.tsx +56 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/StatsPanel.tsx +44 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/TodoItem.tsx +54 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/index.ts +6 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/types/todo.ts +23 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/tsconfig.json +12 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/vite.config.ts +22 -0
- package/__fixtures__/3.vite-lib.sample-real-app/output.js +63 -0
- package/__fixtures__/4.vite-app.user-card/input/package.json +8 -0
- package/__fixtures__/4.vite-app.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/4.vite-app.user-card/input/src/index.ts +1 -0
- package/__fixtures__/4.vite-app.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/4.vite-app.user-card/input/vite.config.ts +23 -0
- package/__fixtures__/4.vite-app.user-card/output.js +10 -0
- package/__fixtures__/5.rollup.user-card/input/package.json +8 -0
- package/__fixtures__/5.rollup.user-card/input/rollup.config.mjs +20 -0
- package/__fixtures__/5.rollup.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/5.rollup.user-card/input/src/index.ts +1 -0
- package/__fixtures__/5.rollup.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/5.rollup.user-card/output.js +11 -0
- package/__fixtures__/6.esbuild.user-card/input/esbuild.config.mjs +19 -0
- package/__fixtures__/6.esbuild.user-card/input/package.json +8 -0
- package/__fixtures__/6.esbuild.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/6.esbuild.user-card/input/src/index.ts +1 -0
- package/__fixtures__/6.esbuild.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/6.esbuild.user-card/output.js +11 -0
- package/__fixtures__/7.react-router.app/input/app/components/ProductCard.tsx +26 -0
- package/__fixtures__/7.react-router.app/input/app/entry.server.tsx +36 -0
- package/__fixtures__/7.react-router.app/input/app/root.tsx +23 -0
- package/__fixtures__/7.react-router.app/input/app/routes/cart.tsx +16 -0
- package/__fixtures__/7.react-router.app/input/app/routes/home.tsx +29 -0
- package/__fixtures__/7.react-router.app/input/app/routes/product.tsx +6 -0
- package/__fixtures__/7.react-router.app/input/app/routes.ts +7 -0
- package/__fixtures__/7.react-router.app/input/app/types/product.ts +12 -0
- package/__fixtures__/7.react-router.app/input/package.json +8 -0
- package/__fixtures__/7.react-router.app/input/react-router.config.ts +2 -0
- package/__fixtures__/7.react-router.app/input/tsconfig.json +13 -0
- package/__fixtures__/7.react-router.app/input/vite.config.ts +14 -0
- package/__fixtures__/7.react-router.app/output.js +44 -0
- package/__fixtures__/8.tanstack-router.app/input/index.html +5 -0
- package/__fixtures__/8.tanstack-router.app/input/package.json +8 -0
- package/__fixtures__/8.tanstack-router.app/input/src/components/EmployeeTable.tsx +45 -0
- package/__fixtures__/8.tanstack-router.app/input/src/main.tsx +19 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routeTree.gen.ts +77 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/__root.tsx +13 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/employees.tsx +23 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/index.tsx +5 -0
- package/__fixtures__/8.tanstack-router.app/input/src/types/employee.ts +13 -0
- package/__fixtures__/8.tanstack-router.app/input/tsconfig.json +12 -0
- package/__fixtures__/8.tanstack-router.app/input/vite.config.ts +21 -0
- package/__fixtures__/8.tanstack-router.app/output.js +63 -0
- package/__tests__/source-react-runtime.test.ts +61 -0
- package/__tests__/utils.ts +100 -0
- package/dist/esbuild.d.ts +20 -0
- package/dist/esbuild.js +378 -0
- package/dist/esbuild.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +348 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/esbuild.ts +45 -0
- package/src/index.ts +437 -0
- package/src/json-schema-to-uniform.ts +108 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsup.json +6 -0
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +7 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import type {Plugin} from 'vite';
|
|
4
|
+
import {jsonSchemaToUniformReference} from './json-schema-to-uniform';
|
|
5
|
+
|
|
6
|
+
export {jsonSchemaToUniformReference} from './json-schema-to-uniform';
|
|
7
|
+
|
|
8
|
+
export interface XydSourceReactRuntimeOptions {
|
|
9
|
+
/** Path to tsconfig.json. Required for typia's TypeScript transform. */
|
|
10
|
+
tsconfig?: string;
|
|
11
|
+
/** Property name for the injected uniform data. Defaults to `__xydUniform`. */
|
|
12
|
+
propertyName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ComponentInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
propsType: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Vite plugin that auto-detects React components, uses typia to generate
|
|
23
|
+
* JSON Schema from their props at build time, and injects `__xydUniform`.
|
|
24
|
+
*
|
|
25
|
+
* No manual annotations needed — the plugin:
|
|
26
|
+
* 1. Scans source files for exported functions with typed props
|
|
27
|
+
* 2. Injects `typia.json.schemas<[PropsType]>()` calls
|
|
28
|
+
* 3. Runs typia's TS transform to resolve all types (cross-file, generics, etc.)
|
|
29
|
+
* 4. Converts JSON Schema → xyd uniform format
|
|
30
|
+
* 5. Injects `Component.__xydUniform = JSON.parse('...')`
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import react from '@vitejs/plugin-react';
|
|
34
|
+
* import { xydSourceReactRuntime } from '@xyd-js/source-react-runtime';
|
|
35
|
+
*
|
|
36
|
+
* export default defineConfig({
|
|
37
|
+
* plugins: [
|
|
38
|
+
* xydSourceReactRuntime({ tsconfig: './tsconfig.json' }),
|
|
39
|
+
* react(),
|
|
40
|
+
* ],
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function xydSourceReactRuntime(options?: XydSourceReactRuntimeOptions): Plugin {
|
|
45
|
+
// Map of fileName → typia-transformed output with __xydSchema injected
|
|
46
|
+
let transformedFiles: Map<string, string> = new Map();
|
|
47
|
+
const propName = options?.propertyName || '__xydUniform';
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: 'xyd-source-react-runtime',
|
|
51
|
+
enforce: 'pre',
|
|
52
|
+
|
|
53
|
+
async buildStart() {
|
|
54
|
+
const tsconfigPath = options?.tsconfig || path.resolve(process.cwd(), 'tsconfig.json');
|
|
55
|
+
transformedFiles = await buildTypiaSchemas(tsconfigPath);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Vite path: transform hook receives the original code
|
|
59
|
+
transform(code, id) {
|
|
60
|
+
if (!id.match(/\.[jt]sx?$/)) return null;
|
|
61
|
+
|
|
62
|
+
const transformed = transformedFiles.get(path.resolve(id));
|
|
63
|
+
if (!transformed) return null;
|
|
64
|
+
|
|
65
|
+
return convertSchemaToUniform(transformed, propName);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Rollup path: load hook returns our compiled JS before Rollup's parser
|
|
69
|
+
load(id) {
|
|
70
|
+
if (!id.match(/\.[jt]sx?$/)) return null;
|
|
71
|
+
|
|
72
|
+
const transformed = transformedFiles.get(path.resolve(id));
|
|
73
|
+
if (!transformed) return null;
|
|
74
|
+
|
|
75
|
+
return convertSchemaToUniform(transformed, propName);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scans all source files, detects React components, injects typia calls,
|
|
82
|
+
* and runs typia transform to generate JSON schemas.
|
|
83
|
+
*/
|
|
84
|
+
async function buildTypiaSchemas(tsconfigPath: string): Promise<Map<string, string>> {
|
|
85
|
+
const result = new Map<string, string>();
|
|
86
|
+
|
|
87
|
+
let ts: typeof import('typescript');
|
|
88
|
+
let typiaTransformModule: any;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
ts = await import('typescript');
|
|
92
|
+
typiaTransformModule = await import('typia/lib/transform');
|
|
93
|
+
} catch {
|
|
94
|
+
console.warn('[xyd-source-react-runtime] typia or typescript not found');
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
99
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
100
|
+
configFile.config,
|
|
101
|
+
ts.sys,
|
|
102
|
+
path.dirname(tsconfigPath),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Step 1: Scan source files for exported React components
|
|
106
|
+
const componentsByFile = new Map<string, ComponentInfo[]>();
|
|
107
|
+
|
|
108
|
+
for (const fileName of parsedConfig.fileNames) {
|
|
109
|
+
const content = ts.sys.readFile(fileName);
|
|
110
|
+
if (!content) continue;
|
|
111
|
+
|
|
112
|
+
const components = detectComponents(ts, content, fileName);
|
|
113
|
+
if (components.length > 0) {
|
|
114
|
+
componentsByFile.set(fileName, components);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (componentsByFile.size === 0) return result;
|
|
119
|
+
|
|
120
|
+
// Step 2: Create modified source files with typia calls injected
|
|
121
|
+
const modifiedSources = new Map<string, string>();
|
|
122
|
+
|
|
123
|
+
for (const [fileName, components] of componentsByFile) {
|
|
124
|
+
// Filter out components whose propsType is an inline type literal with
|
|
125
|
+
// React types (e.g. { children: React.ReactNode }) — typia can't resolve these
|
|
126
|
+
const supported = components.filter(c => !c.propsType.includes('React.'));
|
|
127
|
+
|
|
128
|
+
if (supported.length === 0) continue;
|
|
129
|
+
|
|
130
|
+
const original = ts.sys.readFile(fileName)!;
|
|
131
|
+
let modified = `import typia from "typia";\n${original}`;
|
|
132
|
+
|
|
133
|
+
for (const comp of supported) {
|
|
134
|
+
modified += `\n${comp.name}.__xydSchema = typia.json.schemas<[${comp.propsType}]>();`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
modifiedSources.set(fileName, modified);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Step 3: Create a TS program with modified sources for typia transform
|
|
141
|
+
const compilerHost = ts.createCompilerHost(parsedConfig.options);
|
|
142
|
+
const origGetSourceFile = compilerHost.getSourceFile;
|
|
143
|
+
const origFileExists = compilerHost.fileExists;
|
|
144
|
+
const origReadFile = compilerHost.readFile;
|
|
145
|
+
|
|
146
|
+
compilerHost.getSourceFile = (name, languageVersion, onError) => {
|
|
147
|
+
const resolved = path.resolve(name);
|
|
148
|
+
const modified = modifiedSources.get(resolved);
|
|
149
|
+
if (modified) {
|
|
150
|
+
return ts.createSourceFile(
|
|
151
|
+
name, modified, languageVersion, true,
|
|
152
|
+
name.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return origGetSourceFile.call(compilerHost, name, languageVersion, onError);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
compilerHost.fileExists = (name) => {
|
|
159
|
+
if (modifiedSources.has(path.resolve(name))) return true;
|
|
160
|
+
return origFileExists.call(compilerHost, name);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
compilerHost.readFile = (name) => {
|
|
164
|
+
const modified = modifiedSources.get(path.resolve(name));
|
|
165
|
+
if (modified) return modified;
|
|
166
|
+
return origReadFile.call(compilerHost, name);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const program = ts.createProgram({
|
|
170
|
+
rootNames: parsedConfig.fileNames,
|
|
171
|
+
options: {...parsedConfig.options, noEmit: false, declaration: false, sourceMap: false},
|
|
172
|
+
host: compilerHost,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const factory = (typiaTransformModule.default || typiaTransformModule)(program);
|
|
176
|
+
|
|
177
|
+
// Step 4: Emit typia-transformed output for each file with components
|
|
178
|
+
for (const fileName of componentsByFile.keys()) {
|
|
179
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
180
|
+
if (!sourceFile) continue;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
let output = '';
|
|
184
|
+
program.emit(
|
|
185
|
+
sourceFile,
|
|
186
|
+
(_name: string, text: string) => {
|
|
187
|
+
if (!_name.endsWith('.d.ts')) {
|
|
188
|
+
output = text;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
undefined,
|
|
192
|
+
false,
|
|
193
|
+
{before: [factory]},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (output) {
|
|
197
|
+
result.set(path.resolve(fileName), output);
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// typia transform can fail on files with unresolvable types (e.g. React.ReactNode)
|
|
201
|
+
// Skip these files — their components won't get __xydUniform
|
|
202
|
+
console.warn(`[xyd-source-react-runtime] typia transform failed for ${fileName}, skipping`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Detects exported React components and their props type names using TS AST.
|
|
211
|
+
*/
|
|
212
|
+
function detectComponents(
|
|
213
|
+
ts: typeof import('typescript'),
|
|
214
|
+
code: string,
|
|
215
|
+
fileName: string,
|
|
216
|
+
): ComponentInfo[] {
|
|
217
|
+
const sourceFile = ts.createSourceFile(
|
|
218
|
+
fileName, code, ts.ScriptTarget.Latest, true,
|
|
219
|
+
fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const components: ComponentInfo[] = [];
|
|
223
|
+
|
|
224
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
225
|
+
// export function ComponentName(props: PropsType) { ... }
|
|
226
|
+
if (ts.isFunctionDeclaration(node) && node.name && isExported(ts, node)) {
|
|
227
|
+
const name = node.name.text;
|
|
228
|
+
if (!isPascalCase(name)) return;
|
|
229
|
+
|
|
230
|
+
const propsType = extractPropsType(ts, node, sourceFile);
|
|
231
|
+
if (propsType) {
|
|
232
|
+
components.push({name, propsType, fileName});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// export const ComponentName = (props: PropsType) => { ... }
|
|
237
|
+
// export const SomeContext = createContext<ValueType>(...)
|
|
238
|
+
if (ts.isVariableStatement(node) && isExported(ts, node)) {
|
|
239
|
+
for (const decl of node.declarationList.declarations) {
|
|
240
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
241
|
+
const name = decl.name.text;
|
|
242
|
+
if (!isPascalCase(name)) continue;
|
|
243
|
+
|
|
244
|
+
if (!decl.initializer) continue;
|
|
245
|
+
|
|
246
|
+
// Arrow function / function expression → component
|
|
247
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
248
|
+
const propsType = extractPropsTypeFromParams(ts, decl.initializer.parameters, sourceFile);
|
|
249
|
+
if (propsType) {
|
|
250
|
+
components.push({name, propsType, fileName});
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// createContext<T>(...) → context
|
|
256
|
+
const contextType = extractCreateContextType(ts, decl.initializer, sourceFile);
|
|
257
|
+
if (contextType) {
|
|
258
|
+
components.push({name, propsType: contextType, fileName});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Also detect non-inline exports: declarations + export { Name }
|
|
265
|
+
// Collect all PascalCase declarations that weren't already found
|
|
266
|
+
const foundNames = new Set(components.map(c => c.name));
|
|
267
|
+
const reExportedNames = new Set<string>();
|
|
268
|
+
|
|
269
|
+
// Find all names in export { ... } blocks
|
|
270
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
271
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
272
|
+
for (const spec of node.exportClause.elements) {
|
|
273
|
+
reExportedNames.add(spec.name.text);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Find non-exported declarations that are re-exported
|
|
279
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
280
|
+
// function Name(...) { ... } (not exported inline)
|
|
281
|
+
if (ts.isFunctionDeclaration(node) && node.name && !isExported(ts, node)) {
|
|
282
|
+
const name = node.name.text;
|
|
283
|
+
if (!isPascalCase(name) || foundNames.has(name) || !reExportedNames.has(name)) return;
|
|
284
|
+
|
|
285
|
+
const propsType = extractPropsType(ts, node, sourceFile);
|
|
286
|
+
if (propsType) {
|
|
287
|
+
components.push({name, propsType, fileName});
|
|
288
|
+
foundNames.add(name);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// const Name = createContext<T>(...) or const Name = (...) => { ... }
|
|
293
|
+
if (ts.isVariableStatement(node) && !isExported(ts, node)) {
|
|
294
|
+
for (const decl of node.declarationList.declarations) {
|
|
295
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
296
|
+
const name = decl.name.text;
|
|
297
|
+
if (!isPascalCase(name) || foundNames.has(name) || !reExportedNames.has(name)) continue;
|
|
298
|
+
if (!decl.initializer) continue;
|
|
299
|
+
|
|
300
|
+
// Arrow/function expression
|
|
301
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
302
|
+
const propsType = extractPropsTypeFromParams(ts, decl.initializer.parameters, sourceFile);
|
|
303
|
+
if (propsType) {
|
|
304
|
+
components.push({name, propsType, fileName});
|
|
305
|
+
foundNames.add(name);
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// createContext<T>(...)
|
|
311
|
+
const contextType = extractCreateContextType(ts, decl.initializer, sourceFile);
|
|
312
|
+
if (contextType) {
|
|
313
|
+
components.push({name, propsType: contextType, fileName});
|
|
314
|
+
foundNames.add(name);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return components;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Extracts the type argument from createContext<T>() calls.
|
|
325
|
+
* Handles: createContext<T | null>() → strips null from union, returns T.
|
|
326
|
+
*/
|
|
327
|
+
function extractCreateContextType(
|
|
328
|
+
ts: typeof import('typescript'),
|
|
329
|
+
initializer: import('typescript').Expression,
|
|
330
|
+
sourceFile: import('typescript').SourceFile,
|
|
331
|
+
): string | null {
|
|
332
|
+
if (!ts.isCallExpression(initializer)) return null;
|
|
333
|
+
|
|
334
|
+
// Match createContext(...) or React.createContext(...)
|
|
335
|
+
const callee = initializer.expression;
|
|
336
|
+
const isCreateContext =
|
|
337
|
+
(ts.isIdentifier(callee) && callee.text === 'createContext') ||
|
|
338
|
+
(ts.isPropertyAccessExpression(callee) && callee.name.text === 'createContext');
|
|
339
|
+
|
|
340
|
+
if (!isCreateContext) return null;
|
|
341
|
+
|
|
342
|
+
// Get type arguments: createContext<T>(...)
|
|
343
|
+
const typeArgs = initializer.typeArguments;
|
|
344
|
+
if (!typeArgs || typeArgs.length === 0) return null;
|
|
345
|
+
|
|
346
|
+
const typeArg = typeArgs[0];
|
|
347
|
+
|
|
348
|
+
// If it's a union like T | null, strip null/undefined and return T
|
|
349
|
+
if (ts.isUnionTypeNode(typeArg)) {
|
|
350
|
+
const nonNullTypes = typeArg.types.filter(
|
|
351
|
+
(t) => !(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) &&
|
|
352
|
+
!(t.kind === ts.SyntaxKind.UndefinedKeyword),
|
|
353
|
+
);
|
|
354
|
+
if (nonNullTypes.length === 1) {
|
|
355
|
+
return nonNullTypes[0].getText(sourceFile);
|
|
356
|
+
}
|
|
357
|
+
// Multiple non-null types — keep as union
|
|
358
|
+
return nonNullTypes.map(t => t.getText(sourceFile)).join(' | ');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return typeArg.getText(sourceFile);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isExported(ts: typeof import('typescript'), node: any): boolean {
|
|
365
|
+
return node.modifiers?.some((m: any) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function isPascalCase(name: string): boolean {
|
|
369
|
+
return /^[A-Z]/.test(name);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function extractPropsType(
|
|
373
|
+
ts: typeof import('typescript'),
|
|
374
|
+
node: import('typescript').FunctionDeclaration,
|
|
375
|
+
sourceFile: import('typescript').SourceFile,
|
|
376
|
+
): string | null {
|
|
377
|
+
return extractPropsTypeFromParams(ts, node.parameters, sourceFile);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function extractPropsTypeFromParams(
|
|
381
|
+
ts: typeof import('typescript'),
|
|
382
|
+
parameters: import('typescript').NodeArray<import('typescript').ParameterDeclaration>,
|
|
383
|
+
sourceFile: import('typescript').SourceFile,
|
|
384
|
+
): string | null {
|
|
385
|
+
if (parameters.length === 0) return null;
|
|
386
|
+
|
|
387
|
+
const firstParam = parameters[0];
|
|
388
|
+
|
|
389
|
+
// props: PropsType
|
|
390
|
+
if (firstParam.type && ts.isTypeReferenceNode(firstParam.type)) {
|
|
391
|
+
return firstParam.type.getText(sourceFile);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// { prop1, prop2 }: PropsType
|
|
395
|
+
if (firstParam.type && ts.isTypeReferenceNode(firstParam.type) && ts.isObjectBindingPattern(firstParam.name)) {
|
|
396
|
+
return firstParam.type.getText(sourceFile);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Destructured with type annotation: ({ prop1, prop2 }: PropsType)
|
|
400
|
+
if (ts.isObjectBindingPattern(firstParam.name) && firstParam.type) {
|
|
401
|
+
return firstParam.type.getText(sourceFile);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Finds __xydSchema assignments and converts JSON Schema → xyd uniform format.
|
|
409
|
+
*/
|
|
410
|
+
function convertSchemaToUniform(code: string, propertyName: string = '__xydUniform'): {code: string; map: null} | null {
|
|
411
|
+
const schemaRegex = /(\w+)\.__xydSchema\s*=\s*(\{[\s\S]*?\n\});/g;
|
|
412
|
+
let modified = code;
|
|
413
|
+
let hasChanges = false;
|
|
414
|
+
|
|
415
|
+
let match: RegExpExecArray | null;
|
|
416
|
+
while ((match = schemaRegex.exec(code)) !== null) {
|
|
417
|
+
const componentName = match[1];
|
|
418
|
+
const schemaStr = match[2];
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const schema = new Function(`return ${schemaStr}`)();
|
|
422
|
+
const uniform = jsonSchemaToUniformReference(componentName, schema);
|
|
423
|
+
const jsonStr = JSON.stringify(uniform).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
424
|
+
|
|
425
|
+
modified = modified.replace(
|
|
426
|
+
match[0],
|
|
427
|
+
`${componentName}.${propertyName} = JSON.parse('${jsonStr}');`,
|
|
428
|
+
);
|
|
429
|
+
hasChanges = true;
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.warn(`[xyd-source-react-runtime] Failed to parse schema for ${componentName}:`, e);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!hasChanges) return null;
|
|
436
|
+
return {code: modified, map: null};
|
|
437
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Reference,
|
|
3
|
+
Definition,
|
|
4
|
+
} from '@xyd-js/uniform';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
schemaObjectToUniformDefinitionProperties,
|
|
8
|
+
} from '@xyd-js/openapi';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JSON Schema structure as produced by typia.json.schemas<[T]>().
|
|
12
|
+
*/
|
|
13
|
+
interface JsonSchemaOutput {
|
|
14
|
+
version?: string;
|
|
15
|
+
components?: {
|
|
16
|
+
schemas?: Record<string, any>;
|
|
17
|
+
};
|
|
18
|
+
schemas?: Array<{$ref?: string} | any>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a typia JSON Schema output into an xyd uniform Reference,
|
|
23
|
+
* reusing xyd-openapi's battle-tested schema → uniform converter.
|
|
24
|
+
*/
|
|
25
|
+
export function jsonSchemaToUniformReference(
|
|
26
|
+
componentName: string,
|
|
27
|
+
schema: JsonSchemaOutput,
|
|
28
|
+
): Reference {
|
|
29
|
+
const ref: Reference = {
|
|
30
|
+
title: componentName,
|
|
31
|
+
canonical: '',
|
|
32
|
+
description: '',
|
|
33
|
+
definitions: [],
|
|
34
|
+
examples: {groups: []},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Resolve the root schema (follows $ref to components.schemas)
|
|
38
|
+
const rootSchema = resolveRootSchema(schema);
|
|
39
|
+
if (!rootSchema) return ref;
|
|
40
|
+
|
|
41
|
+
// Inline all $ref pointers so xyd-openapi doesn't encounter unresolved refs
|
|
42
|
+
const schemas = schema.components?.schemas || {};
|
|
43
|
+
const inlined = inlineRefs(rootSchema, schemas);
|
|
44
|
+
|
|
45
|
+
// Use xyd-openapi's converter
|
|
46
|
+
const result = schemaObjectToUniformDefinitionProperties(inlined as any);
|
|
47
|
+
|
|
48
|
+
const propsDef: Definition = {
|
|
49
|
+
title: 'Props',
|
|
50
|
+
properties: [],
|
|
51
|
+
meta: [{name: 'type' as any, value: 'parameters'}],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(result)) {
|
|
55
|
+
propsDef.properties = result;
|
|
56
|
+
} else if (result) {
|
|
57
|
+
propsDef.rootProperty = result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (propsDef.properties.length > 0 || propsDef.rootProperty) {
|
|
61
|
+
ref.definitions.push(propsDef);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return ref;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveRootSchema(output: JsonSchemaOutput): any | null {
|
|
68
|
+
const rootRef = output.schemas?.[0];
|
|
69
|
+
if (!rootRef) return null;
|
|
70
|
+
|
|
71
|
+
if ('$ref' in rootRef && rootRef.$ref) {
|
|
72
|
+
const refPath = rootRef.$ref.replace('#/components/schemas/', '');
|
|
73
|
+
return output.components?.schemas?.[refPath] || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return rootRef;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Recursively inlines all $ref pointers so the schema is self-contained
|
|
81
|
+
* for xyd-openapi's converter (which warns on unresolved $ref).
|
|
82
|
+
*/
|
|
83
|
+
function inlineRefs(schema: any, schemas: Record<string, any>, visited = new Set<string>()): any {
|
|
84
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
85
|
+
|
|
86
|
+
if (schema.$ref) {
|
|
87
|
+
const refPath = schema.$ref.replace('#/components/schemas/', '');
|
|
88
|
+
if (visited.has(refPath)) {
|
|
89
|
+
return {type: 'object', description: `(circular: ${refPath})`};
|
|
90
|
+
}
|
|
91
|
+
const resolved = schemas[refPath];
|
|
92
|
+
if (!resolved) return schema;
|
|
93
|
+
visited.add(refPath);
|
|
94
|
+
const result = inlineRefs({...resolved}, schemas, visited);
|
|
95
|
+
visited.delete(refPath);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(schema)) {
|
|
100
|
+
return schema.map((item) => inlineRefs(item, schemas, visited));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result: any = {};
|
|
104
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
105
|
+
result[key] = inlineRefs(value, schemas, visited);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"jsx": "react-jsx"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"],
|
|
15
|
+
"exclude": ["node_modules", "dist", "__tests__", "__fixtures__"]
|
|
16
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {defineConfig, Options} from 'tsup';
|
|
2
|
+
|
|
3
|
+
const config: Options = {
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
esbuild: 'src/esbuild.ts',
|
|
7
|
+
},
|
|
8
|
+
format: ['esm'],
|
|
9
|
+
target: 'node16',
|
|
10
|
+
dts: {
|
|
11
|
+
entry: {
|
|
12
|
+
index: 'src/index.ts',
|
|
13
|
+
esbuild: 'src/esbuild.ts',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
splitting: false,
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
clean: true,
|
|
19
|
+
external: ['vite', 'esbuild', 'typia', 'typescript', '@xyd-js/openapi', '@xyd-js/uniform'],
|
|
20
|
+
tsconfig: 'tsconfig.tsup.json',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default defineConfig(config);
|