@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +121 -0
  4. package/__fixtures__/-1.vite-lib.custom-property/input/package.json +8 -0
  5. package/__fixtures__/-1.vite-lib.custom-property/input/src/UserCard.tsx +27 -0
  6. package/__fixtures__/-1.vite-lib.custom-property/input/src/index.ts +1 -0
  7. package/__fixtures__/-1.vite-lib.custom-property/input/tsconfig.json +12 -0
  8. package/__fixtures__/-1.vite-lib.custom-property/input/vite.config.ts +22 -0
  9. package/__fixtures__/-1.vite-lib.custom-property/output.js +10 -0
  10. package/__fixtures__/1.vite-lib.user-card/input/package.json +8 -0
  11. package/__fixtures__/1.vite-lib.user-card/input/src/UserCard.tsx +27 -0
  12. package/__fixtures__/1.vite-lib.user-card/input/src/index.ts +1 -0
  13. package/__fixtures__/1.vite-lib.user-card/input/tsconfig.json +12 -0
  14. package/__fixtures__/1.vite-lib.user-card/input/vite.config.ts +22 -0
  15. package/__fixtures__/1.vite-lib.user-card/output.js +10 -0
  16. package/__fixtures__/2.vite-lib.sample-app/input/package.json +8 -0
  17. package/__fixtures__/2.vite-lib.sample-app/input/src/components/UserProfile.tsx +42 -0
  18. package/__fixtures__/2.vite-lib.sample-app/input/src/contexts/UserContext.tsx +27 -0
  19. package/__fixtures__/2.vite-lib.sample-app/input/src/index.ts +3 -0
  20. package/__fixtures__/2.vite-lib.sample-app/input/src/types/user.ts +18 -0
  21. package/__fixtures__/2.vite-lib.sample-app/input/tsconfig.json +12 -0
  22. package/__fixtures__/2.vite-lib.sample-app/input/vite.config.ts +22 -0
  23. package/__fixtures__/2.vite-lib.sample-app/output.js +27 -0
  24. package/__fixtures__/3.vite-lib.sample-real-app/input/package.json +8 -0
  25. package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/AddTodoForm.tsx +51 -0
  26. package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/FilterBar.tsx +56 -0
  27. package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/StatsPanel.tsx +44 -0
  28. package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/TodoItem.tsx +54 -0
  29. package/__fixtures__/3.vite-lib.sample-real-app/input/src/index.ts +6 -0
  30. package/__fixtures__/3.vite-lib.sample-real-app/input/src/types/todo.ts +23 -0
  31. package/__fixtures__/3.vite-lib.sample-real-app/input/tsconfig.json +12 -0
  32. package/__fixtures__/3.vite-lib.sample-real-app/input/vite.config.ts +22 -0
  33. package/__fixtures__/3.vite-lib.sample-real-app/output.js +63 -0
  34. package/__fixtures__/4.vite-app.user-card/input/package.json +8 -0
  35. package/__fixtures__/4.vite-app.user-card/input/src/UserCard.tsx +27 -0
  36. package/__fixtures__/4.vite-app.user-card/input/src/index.ts +1 -0
  37. package/__fixtures__/4.vite-app.user-card/input/tsconfig.json +12 -0
  38. package/__fixtures__/4.vite-app.user-card/input/vite.config.ts +23 -0
  39. package/__fixtures__/4.vite-app.user-card/output.js +10 -0
  40. package/__fixtures__/5.rollup.user-card/input/package.json +8 -0
  41. package/__fixtures__/5.rollup.user-card/input/rollup.config.mjs +20 -0
  42. package/__fixtures__/5.rollup.user-card/input/src/UserCard.tsx +27 -0
  43. package/__fixtures__/5.rollup.user-card/input/src/index.ts +1 -0
  44. package/__fixtures__/5.rollup.user-card/input/tsconfig.json +12 -0
  45. package/__fixtures__/5.rollup.user-card/output.js +11 -0
  46. package/__fixtures__/6.esbuild.user-card/input/esbuild.config.mjs +19 -0
  47. package/__fixtures__/6.esbuild.user-card/input/package.json +8 -0
  48. package/__fixtures__/6.esbuild.user-card/input/src/UserCard.tsx +27 -0
  49. package/__fixtures__/6.esbuild.user-card/input/src/index.ts +1 -0
  50. package/__fixtures__/6.esbuild.user-card/input/tsconfig.json +12 -0
  51. package/__fixtures__/6.esbuild.user-card/output.js +11 -0
  52. package/__fixtures__/7.react-router.app/input/app/components/ProductCard.tsx +26 -0
  53. package/__fixtures__/7.react-router.app/input/app/entry.server.tsx +36 -0
  54. package/__fixtures__/7.react-router.app/input/app/root.tsx +23 -0
  55. package/__fixtures__/7.react-router.app/input/app/routes/cart.tsx +16 -0
  56. package/__fixtures__/7.react-router.app/input/app/routes/home.tsx +29 -0
  57. package/__fixtures__/7.react-router.app/input/app/routes/product.tsx +6 -0
  58. package/__fixtures__/7.react-router.app/input/app/routes.ts +7 -0
  59. package/__fixtures__/7.react-router.app/input/app/types/product.ts +12 -0
  60. package/__fixtures__/7.react-router.app/input/package.json +8 -0
  61. package/__fixtures__/7.react-router.app/input/react-router.config.ts +2 -0
  62. package/__fixtures__/7.react-router.app/input/tsconfig.json +13 -0
  63. package/__fixtures__/7.react-router.app/input/vite.config.ts +14 -0
  64. package/__fixtures__/7.react-router.app/output.js +44 -0
  65. package/__fixtures__/8.tanstack-router.app/input/index.html +5 -0
  66. package/__fixtures__/8.tanstack-router.app/input/package.json +8 -0
  67. package/__fixtures__/8.tanstack-router.app/input/src/components/EmployeeTable.tsx +45 -0
  68. package/__fixtures__/8.tanstack-router.app/input/src/main.tsx +19 -0
  69. package/__fixtures__/8.tanstack-router.app/input/src/routeTree.gen.ts +77 -0
  70. package/__fixtures__/8.tanstack-router.app/input/src/routes/__root.tsx +13 -0
  71. package/__fixtures__/8.tanstack-router.app/input/src/routes/employees.tsx +23 -0
  72. package/__fixtures__/8.tanstack-router.app/input/src/routes/index.tsx +5 -0
  73. package/__fixtures__/8.tanstack-router.app/input/src/types/employee.ts +13 -0
  74. package/__fixtures__/8.tanstack-router.app/input/tsconfig.json +12 -0
  75. package/__fixtures__/8.tanstack-router.app/input/vite.config.ts +21 -0
  76. package/__fixtures__/8.tanstack-router.app/output.js +63 -0
  77. package/__tests__/source-react-runtime.test.ts +61 -0
  78. package/__tests__/utils.ts +100 -0
  79. package/dist/esbuild.d.ts +20 -0
  80. package/dist/esbuild.js +378 -0
  81. package/dist/esbuild.js.map +1 -0
  82. package/dist/index.d.ts +53 -0
  83. package/dist/index.js +348 -0
  84. package/dist/index.js.map +1 -0
  85. package/package.json +47 -0
  86. package/src/esbuild.ts +45 -0
  87. package/src/index.ts +437 -0
  88. package/src/json-schema-to-uniform.ts +108 -0
  89. package/tsconfig.json +16 -0
  90. package/tsconfig.tsup.json +6 -0
  91. package/tsup.config.ts +23 -0
  92. 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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": ""
5
+ }
6
+ }
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);
@@ -0,0 +1,7 @@
1
+ import {defineConfig} from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ testTimeout: 60_000,
6
+ },
7
+ });