@shipfox/client-shell 0.2.0 → 1.0.0

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 (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/tsconfig.test.tsbuildinfo +1 -1
  4. package/dist/vite/evaluate-features.d.ts +0 -1
  5. package/dist/vite/evaluate-features.d.ts.map +1 -1
  6. package/dist/vite/evaluate-features.js +60 -47
  7. package/dist/vite/evaluate-features.js.map +1 -1
  8. package/dist/vite/generate.js +2 -2
  9. package/dist/vite/generate.js.map +1 -1
  10. package/dist/vite/plugin.d.ts.map +1 -1
  11. package/dist/vite/plugin.js +3 -12
  12. package/dist/vite/plugin.js.map +1 -1
  13. package/package.json +28 -49
  14. package/src/vite/evaluate-features.test.ts +58 -3
  15. package/src/vite/evaluate-features.ts +71 -50
  16. package/src/vite/generate.ts +2 -2
  17. package/src/vite/plugin.test.ts +59 -9
  18. package/src/vite/plugin.ts +3 -16
  19. package/test/external/FINDINGS.md +33 -45
  20. package/test/external/README.md +21 -23
  21. package/test/external/fixture/package.json +1 -2
  22. package/test/external/fixture/src/app.fixture.tsx +78 -46
  23. package/test/external/fixture/src/features/external-settings.tsx +19 -0
  24. package/test/external/fixture/src/features/login-override.tsx +9 -0
  25. package/test/external/fixture/src/features.collision.ts +5 -5
  26. package/test/external/fixture/src/features.ts +49 -3
  27. package/test/external/fixture/src/link-typecheck.tsx +5 -5
  28. package/test/external/fixture/src/main.tsx +83 -1
  29. package/test/external/fixture/src/provider.ts +45 -0
  30. package/test/external/fixture/tsconfig.json +1 -1
  31. package/test/external/fixture/vite.config.ts +13 -2
  32. package/test/external/fixture/vitest.config.ts +3 -1
  33. package/test/external/verify.mjs +233 -47
  34. package/test/typecheck/shipfox-app.gen.ts +6 -6
  35. package/test/typecheck/types.tsx +6 -1
  36. package/tsconfig.build.tsbuildinfo +1 -1
  37. package/test/external/fixture/src/features/override-impl.tsx +0 -12
  38. package/test/external/fixture/src/features/override.tsx +0 -20
  39. package/test/external/toy-feature/CHANGELOG.md +0 -15
  40. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-check +0 -43
  41. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-format +0 -43
  42. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-lint +0 -43
  43. package/test/external/toy-feature/node_modules/.bin/shipfox-swc +0 -43
  44. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-check +0 -43
  45. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-emit +0 -43
  46. package/test/external/toy-feature/package.json +0 -57
  47. package/test/external/toy-feature/src/config.ts +0 -5
  48. package/test/external/toy-feature/src/index.ts +0 -64
  49. package/test/external/toy-feature/src/provider.tsx +0 -33
  50. package/test/external/toy-feature/src/routes/insights.tsx +0 -7
  51. package/test/external/toy-feature/src/routes/settings.tsx +0 -7
  52. package/test/external/toy-feature/tsconfig.build.json +0 -5
  53. package/test/external/toy-feature/tsconfig.json +0 -1
  54. package/test/not-route-impl.ts +0 -1
@@ -3,6 +3,5 @@ export interface EvaluatedFeatures {
3
3
  features: readonly ClientFeature[];
4
4
  loadedFiles: readonly string[];
5
5
  }
6
- export declare function invalidateFeatures(loadedFiles: Iterable<string>): void;
7
6
  export declare function evaluateFeatures(featuresModule: string): Promise<EvaluatedFeatures>;
8
7
  //# sourceMappingURL=evaluate-features.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-features.d.ts","sourceRoot":"","sources":["../../src/vite/evaluate-features.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAOhD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAEtE;AA4CD,wBAAsB,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA6BzF"}
1
+ {"version":3,"file":"evaluate-features.d.ts","sourceRoot":"","sources":["../../src/vite/evaluate-features.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAKhD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAkDD,wBAAsB,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAkDzF"}
@@ -1,71 +1,84 @@
1
- import { readFile, realpath } from 'node:fs/promises';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpath } from 'node:fs/promises';
2
3
  import { createRequire } from 'node:module';
3
4
  import { resolve } from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
5
- import { createJiti } from 'jiti';
5
+ import { register } from 'tsx/cjs/api';
6
6
  const require = createRequire(import.meta.url);
7
- const importPattern = /(?:\b(?:import|export)\s+(?:[^'"]*?\s+from\s+)?|\bimport\s*\(\s*)['"]([^'"]+)['"]/g;
8
7
  const nodeModulesPathPattern = /[\\/]node_modules[\\/]/;
9
- export function invalidateFeatures(loadedFiles) {
10
- for (const file of loadedFiles)delete require.cache[file];
11
- }
12
- function loadedModuleGraph(entry) {
13
- const files = new Set();
14
- function visit(file) {
15
- if (files.has(file)) return;
16
- files.add(file);
17
- for (const child of require.cache[file]?.children ?? [])visit(child.filename);
8
+ function loadedModuleGraph(namespaceQuery) {
9
+ const cacheKeys = new Set();
10
+ const loadedPaths = new Set();
11
+ const keysByModule = new Map();
12
+ for (const [cacheKey, cachedModule] of Object.entries(require.cache)){
13
+ if (!cachedModule) continue;
14
+ const keys = keysByModule.get(cachedModule) ?? [];
15
+ keys.push(cacheKey);
16
+ keysByModule.set(cachedModule, keys);
18
17
  }
19
- visit(entry);
20
- return [
21
- ...files
22
- ];
18
+ const visited = new Set();
19
+ function visit(cachedModule) {
20
+ if (visited.has(cachedModule)) return;
21
+ visited.add(cachedModule);
22
+ for (const cacheKey of keysByModule.get(cachedModule) ?? []){
23
+ const path = cacheKey.endsWith(namespaceQuery) ? cacheKey.slice(0, -namespaceQuery.length) : cacheKey;
24
+ const isLocal = !nodeModulesPathPattern.test(path);
25
+ if (isLocal) loadedPaths.add(path);
26
+ if (isLocal || cacheKey.endsWith(namespaceQuery)) cacheKeys.add(cacheKey);
27
+ }
28
+ for (const child of cachedModule.children)visit(child);
29
+ }
30
+ for (const [cacheKey, cachedModule] of Object.entries(require.cache)){
31
+ if (cachedModule && cacheKey.endsWith(namespaceQuery)) visit(cachedModule);
32
+ }
33
+ return {
34
+ cacheKeys,
35
+ loadedPaths
36
+ };
23
37
  }
24
- async function staticallyImportedModules(entry, jiti) {
38
+ async function localFiles(paths) {
25
39
  const files = new Set();
26
- async function visit(file) {
27
- const path = file.startsWith('file:') ? fileURLToPath(file) : file;
28
- if (path.startsWith('node:') || nodeModulesPathPattern.test(path) || files.has(path)) return;
29
- const resolvedPath = await realpath(path).catch(()=>path);
30
- files.add(resolvedPath);
31
- const source = await readFile(resolvedPath, 'utf8').catch(()=>undefined);
32
- if (!source) return;
33
- for (const match of source.matchAll(importPattern)){
34
- const specifier = match[1];
35
- if (!specifier) continue;
36
- const dependency = jiti.esmResolve(specifier, {
37
- parentURL: pathToFileURL(resolvedPath),
38
- try: true
39
- });
40
- if (dependency) await visit(dependency);
41
- }
40
+ for (const path of paths){
41
+ if (nodeModulesPathPattern.test(path)) continue;
42
+ files.add(await realpath(path).catch(()=>path));
42
43
  }
43
- await visit(entry);
44
44
  return [
45
45
  ...files
46
46
  ];
47
47
  }
48
48
  export async function evaluateFeatures(featuresModule) {
49
49
  const resolvedFeaturesModule = await realpath(resolve(featuresModule));
50
- const jiti = createJiti(resolvedFeaturesModule, {
51
- interopDefault: false,
52
- tsconfigPaths: true
50
+ const namespace = randomUUID();
51
+ const namespaceQuery = `?namespace=${namespace}`;
52
+ const loader = register({
53
+ namespace
53
54
  });
55
+ const loadedPaths = new Set([
56
+ resolvedFeaturesModule
57
+ ]);
58
+ const cacheKeys = new Set();
59
+ const initialCacheKeys = new Set(Object.keys(require.cache));
54
60
  let module;
55
- let loadedFiles;
56
61
  try {
57
- module = jiti(resolvedFeaturesModule);
58
- loadedFiles = [
59
- ...new Set([
60
- ...loadedModuleGraph(resolvedFeaturesModule),
61
- ...await staticallyImportedModules(resolvedFeaturesModule, jiti)
62
- ])
63
- ];
62
+ module = loader.require(resolvedFeaturesModule, import.meta.url);
64
63
  } catch (error) {
65
64
  const message = error instanceof Error ? error.message : String(error);
66
65
  throw new Error(`Failed to evaluate features module "${resolvedFeaturesModule}". Features modules must be Node-safe: ${message}`, {
67
66
  cause: error
68
67
  });
68
+ } finally{
69
+ const graph = loadedModuleGraph(namespaceQuery);
70
+ for (const path of graph.loadedPaths)loadedPaths.add(path);
71
+ for (const cacheKey of graph.cacheKeys)cacheKeys.add(cacheKey);
72
+ for (const cacheKey of Object.keys(require.cache)){
73
+ if (initialCacheKeys.has(cacheKey)) continue;
74
+ const path = cacheKey.endsWith(namespaceQuery) ? cacheKey.slice(0, -namespaceQuery.length) : cacheKey;
75
+ const isLocal = !nodeModulesPathPattern.test(path);
76
+ if (isLocal) loadedPaths.add(path);
77
+ if (isLocal || cacheKey.endsWith(namespaceQuery)) cacheKeys.add(cacheKey);
78
+ }
79
+ loader.unregister();
80
+ for (const cacheKey of cacheKeys)delete require.cache[cacheKey];
81
+ for (const cacheKey of Object.keys(require.cache))if (cacheKey.endsWith(namespaceQuery)) delete require.cache[cacheKey];
69
82
  }
70
83
  const features = module.features ?? module.default;
71
84
  if (!Array.isArray(features)) {
@@ -73,7 +86,7 @@ export async function evaluateFeatures(featuresModule) {
73
86
  }
74
87
  return {
75
88
  features: features,
76
- loadedFiles
89
+ loadedFiles: await localFiles(loadedPaths)
77
90
  };
78
91
  }
79
92
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/evaluate-features.ts"],"sourcesContent":["import {readFile, realpath} from 'node:fs/promises';\nimport {createRequire} from 'node:module';\nimport {resolve} from 'node:path';\nimport {fileURLToPath, pathToFileURL} from 'node:url';\nimport {createJiti} from 'jiti';\nimport type {ClientFeature} from '#contract.js';\n\nconst require = createRequire(import.meta.url);\nconst importPattern =\n /(?:\\b(?:import|export)\\s+(?:[^'\"]*?\\s+from\\s+)?|\\bimport\\s*\\(\\s*)['\"]([^'\"]+)['\"]/g;\nconst nodeModulesPathPattern = /[\\\\/]node_modules[\\\\/]/;\n\nexport interface EvaluatedFeatures {\n features: readonly ClientFeature[];\n loadedFiles: readonly string[];\n}\n\nexport function invalidateFeatures(loadedFiles: Iterable<string>): void {\n for (const file of loadedFiles) delete require.cache[file];\n}\n\nfunction loadedModuleGraph(entry: string): string[] {\n const files = new Set<string>();\n\n function visit(file: string): void {\n if (files.has(file)) return;\n files.add(file);\n for (const child of require.cache[file]?.children ?? []) visit(child.filename);\n }\n\n visit(entry);\n return [...files];\n}\n\nasync function staticallyImportedModules(\n entry: string,\n jiti: ReturnType<typeof createJiti>,\n): Promise<string[]> {\n const files = new Set<string>();\n\n async function visit(file: string): Promise<void> {\n const path = file.startsWith('file:') ? fileURLToPath(file) : file;\n if (path.startsWith('node:') || nodeModulesPathPattern.test(path) || files.has(path)) return;\n const resolvedPath = await realpath(path).catch(() => path);\n files.add(resolvedPath);\n const source = await readFile(resolvedPath, 'utf8').catch(() => undefined);\n if (!source) return;\n\n for (const match of source.matchAll(importPattern)) {\n const specifier = match[1];\n if (!specifier) continue;\n const dependency = jiti.esmResolve(specifier, {\n parentURL: pathToFileURL(resolvedPath),\n try: true,\n });\n if (dependency) await visit(dependency);\n }\n }\n\n await visit(entry);\n return [...files];\n}\n\nexport async function evaluateFeatures(featuresModule: string): Promise<EvaluatedFeatures> {\n const resolvedFeaturesModule = await realpath(resolve(featuresModule));\n const jiti = createJiti(resolvedFeaturesModule, {interopDefault: false, tsconfigPaths: true});\n let module: {default?: unknown; features?: unknown};\n let loadedFiles: string[];\n try {\n module = jiti(resolvedFeaturesModule) as {default?: unknown; features?: unknown};\n loadedFiles = [\n ...new Set([\n ...loadedModuleGraph(resolvedFeaturesModule),\n ...(await staticallyImportedModules(resolvedFeaturesModule, jiti)),\n ]),\n ];\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Failed to evaluate features module \"${resolvedFeaturesModule}\". Features modules must be Node-safe: ${message}`,\n {cause: error},\n );\n }\n\n const features = module.features ?? module.default;\n if (!Array.isArray(features)) {\n throw new TypeError(\n `Features module \"${resolvedFeaturesModule}\" must export a features array.`,\n );\n }\n\n return {features: features as readonly ClientFeature[], loadedFiles};\n}\n"],"names":["readFile","realpath","createRequire","resolve","fileURLToPath","pathToFileURL","createJiti","require","url","importPattern","nodeModulesPathPattern","invalidateFeatures","loadedFiles","file","cache","loadedModuleGraph","entry","files","Set","visit","has","add","child","children","filename","staticallyImportedModules","jiti","path","startsWith","test","resolvedPath","catch","source","undefined","match","matchAll","specifier","dependency","esmResolve","parentURL","try","evaluateFeatures","featuresModule","resolvedFeaturesModule","interopDefault","tsconfigPaths","module","error","message","Error","String","cause","features","default","Array","isArray","TypeError"],"mappings":"AAAA,SAAQA,QAAQ,EAAEC,QAAQ,QAAO,mBAAmB;AACpD,SAAQC,aAAa,QAAO,cAAc;AAC1C,SAAQC,OAAO,QAAO,YAAY;AAClC,SAAQC,aAAa,EAAEC,aAAa,QAAO,WAAW;AACtD,SAAQC,UAAU,QAAO,OAAO;AAGhC,MAAMC,UAAUL,cAAc,YAAYM,GAAG;AAC7C,MAAMC,gBACJ;AACF,MAAMC,yBAAyB;AAO/B,OAAO,SAASC,mBAAmBC,WAA6B;IAC9D,KAAK,MAAMC,QAAQD,YAAa,OAAOL,QAAQO,KAAK,CAACD,KAAK;AAC5D;AAEA,SAASE,kBAAkBC,KAAa;IACtC,MAAMC,QAAQ,IAAIC;IAElB,SAASC,MAAMN,IAAY;QACzB,IAAII,MAAMG,GAAG,CAACP,OAAO;QACrBI,MAAMI,GAAG,CAACR;QACV,KAAK,MAAMS,SAASf,QAAQO,KAAK,CAACD,KAAK,EAAEU,YAAY,EAAE,CAAEJ,MAAMG,MAAME,QAAQ;IAC/E;IAEAL,MAAMH;IACN,OAAO;WAAIC;KAAM;AACnB;AAEA,eAAeQ,0BACbT,KAAa,EACbU,IAAmC;IAEnC,MAAMT,QAAQ,IAAIC;IAElB,eAAeC,MAAMN,IAAY;QAC/B,MAAMc,OAAOd,KAAKe,UAAU,CAAC,WAAWxB,cAAcS,QAAQA;QAC9D,IAAIc,KAAKC,UAAU,CAAC,YAAYlB,uBAAuBmB,IAAI,CAACF,SAASV,MAAMG,GAAG,CAACO,OAAO;QACtF,MAAMG,eAAe,MAAM7B,SAAS0B,MAAMI,KAAK,CAAC,IAAMJ;QACtDV,MAAMI,GAAG,CAACS;QACV,MAAME,SAAS,MAAMhC,SAAS8B,cAAc,QAAQC,KAAK,CAAC,IAAME;QAChE,IAAI,CAACD,QAAQ;QAEb,KAAK,MAAME,SAASF,OAAOG,QAAQ,CAAC1B,eAAgB;YAClD,MAAM2B,YAAYF,KAAK,CAAC,EAAE;YAC1B,IAAI,CAACE,WAAW;YAChB,MAAMC,aAAaX,KAAKY,UAAU,CAACF,WAAW;gBAC5CG,WAAWlC,cAAcyB;gBACzBU,KAAK;YACP;YACA,IAAIH,YAAY,MAAMlB,MAAMkB;QAC9B;IACF;IAEA,MAAMlB,MAAMH;IACZ,OAAO;WAAIC;KAAM;AACnB;AAEA,OAAO,eAAewB,iBAAiBC,cAAsB;IAC3D,MAAMC,yBAAyB,MAAM1C,SAASE,QAAQuC;IACtD,MAAMhB,OAAOpB,WAAWqC,wBAAwB;QAACC,gBAAgB;QAAOC,eAAe;IAAI;IAC3F,IAAIC;IACJ,IAAIlC;IACJ,IAAI;QACFkC,SAASpB,KAAKiB;QACd/B,cAAc;eACT,IAAIM,IAAI;mBACNH,kBAAkB4B;mBACjB,MAAMlB,0BAA0BkB,wBAAwBjB;aAC7D;SACF;IACH,EAAE,OAAOqB,OAAO;QACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;QAChE,MAAM,IAAIE,MACR,CAAC,oCAAoC,EAAEN,uBAAuB,uCAAuC,EAAEK,SAAS,EAChH;YAACG,OAAOJ;QAAK;IAEjB;IAEA,MAAMK,WAAWN,OAAOM,QAAQ,IAAIN,OAAOO,OAAO;IAClD,IAAI,CAACC,MAAMC,OAAO,CAACH,WAAW;QAC5B,MAAM,IAAII,UACR,CAAC,iBAAiB,EAAEb,uBAAuB,+BAA+B,CAAC;IAE/E;IAEA,OAAO;QAACS,UAAUA;QAAsCxC;IAAW;AACrE"}
1
+ {"version":3,"sources":["../../src/vite/evaluate-features.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {realpath} from 'node:fs/promises';\nimport {createRequire} from 'node:module';\nimport {resolve} from 'node:path';\nimport {register} from 'tsx/cjs/api';\nimport type {ClientFeature} from '#contract.js';\n\nconst require = createRequire(import.meta.url);\nconst nodeModulesPathPattern = /[\\\\/]node_modules[\\\\/]/;\n\nexport interface EvaluatedFeatures {\n features: readonly ClientFeature[];\n loadedFiles: readonly string[];\n}\n\nfunction loadedModuleGraph(namespaceQuery: string): {\n cacheKeys: Set<string>;\n loadedPaths: Set<string>;\n} {\n const cacheKeys = new Set<string>();\n const loadedPaths = new Set<string>();\n const keysByModule = new Map<NodeJS.Module, string[]>();\n\n for (const [cacheKey, cachedModule] of Object.entries(require.cache)) {\n if (!cachedModule) continue;\n const keys = keysByModule.get(cachedModule) ?? [];\n keys.push(cacheKey);\n keysByModule.set(cachedModule, keys);\n }\n\n const visited = new Set<NodeJS.Module>();\n function visit(cachedModule: NodeJS.Module): void {\n if (visited.has(cachedModule)) return;\n visited.add(cachedModule);\n\n for (const cacheKey of keysByModule.get(cachedModule) ?? []) {\n const path = cacheKey.endsWith(namespaceQuery)\n ? cacheKey.slice(0, -namespaceQuery.length)\n : cacheKey;\n const isLocal = !nodeModulesPathPattern.test(path);\n if (isLocal) loadedPaths.add(path);\n if (isLocal || cacheKey.endsWith(namespaceQuery)) cacheKeys.add(cacheKey);\n }\n\n for (const child of cachedModule.children) visit(child);\n }\n\n for (const [cacheKey, cachedModule] of Object.entries(require.cache)) {\n if (cachedModule && cacheKey.endsWith(namespaceQuery)) visit(cachedModule);\n }\n\n return {cacheKeys, loadedPaths};\n}\n\nasync function localFiles(paths: Iterable<string>): Promise<string[]> {\n const files = new Set<string>();\n for (const path of paths) {\n if (nodeModulesPathPattern.test(path)) continue;\n files.add(await realpath(path).catch(() => path));\n }\n return [...files];\n}\n\nexport async function evaluateFeatures(featuresModule: string): Promise<EvaluatedFeatures> {\n const resolvedFeaturesModule = await realpath(resolve(featuresModule));\n const namespace = randomUUID();\n const namespaceQuery = `?namespace=${namespace}`;\n const loader = register({namespace});\n const loadedPaths = new Set<string>([resolvedFeaturesModule]);\n const cacheKeys = new Set<string>();\n const initialCacheKeys = new Set(Object.keys(require.cache));\n let module: {default?: unknown; features?: unknown};\n try {\n module = loader.require(resolvedFeaturesModule, import.meta.url) as {\n default?: unknown;\n features?: unknown;\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Failed to evaluate features module \"${resolvedFeaturesModule}\". Features modules must be Node-safe: ${message}`,\n {cause: error},\n );\n } finally {\n const graph = loadedModuleGraph(namespaceQuery);\n for (const path of graph.loadedPaths) loadedPaths.add(path);\n for (const cacheKey of graph.cacheKeys) cacheKeys.add(cacheKey);\n for (const cacheKey of Object.keys(require.cache)) {\n if (initialCacheKeys.has(cacheKey)) continue;\n const path = cacheKey.endsWith(namespaceQuery)\n ? cacheKey.slice(0, -namespaceQuery.length)\n : cacheKey;\n const isLocal = !nodeModulesPathPattern.test(path);\n if (isLocal) loadedPaths.add(path);\n if (isLocal || cacheKey.endsWith(namespaceQuery)) cacheKeys.add(cacheKey);\n }\n loader.unregister();\n for (const cacheKey of cacheKeys) delete require.cache[cacheKey];\n for (const cacheKey of Object.keys(require.cache))\n if (cacheKey.endsWith(namespaceQuery)) delete require.cache[cacheKey];\n }\n\n const features = module.features ?? module.default;\n if (!Array.isArray(features)) {\n throw new TypeError(\n `Features module \"${resolvedFeaturesModule}\" must export a features array.`,\n );\n }\n\n return {\n features: features as readonly ClientFeature[],\n loadedFiles: await localFiles(loadedPaths),\n };\n}\n"],"names":["randomUUID","realpath","createRequire","resolve","register","require","url","nodeModulesPathPattern","loadedModuleGraph","namespaceQuery","cacheKeys","Set","loadedPaths","keysByModule","Map","cacheKey","cachedModule","Object","entries","cache","keys","get","push","set","visited","visit","has","add","path","endsWith","slice","length","isLocal","test","child","children","localFiles","paths","files","catch","evaluateFeatures","featuresModule","resolvedFeaturesModule","namespace","loader","initialCacheKeys","module","error","message","Error","String","cause","graph","unregister","features","default","Array","isArray","TypeError","loadedFiles"],"mappings":"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"}
@@ -16,11 +16,11 @@ export function generateAppModule({ routes, navigation, settingsSections }) {
16
16
  ...route,
17
17
  routePath: routePathForAnchor(route.parent, route.path)
18
18
  }));
19
- const imports = routes.map((route, index)=>`import route${index}Impl from ${literal(route.impl)};`).join('\n');
19
+ const imports = routes.map((route, index)=>`import * as route${index}Module from ${literal(route.impl)};`).join('\n');
20
20
  const routeDeclarations = generatedRoutes.map((route, index)=>`const route${index} = createRoute({
21
21
  getParentRoute: () => ${route.parent === 'root' ? 'skeleton.rootRoute' : `skeleton.${route.parent}`},
22
22
  path: ${literal(route.routePath)},
23
- ...routeOptions(route${index}Impl, ${literal(route.impl)}, ${literal(route.path)}),
23
+ ...routeOptions(route${index}Module.default, ${literal(route.impl)}, ${literal(route.path)}),
24
24
  });`).join('\n\n');
25
25
  const rootRoutes = routeNames(generatedRoutes, 'root');
26
26
  const workspaceLayoutRoutes = routeNames(generatedRoutes, 'workspaceLayout');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/generate.ts"],"sourcesContent":["import type {ComposedRoute} from '#compose/compose-routes.js';\nimport type {NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {routePathForAnchor} from '#runtime/anchor-paths.js';\n\nexport interface GenerateAppModuleOptions {\n routes: readonly ComposedRoute[];\n navigation: readonly NavTabEntry[];\n settingsSections: readonly SettingsSectionEntry[];\n}\n\nfunction literal(value: unknown): string {\n return JSON.stringify(value, undefined, 2);\n}\n\nfunction indentedLiteral(value: unknown, indentation: number): string {\n return literal(value).replaceAll('\\n', `\\n${' '.repeat(indentation)}`);\n}\n\nfunction routeNames(routes: readonly ComposedRoute[], parent: ComposedRoute['parent']): string {\n return routes\n .map((route, index) => ({route, index}))\n .filter(({route}) => route.parent === parent)\n .map(({index}) => `route${index}`)\n .join(', ');\n}\n\nexport function generateAppModule({\n routes,\n navigation,\n settingsSections,\n}: GenerateAppModuleOptions): string {\n const generatedRoutes = routes.map((route) => ({\n ...route,\n routePath: routePathForAnchor(route.parent, route.path),\n }));\n const imports = routes\n .map((route, index) => `import route${index}Impl from ${literal(route.impl)};`)\n .join('\\n');\n const routeDeclarations = generatedRoutes\n .map(\n (route, index) => `const route${index} = createRoute({\n getParentRoute: () => ${route.parent === 'root' ? 'skeleton.rootRoute' : `skeleton.${route.parent}`},\n path: ${literal(route.routePath)},\n ...routeOptions(route${index}Impl, ${literal(route.impl)}, ${literal(route.path)}),\n});`,\n )\n .join('\\n\\n');\n const rootRoutes = routeNames(generatedRoutes, 'root');\n const workspaceLayoutRoutes = routeNames(generatedRoutes, 'workspaceLayout');\n const projectLayoutRoutes = routeNames(generatedRoutes, 'projectLayout');\n const workspaceSettingsRoutes = routeNames(generatedRoutes, 'workspaceSettings');\n const rootChildren = [rootRoutes, 'workspaceLayout'].filter(Boolean).join(',\\n ');\n\n return `// GENERATED by @shipfox/client-shell/vite. Do not edit.\n// biome-ignore-all format: generated code has stable, reviewable output.\n// biome-ignore-all assist/source/organizeImports: generated imports follow route order.\nimport {createRoute, createRouter} from '@tanstack/react-router';\nimport {buildAnchorSkeleton, isRouteImpl, type RouteImpl, type RouterContext} from '@shipfox/client-shell/runtime';\n${imports}\n\nfunction routeOptions<T extends RouteImpl>(routeImpl: T, impl: string, path: string): T['options'] {\n if (!isRouteImpl(routeImpl)) {\n throw new TypeError(\\`Route implementation \"\\${impl}\" for \"\\${path}\" must export default defineRoute(...).\\`);\n }\n return routeImpl.options;\n}\n\nconst skeleton = buildAnchorSkeleton({\n navigation: ${indentedLiteral(navigation, 2)},\n settingsSections: ${indentedLiteral(settingsSections, 2)},\n});\n\n${routeDeclarations}\n\nconst projectLayout = skeleton.projectLayout.addChildren([${projectLayoutRoutes}]);\nconst workspaceSettings = skeleton.workspaceSettings.addChildren([${workspaceSettingsRoutes}]);\nconst workspaceLayout = skeleton.workspaceLayout.addChildren([\n ${workspaceLayoutRoutes}${workspaceLayoutRoutes ? ',' : ''}\n projectLayout,\n workspaceSettings,\n]);\n\nexport const routeTree = skeleton.rootRoute.addChildren([\n ${rootChildren},\n]);\n\nexport const router = createRouter({\n routeTree,\n context: {auth: undefined, queryClient: undefined} satisfies RouterContext,\n scrollRestoration: true,\n});\n\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: typeof router;\n }\n}\n`;\n}\n"],"names":["routePathForAnchor","literal","value","JSON","stringify","undefined","indentedLiteral","indentation","replaceAll","repeat","routeNames","routes","parent","map","route","index","filter","join","generateAppModule","navigation","settingsSections","generatedRoutes","routePath","path","imports","impl","routeDeclarations","rootRoutes","workspaceLayoutRoutes","projectLayoutRoutes","workspaceSettingsRoutes","rootChildren","Boolean"],"mappings":"AAEA,SAAQA,kBAAkB,QAAO,2BAA2B;AAQ5D,SAASC,QAAQC,KAAc;IAC7B,OAAOC,KAAKC,SAAS,CAACF,OAAOG,WAAW;AAC1C;AAEA,SAASC,gBAAgBJ,KAAc,EAAEK,WAAmB;IAC1D,OAAON,QAAQC,OAAOM,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,IAAIC,MAAM,CAACF,cAAc;AACvE;AAEA,SAASG,WAAWC,MAAgC,EAAEC,MAA+B;IACnF,OAAOD,OACJE,GAAG,CAAC,CAACC,OAAOC,QAAW,CAAA;YAACD;YAAOC;QAAK,CAAA,GACpCC,MAAM,CAAC,CAAC,EAACF,KAAK,EAAC,GAAKA,MAAMF,MAAM,KAAKA,QACrCC,GAAG,CAAC,CAAC,EAACE,KAAK,EAAC,GAAK,CAAC,KAAK,EAAEA,OAAO,EAChCE,IAAI,CAAC;AACV;AAEA,OAAO,SAASC,kBAAkB,EAChCP,MAAM,EACNQ,UAAU,EACVC,gBAAgB,EACS;IACzB,MAAMC,kBAAkBV,OAAOE,GAAG,CAAC,CAACC,QAAW,CAAA;YAC7C,GAAGA,KAAK;YACRQ,WAAWtB,mBAAmBc,MAAMF,MAAM,EAAEE,MAAMS,IAAI;QACxD,CAAA;IACA,MAAMC,UAAUb,OACbE,GAAG,CAAC,CAACC,OAAOC,QAAU,CAAC,YAAY,EAAEA,MAAM,UAAU,EAAEd,QAAQa,MAAMW,IAAI,EAAE,CAAC,CAAC,EAC7ER,IAAI,CAAC;IACR,MAAMS,oBAAoBL,gBACvBR,GAAG,CACF,CAACC,OAAOC,QAAU,CAAC,WAAW,EAAEA,MAAM;wBACpB,EAAED,MAAMF,MAAM,KAAK,SAAS,uBAAuB,CAAC,SAAS,EAAEE,MAAMF,MAAM,EAAE,CAAC;QAC9F,EAAEX,QAAQa,MAAMQ,SAAS,EAAE;uBACZ,EAAEP,MAAM,MAAM,EAAEd,QAAQa,MAAMW,IAAI,EAAE,EAAE,EAAExB,QAAQa,MAAMS,IAAI,EAAE;GAChF,CAAC,EAECN,IAAI,CAAC;IACR,MAAMU,aAAajB,WAAWW,iBAAiB;IAC/C,MAAMO,wBAAwBlB,WAAWW,iBAAiB;IAC1D,MAAMQ,sBAAsBnB,WAAWW,iBAAiB;IACxD,MAAMS,0BAA0BpB,WAAWW,iBAAiB;IAC5D,MAAMU,eAAe;QAACJ;QAAY;KAAkB,CAACX,MAAM,CAACgB,SAASf,IAAI,CAAC;IAE1E,OAAO,CAAC;;;;;AAKV,EAAEO,QAAQ;;;;;;;;;;cAUI,EAAElB,gBAAgBa,YAAY,GAAG;oBAC3B,EAAEb,gBAAgBc,kBAAkB,GAAG;;;AAG3D,EAAEM,kBAAkB;;0DAEsC,EAAEG,oBAAoB;kEACd,EAAEC,wBAAwB;;EAE1F,EAAEF,wBAAwBA,wBAAwB,MAAM,GAAG;;;;;;EAM3D,EAAEG,aAAa;;;;;;;;;;;;;;AAcjB,CAAC;AACD"}
1
+ {"version":3,"sources":["../../src/vite/generate.ts"],"sourcesContent":["import type {ComposedRoute} from '#compose/compose-routes.js';\nimport type {NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {routePathForAnchor} from '#runtime/anchor-paths.js';\n\nexport interface GenerateAppModuleOptions {\n routes: readonly ComposedRoute[];\n navigation: readonly NavTabEntry[];\n settingsSections: readonly SettingsSectionEntry[];\n}\n\nfunction literal(value: unknown): string {\n return JSON.stringify(value, undefined, 2);\n}\n\nfunction indentedLiteral(value: unknown, indentation: number): string {\n return literal(value).replaceAll('\\n', `\\n${' '.repeat(indentation)}`);\n}\n\nfunction routeNames(routes: readonly ComposedRoute[], parent: ComposedRoute['parent']): string {\n return routes\n .map((route, index) => ({route, index}))\n .filter(({route}) => route.parent === parent)\n .map(({index}) => `route${index}`)\n .join(', ');\n}\n\nexport function generateAppModule({\n routes,\n navigation,\n settingsSections,\n}: GenerateAppModuleOptions): string {\n const generatedRoutes = routes.map((route) => ({\n ...route,\n routePath: routePathForAnchor(route.parent, route.path),\n }));\n const imports = routes\n .map((route, index) => `import * as route${index}Module from ${literal(route.impl)};`)\n .join('\\n');\n const routeDeclarations = generatedRoutes\n .map(\n (route, index) => `const route${index} = createRoute({\n getParentRoute: () => ${route.parent === 'root' ? 'skeleton.rootRoute' : `skeleton.${route.parent}`},\n path: ${literal(route.routePath)},\n ...routeOptions(route${index}Module.default, ${literal(route.impl)}, ${literal(route.path)}),\n});`,\n )\n .join('\\n\\n');\n const rootRoutes = routeNames(generatedRoutes, 'root');\n const workspaceLayoutRoutes = routeNames(generatedRoutes, 'workspaceLayout');\n const projectLayoutRoutes = routeNames(generatedRoutes, 'projectLayout');\n const workspaceSettingsRoutes = routeNames(generatedRoutes, 'workspaceSettings');\n const rootChildren = [rootRoutes, 'workspaceLayout'].filter(Boolean).join(',\\n ');\n\n return `// GENERATED by @shipfox/client-shell/vite. Do not edit.\n// biome-ignore-all format: generated code has stable, reviewable output.\n// biome-ignore-all assist/source/organizeImports: generated imports follow route order.\nimport {createRoute, createRouter} from '@tanstack/react-router';\nimport {buildAnchorSkeleton, isRouteImpl, type RouteImpl, type RouterContext} from '@shipfox/client-shell/runtime';\n${imports}\n\nfunction routeOptions<T extends RouteImpl>(routeImpl: T, impl: string, path: string): T['options'] {\n if (!isRouteImpl(routeImpl)) {\n throw new TypeError(\\`Route implementation \"\\${impl}\" for \"\\${path}\" must export default defineRoute(...).\\`);\n }\n return routeImpl.options;\n}\n\nconst skeleton = buildAnchorSkeleton({\n navigation: ${indentedLiteral(navigation, 2)},\n settingsSections: ${indentedLiteral(settingsSections, 2)},\n});\n\n${routeDeclarations}\n\nconst projectLayout = skeleton.projectLayout.addChildren([${projectLayoutRoutes}]);\nconst workspaceSettings = skeleton.workspaceSettings.addChildren([${workspaceSettingsRoutes}]);\nconst workspaceLayout = skeleton.workspaceLayout.addChildren([\n ${workspaceLayoutRoutes}${workspaceLayoutRoutes ? ',' : ''}\n projectLayout,\n workspaceSettings,\n]);\n\nexport const routeTree = skeleton.rootRoute.addChildren([\n ${rootChildren},\n]);\n\nexport const router = createRouter({\n routeTree,\n context: {auth: undefined, queryClient: undefined} satisfies RouterContext,\n scrollRestoration: true,\n});\n\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: typeof router;\n }\n}\n`;\n}\n"],"names":["routePathForAnchor","literal","value","JSON","stringify","undefined","indentedLiteral","indentation","replaceAll","repeat","routeNames","routes","parent","map","route","index","filter","join","generateAppModule","navigation","settingsSections","generatedRoutes","routePath","path","imports","impl","routeDeclarations","rootRoutes","workspaceLayoutRoutes","projectLayoutRoutes","workspaceSettingsRoutes","rootChildren","Boolean"],"mappings":"AAEA,SAAQA,kBAAkB,QAAO,2BAA2B;AAQ5D,SAASC,QAAQC,KAAc;IAC7B,OAAOC,KAAKC,SAAS,CAACF,OAAOG,WAAW;AAC1C;AAEA,SAASC,gBAAgBJ,KAAc,EAAEK,WAAmB;IAC1D,OAAON,QAAQC,OAAOM,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,IAAIC,MAAM,CAACF,cAAc;AACvE;AAEA,SAASG,WAAWC,MAAgC,EAAEC,MAA+B;IACnF,OAAOD,OACJE,GAAG,CAAC,CAACC,OAAOC,QAAW,CAAA;YAACD;YAAOC;QAAK,CAAA,GACpCC,MAAM,CAAC,CAAC,EAACF,KAAK,EAAC,GAAKA,MAAMF,MAAM,KAAKA,QACrCC,GAAG,CAAC,CAAC,EAACE,KAAK,EAAC,GAAK,CAAC,KAAK,EAAEA,OAAO,EAChCE,IAAI,CAAC;AACV;AAEA,OAAO,SAASC,kBAAkB,EAChCP,MAAM,EACNQ,UAAU,EACVC,gBAAgB,EACS;IACzB,MAAMC,kBAAkBV,OAAOE,GAAG,CAAC,CAACC,QAAW,CAAA;YAC7C,GAAGA,KAAK;YACRQ,WAAWtB,mBAAmBc,MAAMF,MAAM,EAAEE,MAAMS,IAAI;QACxD,CAAA;IACA,MAAMC,UAAUb,OACbE,GAAG,CAAC,CAACC,OAAOC,QAAU,CAAC,iBAAiB,EAAEA,MAAM,YAAY,EAAEd,QAAQa,MAAMW,IAAI,EAAE,CAAC,CAAC,EACpFR,IAAI,CAAC;IACR,MAAMS,oBAAoBL,gBACvBR,GAAG,CACF,CAACC,OAAOC,QAAU,CAAC,WAAW,EAAEA,MAAM;wBACpB,EAAED,MAAMF,MAAM,KAAK,SAAS,uBAAuB,CAAC,SAAS,EAAEE,MAAMF,MAAM,EAAE,CAAC;QAC9F,EAAEX,QAAQa,MAAMQ,SAAS,EAAE;uBACZ,EAAEP,MAAM,gBAAgB,EAAEd,QAAQa,MAAMW,IAAI,EAAE,EAAE,EAAExB,QAAQa,MAAMS,IAAI,EAAE;GAC1F,CAAC,EAECN,IAAI,CAAC;IACR,MAAMU,aAAajB,WAAWW,iBAAiB;IAC/C,MAAMO,wBAAwBlB,WAAWW,iBAAiB;IAC1D,MAAMQ,sBAAsBnB,WAAWW,iBAAiB;IACxD,MAAMS,0BAA0BpB,WAAWW,iBAAiB;IAC5D,MAAMU,eAAe;QAACJ;QAAY;KAAkB,CAACX,MAAM,CAACgB,SAASf,IAAI,CAAC;IAE1E,OAAO,CAAC;;;;;AAKV,EAAEO,QAAQ;;;;;;;;;;cAUI,EAAElB,gBAAgBa,YAAY,GAAG;oBAC3B,EAAEb,gBAAgBc,kBAAkB,GAAG;;;AAG3D,EAAEM,kBAAkB;;0DAEsC,EAAEG,oBAAoB;kEACd,EAAEC,wBAAwB;;EAE1F,EAAEF,wBAAwBA,wBAAwB,MAAM,GAAG;;;;;;EAM3D,EAAEG,aAAa;;;;;;;;;;;;;;AAcjB,CAAC;AACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/vite/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,MAAM,EAAiB,MAAM,MAAM,CAAC;AAWjD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAQD,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,GAAgC,GACjC,EAAE,+BAA+B,GAAG,MAAM,CAsF1C"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/vite/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,MAAM,EAAiB,MAAM,MAAM,CAAC;AASjD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAID,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,GAAgC,GACjC,EAAE,+BAA+B,GAAG,MAAM,CA+E1C"}
@@ -5,38 +5,29 @@ import { mergeConfigShapes } from '#compose/merge-config.js';
5
5
  import { validateProviderIds } from '#compose/validate-providers.js';
6
6
  import { validateNavigation, validateSettingsSections } from '#compose/validate-registries.js';
7
7
  import { navigationEntries, settingsEntries } from '#runtime/registries.js';
8
- import { evaluateFeatures, invalidateFeatures } from './evaluate-features.js';
8
+ import { evaluateFeatures } from './evaluate-features.js';
9
9
  import { generateAppModule } from './generate.js';
10
- const defaultExportPattern = /\bexport\s+default\b|\bexport\s*\{[\s\S]*?\bas\s+default\b[\s\S]*?\}/;
11
- function hasDefaultExport(source) {
12
- return defaultExportPattern.test(source);
13
- }
14
10
  export function shipfoxClientComposition({ features, out = './src/shipfox-app.gen.ts' }) {
15
11
  let config;
16
12
  let watchedFiles = new Set();
17
13
  const outputPath = ()=>resolve(config?.root ?? process.cwd(), out);
18
14
  const featuresPath = ()=>resolve(config?.root ?? process.cwd(), features);
19
- async function assertDefaultRouteExports(resolveRoute, routes) {
15
+ async function assertRoutesResolve(resolveRoute, routes) {
20
16
  for (const route of routes){
21
17
  const resolvedRoute = await resolveRoute(route.impl, outputPath());
22
18
  if (!resolvedRoute) {
23
19
  throw new Error(`Could not resolve route implementation "${route.impl}" for "${route.path}".`);
24
20
  }
25
- const source = await readFile(resolvedRoute.id, 'utf8');
26
- if (!hasDefaultExport(source)) {
27
- throw new Error(`Route implementation "${route.impl}" for "${route.path}" must export default defineRoute(...).`);
28
- }
29
21
  }
30
22
  }
31
23
  async function generate({ addWatchFile, resolveRoute }) {
32
- invalidateFeatures(watchedFiles);
33
24
  const evaluated = await evaluateFeatures(featuresPath());
34
25
  const routes = composeRoutes(evaluated.features);
35
26
  validateProviderIds(evaluated.features);
36
27
  validateNavigation(evaluated.features, routes.map((route)=>route.path));
37
28
  validateSettingsSections(evaluated.features, routes.map((route)=>route.path));
38
29
  mergeConfigShapes(evaluated.features);
39
- await assertDefaultRouteExports(resolveRoute, routes);
30
+ await assertRoutesResolve(resolveRoute, routes);
40
31
  watchedFiles = new Set(evaluated.loadedFiles);
41
32
  for (const file of watchedFiles)addWatchFile(file);
42
33
  const output = generateAppModule({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/plugin.ts"],"sourcesContent":["import {mkdir, readFile, realpath, writeFile} from 'node:fs/promises';\nimport {dirname, resolve} from 'node:path';\nimport type {Plugin, ResolvedConfig} from 'vite';\nimport {composeRoutes} from '#compose/compose-routes.js';\nimport {mergeConfigShapes} from '#compose/merge-config.js';\nimport {validateProviderIds} from '#compose/validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from '#compose/validate-registries.js';\nimport {navigationEntries, settingsEntries} from '#runtime/registries.js';\nimport {evaluateFeatures, invalidateFeatures} from './evaluate-features.js';\nimport {generateAppModule} from './generate.js';\n\nconst defaultExportPattern = /\\bexport\\s+default\\b|\\bexport\\s*\\{[\\s\\S]*?\\bas\\s+default\\b[\\s\\S]*?\\}/;\n\nexport interface ShipfoxClientCompositionOptions {\n features: string;\n out?: string;\n}\n\ntype RouteResolver = (source: string, importer?: string) => Promise<{id: string} | null>;\n\nfunction hasDefaultExport(source: string): boolean {\n return defaultExportPattern.test(source);\n}\n\nexport function shipfoxClientComposition({\n features,\n out = './src/shipfox-app.gen.ts',\n}: ShipfoxClientCompositionOptions): Plugin {\n let config: ResolvedConfig | undefined;\n let watchedFiles = new Set<string>();\n\n const outputPath = () => resolve(config?.root ?? process.cwd(), out);\n const featuresPath = () => resolve(config?.root ?? process.cwd(), features);\n\n async function assertDefaultRouteExports(\n resolveRoute: RouteResolver,\n routes: ReturnType<typeof composeRoutes>,\n ): Promise<void> {\n for (const route of routes) {\n const resolvedRoute = await resolveRoute(route.impl, outputPath());\n if (!resolvedRoute) {\n throw new Error(\n `Could not resolve route implementation \"${route.impl}\" for \"${route.path}\".`,\n );\n }\n const source = await readFile(resolvedRoute.id, 'utf8');\n if (!hasDefaultExport(source)) {\n throw new Error(\n `Route implementation \"${route.impl}\" for \"${route.path}\" must export default defineRoute(...).`,\n );\n }\n }\n }\n\n async function generate({\n addWatchFile,\n resolveRoute,\n }: {\n addWatchFile(file: string): void;\n resolveRoute: RouteResolver;\n }): Promise<void> {\n invalidateFeatures(watchedFiles);\n const evaluated = await evaluateFeatures(featuresPath());\n const routes = composeRoutes(evaluated.features);\n validateProviderIds(evaluated.features);\n validateNavigation(\n evaluated.features,\n routes.map((route) => route.path),\n );\n validateSettingsSections(\n evaluated.features,\n routes.map((route) => route.path),\n );\n mergeConfigShapes(evaluated.features);\n await assertDefaultRouteExports(resolveRoute, routes);\n\n watchedFiles = new Set(evaluated.loadedFiles);\n for (const file of watchedFiles) addWatchFile(file);\n\n const output = generateAppModule({\n routes,\n navigation: navigationEntries(evaluated.features),\n settingsSections: settingsEntries(evaluated.features),\n });\n const path = outputPath();\n const existing = await readFile(path, 'utf8').catch(() => undefined);\n if (existing === output) return;\n await mkdir(dirname(path), {recursive: true});\n await writeFile(path, output);\n }\n\n return {\n name: 'shipfox-client-composition',\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n },\n async buildStart() {\n await generate({\n addWatchFile: this.addWatchFile.bind(this),\n resolveRoute: this.resolve.bind(this),\n });\n },\n async hotUpdate({file, server}) {\n const resolvedFile = await realpath(file).catch(() => resolve(file));\n if (!watchedFiles.has(resolvedFile)) return;\n await generate({\n addWatchFile: server.watcher.add.bind(server.watcher),\n resolveRoute: this.environment.pluginContainer.resolveId.bind(\n this.environment.pluginContainer,\n ),\n });\n },\n };\n}\n"],"names":["mkdir","readFile","realpath","writeFile","dirname","resolve","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","navigationEntries","settingsEntries","evaluateFeatures","invalidateFeatures","generateAppModule","defaultExportPattern","hasDefaultExport","source","test","shipfoxClientComposition","features","out","config","watchedFiles","Set","outputPath","root","process","cwd","featuresPath","assertDefaultRouteExports","resolveRoute","routes","route","resolvedRoute","impl","Error","path","id","generate","addWatchFile","evaluated","map","loadedFiles","file","output","navigation","settingsSections","existing","catch","undefined","recursive","name","configResolved","resolvedConfig","buildStart","bind","hotUpdate","server","resolvedFile","has","watcher","add","environment","pluginContainer","resolveId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/vite/plugin.ts"],"sourcesContent":["import {mkdir, readFile, realpath, writeFile} from 'node:fs/promises';\nimport {dirname, resolve} from 'node:path';\nimport type {Plugin, ResolvedConfig} from 'vite';\nimport {composeRoutes} from '#compose/compose-routes.js';\nimport {mergeConfigShapes} from '#compose/merge-config.js';\nimport {validateProviderIds} from '#compose/validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from '#compose/validate-registries.js';\nimport {navigationEntries, settingsEntries} from '#runtime/registries.js';\nimport {evaluateFeatures} from './evaluate-features.js';\nimport {generateAppModule} from './generate.js';\n\nexport interface ShipfoxClientCompositionOptions {\n features: string;\n out?: string;\n}\n\ntype RouteResolver = (source: string, importer?: string) => Promise<{id: string} | null>;\n\nexport function shipfoxClientComposition({\n features,\n out = './src/shipfox-app.gen.ts',\n}: ShipfoxClientCompositionOptions): Plugin {\n let config: ResolvedConfig | undefined;\n let watchedFiles = new Set<string>();\n\n const outputPath = () => resolve(config?.root ?? process.cwd(), out);\n const featuresPath = () => resolve(config?.root ?? process.cwd(), features);\n\n async function assertRoutesResolve(\n resolveRoute: RouteResolver,\n routes: ReturnType<typeof composeRoutes>,\n ): Promise<void> {\n for (const route of routes) {\n const resolvedRoute = await resolveRoute(route.impl, outputPath());\n if (!resolvedRoute) {\n throw new Error(\n `Could not resolve route implementation \"${route.impl}\" for \"${route.path}\".`,\n );\n }\n }\n }\n\n async function generate({\n addWatchFile,\n resolveRoute,\n }: {\n addWatchFile(file: string): void;\n resolveRoute: RouteResolver;\n }): Promise<void> {\n const evaluated = await evaluateFeatures(featuresPath());\n const routes = composeRoutes(evaluated.features);\n validateProviderIds(evaluated.features);\n validateNavigation(\n evaluated.features,\n routes.map((route) => route.path),\n );\n validateSettingsSections(\n evaluated.features,\n routes.map((route) => route.path),\n );\n mergeConfigShapes(evaluated.features);\n await assertRoutesResolve(resolveRoute, routes);\n\n watchedFiles = new Set(evaluated.loadedFiles);\n for (const file of watchedFiles) addWatchFile(file);\n\n const output = generateAppModule({\n routes,\n navigation: navigationEntries(evaluated.features),\n settingsSections: settingsEntries(evaluated.features),\n });\n const path = outputPath();\n const existing = await readFile(path, 'utf8').catch(() => undefined);\n if (existing === output) return;\n await mkdir(dirname(path), {recursive: true});\n await writeFile(path, output);\n }\n\n return {\n name: 'shipfox-client-composition',\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n },\n async buildStart() {\n await generate({\n addWatchFile: this.addWatchFile.bind(this),\n resolveRoute: this.resolve.bind(this),\n });\n },\n async hotUpdate({file, server}) {\n const resolvedFile = await realpath(file).catch(() => resolve(file));\n if (!watchedFiles.has(resolvedFile)) return;\n await generate({\n addWatchFile: server.watcher.add.bind(server.watcher),\n resolveRoute: this.environment.pluginContainer.resolveId.bind(\n this.environment.pluginContainer,\n ),\n });\n },\n };\n}\n"],"names":["mkdir","readFile","realpath","writeFile","dirname","resolve","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","navigationEntries","settingsEntries","evaluateFeatures","generateAppModule","shipfoxClientComposition","features","out","config","watchedFiles","Set","outputPath","root","process","cwd","featuresPath","assertRoutesResolve","resolveRoute","routes","route","resolvedRoute","impl","Error","path","generate","addWatchFile","evaluated","map","loadedFiles","file","output","navigation","settingsSections","existing","catch","undefined","recursive","name","configResolved","resolvedConfig","buildStart","bind","hotUpdate","server","resolvedFile","has","watcher","add","environment","pluginContainer","resolveId"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-shell",
3
3
  "license": "MIT",
4
- "version": "0.2.0",
4
+ "version": "1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -11,67 +11,40 @@
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
13
  "imports": {
14
- "#*": {
15
- "workspace-source": "./src/*",
16
- "development": "./src/*",
17
- "default": "./dist/*"
18
- },
14
+ "#*": "./dist/*",
19
15
  "#test/*": "./test/*"
20
16
  },
21
17
  "exports": {
22
18
  ".": {
23
- "development": {
24
- "types": "./src/index.ts",
25
- "default": "./src/index.ts"
26
- },
27
- "default": {
28
- "types": "./dist/index.d.ts",
29
- "default": "./dist/index.js"
30
- }
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
31
21
  },
32
22
  "./runtime": {
33
- "development": {
34
- "types": "./src/runtime/index.ts",
35
- "default": "./src/runtime/index.ts"
36
- },
37
- "default": {
38
- "types": "./dist/runtime/index.d.ts",
39
- "default": "./dist/runtime/index.js"
40
- }
23
+ "types": "./dist/runtime/index.d.ts",
24
+ "default": "./dist/runtime/index.js"
41
25
  },
42
26
  "./testing": {
43
- "development": {
44
- "types": "./src/testing/index.tsx",
45
- "default": "./src/testing/index.tsx"
46
- },
47
- "default": {
48
- "types": "./dist/testing/index.d.ts",
49
- "default": "./dist/testing/index.js"
50
- }
27
+ "types": "./dist/testing/index.d.ts",
28
+ "default": "./dist/testing/index.js"
51
29
  },
52
30
  "./vite": {
53
- "development": {
54
- "types": "./src/vite/index.ts",
55
- "default": "./src/vite/index.ts"
56
- },
57
- "default": {
58
- "types": "./dist/vite/index.d.ts",
59
- "default": "./dist/vite/index.js"
60
- }
31
+ "types": "./dist/vite/index.d.ts",
32
+ "default": "./dist/vite/index.js"
61
33
  }
62
34
  },
63
35
  "dependencies": {
64
36
  "@swc/helpers": "^0.5.17",
65
37
  "@tanstack/router-core": "^1.171.13",
66
- "jiti": "^2.7.0",
38
+ "tsx": "^4.22.4",
67
39
  "framer-motion": "^12.23.24",
68
- "@shipfox/api-auth-dto": "2.0.0",
69
- "@shipfox/client-config": "0.2.0",
70
- "@shipfox/client-api": "0.2.0",
71
- "@shipfox/api-workspaces-dto": "2.0.0",
72
- "@shipfox/react-ui": "0.3.1"
40
+ "@shipfox/api-workspaces-dto": "5.0.0",
41
+ "@shipfox/client-api": "1.0.0",
42
+ "@shipfox/client-config": "1.0.0",
43
+ "@shipfox/api-auth-dto": "5.0.0",
44
+ "@shipfox/react-ui": "0.3.2"
73
45
  },
74
46
  "peerDependencies": {
47
+ "@storybook/react": "^10.4.6",
75
48
  "@tanstack/react-query": "^5.101.0",
76
49
  "@tanstack/react-router": "^1.170.16",
77
50
  "jotai": "^2.19.1",
@@ -80,6 +53,11 @@
80
53
  "vite": "^8.0.3",
81
54
  "zod": "^4.4.3"
82
55
  },
56
+ "peerDependenciesMeta": {
57
+ "@storybook/react": {
58
+ "optional": true
59
+ }
60
+ },
83
61
  "devDependencies": {
84
62
  "@storybook/react": "^10.0.0",
85
63
  "@tanstack/react-query": "^5.101.0",
@@ -94,18 +72,19 @@
94
72
  "react-dom": "^19.1.1",
95
73
  "vite": "^8.0.3",
96
74
  "zod": "^4.4.3",
97
- "@shipfox/biome": "1.8.1",
98
- "@shipfox/client-test-setup": "0.0.2",
99
- "@shipfox/typescript": "1.1.6",
75
+ "@shipfox/biome": "1.8.2",
76
+ "@shipfox/swc": "1.2.6",
77
+ "@shipfox/typescript": "1.1.7",
100
78
  "@shipfox/ts-config": "1.3.8",
101
- "@shipfox/vitest": "1.2.2",
102
- "@shipfox/swc": "1.2.5"
79
+ "@shipfox/vitest": "1.2.3",
80
+ "@shipfox/client-test-setup": "0.0.3"
103
81
  },
104
82
  "scripts": {
105
83
  "build": "shipfox-swc",
106
84
  "check": "shipfox-biome-check",
107
85
  "check:fix": "shipfox-biome-check --write",
108
86
  "test": "shipfox-vitest-run",
87
+ "test:external": "turbo build type:emit --filter=@shipfox/client-features... --filter=@shipfox/application-release && node test/external/verify.mjs",
109
88
  "test:watch": "shipfox-vitest-watch",
110
89
  "type": "shipfox-tsc-check",
111
90
  "type:emit": "shipfox-tsc-emit"
@@ -1,7 +1,11 @@
1
1
  import {mkdtemp, realpath, rm, writeFile} from 'node:fs/promises';
2
2
  import {tmpdir} from 'node:os';
3
3
  import {join} from 'node:path';
4
- import {evaluateFeatures, invalidateFeatures} from './evaluate-features.js';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {evaluateFeatures} from './evaluate-features.js';
6
+
7
+ const fixtureFeatures = fileURLToPath(new URL('../../test/fixtures/features.ts', import.meta.url));
8
+ const contractSource = fileURLToPath(new URL('../contract.ts', import.meta.url));
5
9
 
6
10
  describe('evaluateFeatures', () => {
7
11
  let directory: string;
@@ -14,7 +18,7 @@ describe('evaluateFeatures', () => {
14
18
  await rm(directory, {recursive: true, force: true});
15
19
  });
16
20
 
17
- test('returns dependencies that were cached before the manifest reload', async () => {
21
+ test('reloads dependencies and reports them as watched files', async () => {
18
22
  const features = join(directory, 'features.ts');
19
23
  const contribution = join(directory, 'contribution.ts');
20
24
  await writeFile(
@@ -25,10 +29,61 @@ export const features = [feature];`,
25
29
  await writeFile(contribution, `export const feature = {id: 'acme.projects'};`);
26
30
 
27
31
  await evaluateFeatures(features);
28
- invalidateFeatures([await realpath(features)]);
32
+ await writeFile(contribution, `export const feature = {id: 'acme.reports'};`);
33
+
34
+ const evaluated = await evaluateFeatures(features);
35
+
36
+ expect(evaluated.features).toEqual([{id: 'acme.reports'}]);
37
+ expect(evaluated.loadedFiles).toContain(await realpath(contribution));
38
+ });
39
+
40
+ test('reloads JSON dependencies and reports them as watched files', async () => {
41
+ const features = join(directory, 'features.ts');
42
+ const contribution = join(directory, 'contribution.json');
43
+ await writeFile(
44
+ features,
45
+ `import feature from './contribution.json';
46
+ export const features = [feature];`,
47
+ );
48
+ await writeFile(contribution, JSON.stringify({id: 'acme.projects'}));
49
+
50
+ await evaluateFeatures(features);
51
+ await writeFile(contribution, JSON.stringify({id: 'acme.reports'}));
29
52
 
30
53
  const evaluated = await evaluateFeatures(features);
31
54
 
55
+ expect(evaluated.features).toEqual([{id: 'acme.reports'}]);
32
56
  expect(evaluated.loadedFiles).toContain(await realpath(contribution));
33
57
  });
58
+
59
+ test('evicts JSON dependencies after evaluation fails', async () => {
60
+ const features = join(directory, 'features.ts');
61
+ const contribution = join(directory, 'contribution.json');
62
+ await writeFile(
63
+ features,
64
+ `import feature from './contribution.json';
65
+ throw new Error('fixture failure');
66
+ export const features = [feature];`,
67
+ );
68
+ await writeFile(contribution, JSON.stringify({id: 'acme.projects'}));
69
+
70
+ const failedEvaluation = evaluateFeatures(features);
71
+ await expect(failedEvaluation).rejects.toThrow('fixture failure');
72
+ await writeFile(
73
+ features,
74
+ `import feature from './contribution.json';
75
+ export const features = [feature];`,
76
+ );
77
+ await writeFile(contribution, JSON.stringify({id: 'acme.reports'}));
78
+
79
+ const evaluated = await evaluateFeatures(features);
80
+
81
+ expect(evaluated.features).toEqual([{id: 'acme.reports'}]);
82
+ });
83
+
84
+ test('resolves package imports through the workspace source condition', async () => {
85
+ const evaluated = await evaluateFeatures(fixtureFeatures);
86
+
87
+ expect(evaluated.loadedFiles).toContain(await realpath(contractSource));
88
+ });
34
89
  });