@shipfox/client-shell 0.2.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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/dist/components/main-layout.d.ts +11 -0
- package/dist/components/main-layout.d.ts.map +1 -0
- package/dist/components/main-layout.js +39 -0
- package/dist/components/main-layout.js.map +1 -0
- package/dist/components/nav-bar.d.ts +4 -0
- package/dist/components/nav-bar.d.ts.map +1 -0
- package/dist/components/nav-bar.js +62 -0
- package/dist/components/nav-bar.js.map +1 -0
- package/dist/components/nav-tabs.d.ts +6 -0
- package/dist/components/nav-tabs.d.ts.map +1 -0
- package/dist/components/nav-tabs.js +38 -0
- package/dist/components/nav-tabs.js.map +1 -0
- package/dist/components/not-found-page.d.ts +2 -0
- package/dist/components/not-found-page.d.ts.map +1 -0
- package/dist/components/not-found-page.js +25 -0
- package/dist/components/not-found-page.js.map +1 -0
- package/dist/components/settings-nav.d.ts +5 -0
- package/dist/components/settings-nav.d.ts.map +1 -0
- package/dist/components/settings-nav.js +45 -0
- package/dist/components/settings-nav.js.map +1 -0
- package/dist/components/user-menu.d.ts +2 -0
- package/dist/components/user-menu.d.ts.map +1 -0
- package/dist/components/user-menu.js +76 -0
- package/dist/components/user-menu.js.map +1 -0
- package/dist/components/workspace-crumb.d.ts +7 -0
- package/dist/components/workspace-crumb.d.ts.map +1 -0
- package/dist/components/workspace-crumb.js +54 -0
- package/dist/components/workspace-crumb.js.map +1 -0
- package/dist/components/workspace-switcher.d.ts +5 -0
- package/dist/components/workspace-switcher.d.ts.map +1 -0
- package/dist/components/workspace-switcher.js +90 -0
- package/dist/components/workspace-switcher.js.map +1 -0
- package/dist/compose/compose-routes.d.ts +6 -0
- package/dist/compose/compose-routes.d.ts.map +1 -0
- package/dist/compose/compose-routes.js +53 -0
- package/dist/compose/compose-routes.js.map +1 -0
- package/dist/compose/errors.d.ts +26 -0
- package/dist/compose/errors.d.ts.map +1 -0
- package/dist/compose/errors.js +39 -0
- package/dist/compose/errors.js.map +1 -0
- package/dist/compose/merge-config.d.ts +4 -0
- package/dist/compose/merge-config.d.ts.map +1 -0
- package/dist/compose/merge-config.js +28 -0
- package/dist/compose/merge-config.js.map +1 -0
- package/dist/compose/normalize-route-path.d.ts +2 -0
- package/dist/compose/normalize-route-path.d.ts.map +1 -0
- package/dist/compose/normalize-route-path.js +7 -0
- package/dist/compose/normalize-route-path.js.map +1 -0
- package/dist/compose/validate-providers.d.ts +3 -0
- package/dist/compose/validate-providers.d.ts.map +1 -0
- package/dist/compose/validate-providers.js +32 -0
- package/dist/compose/validate-providers.js.map +1 -0
- package/dist/compose/validate-registries.d.ts +4 -0
- package/dist/compose/validate-registries.d.ts.map +1 -0
- package/dist/compose/validate-registries.js +50 -0
- package/dist/compose/validate-registries.js.map +1 -0
- package/dist/config/shell-config.d.ts +10 -0
- package/dist/config/shell-config.d.ts.map +1 -0
- package/dist/config/shell-config.js +12 -0
- package/dist/config/shell-config.js.map +1 -0
- package/dist/contract.d.ts +39 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/active-workspace.d.ts +4 -0
- package/dist/runtime/active-workspace.d.ts.map +1 -0
- package/dist/runtime/active-workspace.js +15 -0
- package/dist/runtime/active-workspace.js.map +1 -0
- package/dist/runtime/anchor-paths.d.ts +9 -0
- package/dist/runtime/anchor-paths.d.ts.map +1 -0
- package/dist/runtime/anchor-paths.js +18 -0
- package/dist/runtime/anchor-paths.js.map +1 -0
- package/dist/runtime/anchors.d.ts +19 -0
- package/dist/runtime/anchors.d.ts.map +1 -0
- package/dist/runtime/anchors.js +125 -0
- package/dist/runtime/anchors.js.map +1 -0
- package/dist/runtime/assemble-route-tree.d.ts +10 -0
- package/dist/runtime/assemble-route-tree.d.ts.map +1 -0
- package/dist/runtime/assemble-route-tree.js +30 -0
- package/dist/runtime/assemble-route-tree.js.map +1 -0
- package/dist/runtime/auth.d.ts +50 -0
- package/dist/runtime/auth.d.ts.map +1 -0
- package/dist/runtime/auth.js +194 -0
- package/dist/runtime/auth.js.map +1 -0
- package/dist/runtime/chrome-context.d.ts +10 -0
- package/dist/runtime/chrome-context.d.ts.map +1 -0
- package/dist/runtime/chrome-context.js +16 -0
- package/dist/runtime/chrome-context.js.map +1 -0
- package/dist/runtime/compose-client-app.d.ts +13 -0
- package/dist/runtime/compose-client-app.d.ts.map +1 -0
- package/dist/runtime/compose-client-app.js +93 -0
- package/dist/runtime/compose-client-app.js.map +1 -0
- package/dist/runtime/define-route.d.ts +19 -0
- package/dist/runtime/define-route.d.ts.map +1 -0
- package/dist/runtime/define-route.js +12 -0
- package/dist/runtime/define-route.js.map +1 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +21 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/last-workspace.d.ts +4 -0
- package/dist/runtime/last-workspace.d.ts.map +1 -0
- package/dist/runtime/last-workspace.js +20 -0
- package/dist/runtime/last-workspace.js.map +1 -0
- package/dist/runtime/nav-order.d.ts +7 -0
- package/dist/runtime/nav-order.d.ts.map +1 -0
- package/dist/runtime/nav-order.js +8 -0
- package/dist/runtime/nav-order.js.map +1 -0
- package/dist/runtime/provider-stack.d.ts +14 -0
- package/dist/runtime/provider-stack.d.ts.map +1 -0
- package/dist/runtime/provider-stack.js +28 -0
- package/dist/runtime/provider-stack.js.map +1 -0
- package/dist/runtime/registries.d.ts +4 -0
- package/dist/runtime/registries.d.ts.map +1 -0
- package/dist/runtime/registries.js +23 -0
- package/dist/runtime/registries.js.map +1 -0
- package/dist/runtime/router-context.d.ts +9 -0
- package/dist/runtime/router-context.d.ts.map +1 -0
- package/dist/runtime/router-context.js +3 -0
- package/dist/runtime/router-context.js.map +1 -0
- package/dist/runtime/workspace-setup.d.ts +18 -0
- package/dist/runtime/workspace-setup.d.ts.map +1 -0
- package/dist/runtime/workspace-setup.js +64 -0
- package/dist/runtime/workspace-setup.js.map +1 -0
- package/dist/testing/index.d.ts +17 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/dist/vite/evaluate-features.d.ts +8 -0
- package/dist/vite/evaluate-features.d.ts.map +1 -0
- package/dist/vite/evaluate-features.js +80 -0
- package/dist/vite/evaluate-features.js.map +1 -0
- package/dist/vite/generate.d.ts +9 -0
- package/dist/vite/generate.d.ts.map +1 -0
- package/dist/vite/generate.js +80 -0
- package/dist/vite/generate.js.map +1 -0
- package/dist/vite/index.d.ts +2 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +3 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/plugin.d.ts +7 -0
- package/dist/vite/plugin.d.ts.map +1 -0
- package/dist/vite/plugin.js +77 -0
- package/dist/vite/plugin.js.map +1 -0
- package/package.json +113 -0
- package/src/components/main-layout.tsx +45 -0
- package/src/components/nav-bar.tsx +41 -0
- package/src/components/nav-tabs.test.tsx +60 -0
- package/src/components/nav-tabs.tsx +47 -0
- package/src/components/not-found-page.tsx +14 -0
- package/src/components/settings-nav.tsx +35 -0
- package/src/components/user-menu.tsx +59 -0
- package/src/components/workspace-crumb.tsx +43 -0
- package/src/components/workspace-switcher.tsx +75 -0
- package/src/compose/compose-routes.test.ts +112 -0
- package/src/compose/compose-routes.ts +60 -0
- package/src/compose/errors.ts +54 -0
- package/src/compose/merge-config.test.ts +26 -0
- package/src/compose/merge-config.ts +27 -0
- package/src/compose/normalize-route-path.ts +6 -0
- package/src/compose/validate-providers.test.ts +31 -0
- package/src/compose/validate-providers.ts +36 -0
- package/src/compose/validate-registries.test.ts +92 -0
- package/src/compose/validate-registries.ts +61 -0
- package/src/config/config-screen.test.tsx +37 -0
- package/src/config/shell-config.ts +10 -0
- package/src/contract.ts +47 -0
- package/src/index.ts +1 -0
- package/src/runtime/active-workspace.ts +13 -0
- package/src/runtime/anchor-paths.ts +18 -0
- package/src/runtime/anchors.tsx +106 -0
- package/src/runtime/assemble-route-tree.ts +42 -0
- package/src/runtime/auth.tsx +213 -0
- package/src/runtime/chrome-context.tsx +22 -0
- package/src/runtime/compose-client-app.tsx +122 -0
- package/src/runtime/define-route.test.ts +15 -0
- package/src/runtime/define-route.ts +33 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/last-workspace.ts +22 -0
- package/src/runtime/nav-order.ts +1 -0
- package/src/runtime/provider-stack.tsx +39 -0
- package/src/runtime/providers.test.tsx +51 -0
- package/src/runtime/registries.test.tsx +100 -0
- package/src/runtime/registries.ts +42 -0
- package/src/runtime/router-context.ts +9 -0
- package/src/runtime/routes.test.tsx +48 -0
- package/src/runtime/workspace-setup.tsx +61 -0
- package/src/testing/index.test.tsx +57 -0
- package/src/testing/index.tsx +51 -0
- package/src/vite/evaluate-features.test.ts +34 -0
- package/src/vite/evaluate-features.ts +93 -0
- package/src/vite/generate.test.ts +22 -0
- package/src/vite/generate.ts +99 -0
- package/src/vite/generated-router.test.tsx +58 -0
- package/src/vite/index.ts +1 -0
- package/src/vite/plugin.test.ts +181 -0
- package/src/vite/plugin.ts +114 -0
- package/src/vite/validate.test.ts +166 -0
- package/test/default-route-impl.tsx +3 -0
- package/test/external/FINDINGS.md +53 -0
- package/test/external/README.md +39 -0
- package/test/external/fixture/index.html +12 -0
- package/test/external/fixture/package.json +32 -0
- package/test/external/fixture/src/app.fixture.tsx +72 -0
- package/test/external/fixture/src/features/override-impl.tsx +12 -0
- package/test/external/fixture/src/features/override.tsx +20 -0
- package/test/external/fixture/src/features.collision.ts +15 -0
- package/test/external/fixture/src/features.ts +4 -0
- package/test/external/fixture/src/link-typecheck.tsx +10 -0
- package/test/external/fixture/src/main.tsx +4 -0
- package/test/external/fixture/tsconfig.json +15 -0
- package/test/external/fixture/vite.config.ts +10 -0
- package/test/external/fixture/vitest.config.ts +15 -0
- package/test/external/toy-feature/CHANGELOG.md +15 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-check +43 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-format +43 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-lint +43 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-swc +43 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-check +43 -0
- package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-emit +43 -0
- package/test/external/toy-feature/package.json +57 -0
- package/test/external/toy-feature/src/config.ts +5 -0
- package/test/external/toy-feature/src/index.ts +64 -0
- package/test/external/toy-feature/src/provider.tsx +33 -0
- package/test/external/toy-feature/src/routes/insights.tsx +7 -0
- package/test/external/toy-feature/src/routes/settings.tsx +7 -0
- package/test/external/toy-feature/tsconfig.build.json +5 -0
- package/test/external/toy-feature/tsconfig.json +1 -0
- package/test/external/verify.mjs +318 -0
- package/test/fixtures/features.ts +56 -0
- package/test/named-route-impl.tsx +3 -0
- package/test/not-route-impl.ts +1 -0
- package/test/render.tsx +67 -0
- package/test/search-route-impl.tsx +7 -0
- package/test/setup.ts +3 -0
- package/test/typecheck/shipfox-app.gen.ts +85 -0
- package/test/typecheck/types.tsx +18 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +1 -0
- package/tsconfig.test.json +12 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ClientFeature } from '#contract.js';
|
|
2
|
+
export interface EvaluatedFeatures {
|
|
3
|
+
features: readonly ClientFeature[];
|
|
4
|
+
loadedFiles: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function invalidateFeatures(loadedFiles: Iterable<string>): void;
|
|
7
|
+
export declare function evaluateFeatures(featuresModule: string): Promise<EvaluatedFeatures>;
|
|
8
|
+
//# sourceMappingURL=evaluate-features.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { createJiti } from 'jiti';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const importPattern = /(?:\b(?:import|export)\s+(?:[^'"]*?\s+from\s+)?|\bimport\s*\(\s*)['"]([^'"]+)['"]/g;
|
|
8
|
+
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);
|
|
18
|
+
}
|
|
19
|
+
visit(entry);
|
|
20
|
+
return [
|
|
21
|
+
...files
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
async function staticallyImportedModules(entry, jiti) {
|
|
25
|
+
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
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await visit(entry);
|
|
44
|
+
return [
|
|
45
|
+
...files
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
export async function evaluateFeatures(featuresModule) {
|
|
49
|
+
const resolvedFeaturesModule = await realpath(resolve(featuresModule));
|
|
50
|
+
const jiti = createJiti(resolvedFeaturesModule, {
|
|
51
|
+
interopDefault: false,
|
|
52
|
+
tsconfigPaths: true
|
|
53
|
+
});
|
|
54
|
+
let module;
|
|
55
|
+
let loadedFiles;
|
|
56
|
+
try {
|
|
57
|
+
module = jiti(resolvedFeaturesModule);
|
|
58
|
+
loadedFiles = [
|
|
59
|
+
...new Set([
|
|
60
|
+
...loadedModuleGraph(resolvedFeaturesModule),
|
|
61
|
+
...await staticallyImportedModules(resolvedFeaturesModule, jiti)
|
|
62
|
+
])
|
|
63
|
+
];
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Failed to evaluate features module "${resolvedFeaturesModule}". Features modules must be Node-safe: ${message}`, {
|
|
67
|
+
cause: error
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const features = module.features ?? module.default;
|
|
71
|
+
if (!Array.isArray(features)) {
|
|
72
|
+
throw new TypeError(`Features module "${resolvedFeaturesModule}" must export a features array.`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
features: features,
|
|
76
|
+
loadedFiles
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=evaluate-features.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComposedRoute } from '#compose/compose-routes.js';
|
|
2
|
+
import type { NavTabEntry, SettingsSectionEntry } from '#contract.js';
|
|
3
|
+
export interface GenerateAppModuleOptions {
|
|
4
|
+
routes: readonly ComposedRoute[];
|
|
5
|
+
navigation: readonly NavTabEntry[];
|
|
6
|
+
settingsSections: readonly SettingsSectionEntry[];
|
|
7
|
+
}
|
|
8
|
+
export declare function generateAppModule({ routes, navigation, settingsSections, }: GenerateAppModuleOptions): string;
|
|
9
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/vite/generate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAC,WAAW,EAAE,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAGpE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IACjC,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACnD;AAkBD,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,UAAU,EACV,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,MAAM,CAoEnC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { routePathForAnchor } from '#runtime/anchor-paths.js';
|
|
2
|
+
function literal(value) {
|
|
3
|
+
return JSON.stringify(value, undefined, 2);
|
|
4
|
+
}
|
|
5
|
+
function indentedLiteral(value, indentation) {
|
|
6
|
+
return literal(value).replaceAll('\n', `\n${' '.repeat(indentation)}`);
|
|
7
|
+
}
|
|
8
|
+
function routeNames(routes, parent) {
|
|
9
|
+
return routes.map((route, index)=>({
|
|
10
|
+
route,
|
|
11
|
+
index
|
|
12
|
+
})).filter(({ route })=>route.parent === parent).map(({ index })=>`route${index}`).join(', ');
|
|
13
|
+
}
|
|
14
|
+
export function generateAppModule({ routes, navigation, settingsSections }) {
|
|
15
|
+
const generatedRoutes = routes.map((route)=>({
|
|
16
|
+
...route,
|
|
17
|
+
routePath: routePathForAnchor(route.parent, route.path)
|
|
18
|
+
}));
|
|
19
|
+
const imports = routes.map((route, index)=>`import route${index}Impl from ${literal(route.impl)};`).join('\n');
|
|
20
|
+
const routeDeclarations = generatedRoutes.map((route, index)=>`const route${index} = createRoute({
|
|
21
|
+
getParentRoute: () => ${route.parent === 'root' ? 'skeleton.rootRoute' : `skeleton.${route.parent}`},
|
|
22
|
+
path: ${literal(route.routePath)},
|
|
23
|
+
...routeOptions(route${index}Impl, ${literal(route.impl)}, ${literal(route.path)}),
|
|
24
|
+
});`).join('\n\n');
|
|
25
|
+
const rootRoutes = routeNames(generatedRoutes, 'root');
|
|
26
|
+
const workspaceLayoutRoutes = routeNames(generatedRoutes, 'workspaceLayout');
|
|
27
|
+
const projectLayoutRoutes = routeNames(generatedRoutes, 'projectLayout');
|
|
28
|
+
const workspaceSettingsRoutes = routeNames(generatedRoutes, 'workspaceSettings');
|
|
29
|
+
const rootChildren = [
|
|
30
|
+
rootRoutes,
|
|
31
|
+
'workspaceLayout'
|
|
32
|
+
].filter(Boolean).join(',\n ');
|
|
33
|
+
return `// GENERATED by @shipfox/client-shell/vite. Do not edit.
|
|
34
|
+
// biome-ignore-all format: generated code has stable, reviewable output.
|
|
35
|
+
// biome-ignore-all assist/source/organizeImports: generated imports follow route order.
|
|
36
|
+
import {createRoute, createRouter} from '@tanstack/react-router';
|
|
37
|
+
import {buildAnchorSkeleton, isRouteImpl, type RouteImpl, type RouterContext} from '@shipfox/client-shell/runtime';
|
|
38
|
+
${imports}
|
|
39
|
+
|
|
40
|
+
function routeOptions<T extends RouteImpl>(routeImpl: T, impl: string, path: string): T['options'] {
|
|
41
|
+
if (!isRouteImpl(routeImpl)) {
|
|
42
|
+
throw new TypeError(\`Route implementation "\${impl}" for "\${path}" must export default defineRoute(...).\`);
|
|
43
|
+
}
|
|
44
|
+
return routeImpl.options;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const skeleton = buildAnchorSkeleton({
|
|
48
|
+
navigation: ${indentedLiteral(navigation, 2)},
|
|
49
|
+
settingsSections: ${indentedLiteral(settingsSections, 2)},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
${routeDeclarations}
|
|
53
|
+
|
|
54
|
+
const projectLayout = skeleton.projectLayout.addChildren([${projectLayoutRoutes}]);
|
|
55
|
+
const workspaceSettings = skeleton.workspaceSettings.addChildren([${workspaceSettingsRoutes}]);
|
|
56
|
+
const workspaceLayout = skeleton.workspaceLayout.addChildren([
|
|
57
|
+
${workspaceLayoutRoutes}${workspaceLayoutRoutes ? ',' : ''}
|
|
58
|
+
projectLayout,
|
|
59
|
+
workspaceSettings,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
export const routeTree = skeleton.rootRoute.addChildren([
|
|
63
|
+
${rootChildren},
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
export const router = createRouter({
|
|
67
|
+
routeTree,
|
|
68
|
+
context: {auth: undefined, queryClient: undefined} satisfies RouterContext,
|
|
69
|
+
scrollRestoration: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
declare module '@tanstack/react-router' {
|
|
73
|
+
interface Register {
|
|
74
|
+
router: typeof router;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/vite/index.ts"],"sourcesContent":["export {shipfoxClientComposition} from './plugin.js';\n"],"names":["shipfoxClientComposition"],"mappings":"AAAA,SAAQA,wBAAwB,QAAO,cAAc"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export interface ShipfoxClientCompositionOptions {
|
|
3
|
+
features: string;
|
|
4
|
+
out?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function shipfoxClientComposition({ features, out, }: ShipfoxClientCompositionOptions): Plugin;
|
|
7
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { composeRoutes } from '#compose/compose-routes.js';
|
|
4
|
+
import { mergeConfigShapes } from '#compose/merge-config.js';
|
|
5
|
+
import { validateProviderIds } from '#compose/validate-providers.js';
|
|
6
|
+
import { validateNavigation, validateSettingsSections } from '#compose/validate-registries.js';
|
|
7
|
+
import { navigationEntries, settingsEntries } from '#runtime/registries.js';
|
|
8
|
+
import { evaluateFeatures, invalidateFeatures } from './evaluate-features.js';
|
|
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
|
+
export function shipfoxClientComposition({ features, out = './src/shipfox-app.gen.ts' }) {
|
|
15
|
+
let config;
|
|
16
|
+
let watchedFiles = new Set();
|
|
17
|
+
const outputPath = ()=>resolve(config?.root ?? process.cwd(), out);
|
|
18
|
+
const featuresPath = ()=>resolve(config?.root ?? process.cwd(), features);
|
|
19
|
+
async function assertDefaultRouteExports(resolveRoute, routes) {
|
|
20
|
+
for (const route of routes){
|
|
21
|
+
const resolvedRoute = await resolveRoute(route.impl, outputPath());
|
|
22
|
+
if (!resolvedRoute) {
|
|
23
|
+
throw new Error(`Could not resolve route implementation "${route.impl}" for "${route.path}".`);
|
|
24
|
+
}
|
|
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function generate({ addWatchFile, resolveRoute }) {
|
|
32
|
+
invalidateFeatures(watchedFiles);
|
|
33
|
+
const evaluated = await evaluateFeatures(featuresPath());
|
|
34
|
+
const routes = composeRoutes(evaluated.features);
|
|
35
|
+
validateProviderIds(evaluated.features);
|
|
36
|
+
validateNavigation(evaluated.features, routes.map((route)=>route.path));
|
|
37
|
+
validateSettingsSections(evaluated.features, routes.map((route)=>route.path));
|
|
38
|
+
mergeConfigShapes(evaluated.features);
|
|
39
|
+
await assertDefaultRouteExports(resolveRoute, routes);
|
|
40
|
+
watchedFiles = new Set(evaluated.loadedFiles);
|
|
41
|
+
for (const file of watchedFiles)addWatchFile(file);
|
|
42
|
+
const output = generateAppModule({
|
|
43
|
+
routes,
|
|
44
|
+
navigation: navigationEntries(evaluated.features),
|
|
45
|
+
settingsSections: settingsEntries(evaluated.features)
|
|
46
|
+
});
|
|
47
|
+
const path = outputPath();
|
|
48
|
+
const existing = await readFile(path, 'utf8').catch(()=>undefined);
|
|
49
|
+
if (existing === output) return;
|
|
50
|
+
await mkdir(dirname(path), {
|
|
51
|
+
recursive: true
|
|
52
|
+
});
|
|
53
|
+
await writeFile(path, output);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
name: 'shipfox-client-composition',
|
|
57
|
+
configResolved (resolvedConfig) {
|
|
58
|
+
config = resolvedConfig;
|
|
59
|
+
},
|
|
60
|
+
async buildStart () {
|
|
61
|
+
await generate({
|
|
62
|
+
addWatchFile: this.addWatchFile.bind(this),
|
|
63
|
+
resolveRoute: this.resolve.bind(this)
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
async hotUpdate ({ file, server }) {
|
|
67
|
+
const resolvedFile = await realpath(file).catch(()=>resolve(file));
|
|
68
|
+
if (!watchedFiles.has(resolvedFile)) return;
|
|
69
|
+
await generate({
|
|
70
|
+
addWatchFile: server.watcher.add.bind(server.watcher),
|
|
71
|
+
resolveRoute: this.environment.pluginContainer.resolveId.bind(this.environment.pluginContainer)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shipfox/client-shell",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
8
|
+
"directory": "libs/client/shell"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"imports": {
|
|
14
|
+
"#*": {
|
|
15
|
+
"workspace-source": "./src/*",
|
|
16
|
+
"development": "./src/*",
|
|
17
|
+
"default": "./dist/*"
|
|
18
|
+
},
|
|
19
|
+
"#test/*": "./test/*"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
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
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./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
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./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
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"./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
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@swc/helpers": "^0.5.17",
|
|
65
|
+
"@tanstack/router-core": "^1.171.13",
|
|
66
|
+
"jiti": "^2.7.0",
|
|
67
|
+
"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"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@tanstack/react-query": "^5.101.0",
|
|
76
|
+
"@tanstack/react-router": "^1.170.16",
|
|
77
|
+
"jotai": "^2.19.1",
|
|
78
|
+
"react": "^19.0.0",
|
|
79
|
+
"react-dom": "^19.0.0",
|
|
80
|
+
"vite": "^8.0.3",
|
|
81
|
+
"zod": "^4.4.3"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@storybook/react": "^10.0.0",
|
|
85
|
+
"@tanstack/react-query": "^5.101.0",
|
|
86
|
+
"@tanstack/react-router": "^1.170.16",
|
|
87
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
88
|
+
"@testing-library/react": "^16.3.2",
|
|
89
|
+
"@types/react": "^19.1.11",
|
|
90
|
+
"@types/react-dom": "^19.1.7",
|
|
91
|
+
"jotai": "^2.19.1",
|
|
92
|
+
"jsdom": "^29.0.0",
|
|
93
|
+
"react": "^19.1.1",
|
|
94
|
+
"react-dom": "^19.1.1",
|
|
95
|
+
"vite": "^8.0.3",
|
|
96
|
+
"zod": "^4.4.3",
|
|
97
|
+
"@shipfox/biome": "1.8.1",
|
|
98
|
+
"@shipfox/client-test-setup": "0.0.2",
|
|
99
|
+
"@shipfox/typescript": "1.1.6",
|
|
100
|
+
"@shipfox/ts-config": "1.3.8",
|
|
101
|
+
"@shipfox/vitest": "1.2.2",
|
|
102
|
+
"@shipfox/swc": "1.2.5"
|
|
103
|
+
},
|
|
104
|
+
"scripts": {
|
|
105
|
+
"build": "shipfox-swc",
|
|
106
|
+
"check": "shipfox-biome-check",
|
|
107
|
+
"check:fix": "shipfox-biome-check --write",
|
|
108
|
+
"test": "shipfox-vitest-run",
|
|
109
|
+
"test:watch": "shipfox-vitest-watch",
|
|
110
|
+
"type": "shipfox-tsc-check",
|
|
111
|
+
"type:emit": "shipfox-tsc-emit"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
2
|
+
import {Outlet, useMatches, useParams} from '@tanstack/react-router';
|
|
3
|
+
import type {NavTabEntry} from '#contract.js';
|
|
4
|
+
import {useMaybeActiveWorkspace} from '#runtime/active-workspace.js';
|
|
5
|
+
import {NavBar} from './nav-bar.js';
|
|
6
|
+
import {NavTabs} from './nav-tabs.js';
|
|
7
|
+
|
|
8
|
+
declare module '@tanstack/react-router' {
|
|
9
|
+
interface StaticDataRouteOption {
|
|
10
|
+
layout?: 'full-bleed';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MainLayout({
|
|
15
|
+
navigation,
|
|
16
|
+
hideProjectNavigation = false,
|
|
17
|
+
}: {
|
|
18
|
+
navigation: readonly NavTabEntry[];
|
|
19
|
+
hideProjectNavigation?: boolean;
|
|
20
|
+
}) {
|
|
21
|
+
const workspace = useMaybeActiveWorkspace();
|
|
22
|
+
const {pid} = useParams({strict: false}) as {pid?: string};
|
|
23
|
+
const matches = useMatches();
|
|
24
|
+
if (!workspace) return <FullPageLoader />;
|
|
25
|
+
const fullBleed = matches.some((match) => match.staticData.layout === 'full-bleed');
|
|
26
|
+
return (
|
|
27
|
+
<div className="h-screen w-full flex flex-col bg-background-subtle-base">
|
|
28
|
+
<NavBar hideProjectNavigation={hideProjectNavigation} />
|
|
29
|
+
{hideProjectNavigation ? undefined : (
|
|
30
|
+
<NavTabs entries={navigation} scope={pid ? 'project' : 'workspace'} />
|
|
31
|
+
)}
|
|
32
|
+
{fullBleed ? (
|
|
33
|
+
<main className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
34
|
+
<Outlet />
|
|
35
|
+
</main>
|
|
36
|
+
) : (
|
|
37
|
+
<main className="flex-1 overflow-auto">
|
|
38
|
+
<div className="max-w-[1120px] mx-auto px-24 py-32">
|
|
39
|
+
<Outlet />
|
|
40
|
+
</div>
|
|
41
|
+
</main>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {Logo} from '@shipfox/react-ui/logo';
|
|
2
|
+
import {Link} from '@tanstack/react-router';
|
|
3
|
+
import {useActiveWorkspace} from '#runtime/active-workspace.js';
|
|
4
|
+
import {useChrome} from '#runtime/chrome-context.js';
|
|
5
|
+
import {UserMenu} from './user-menu.js';
|
|
6
|
+
import {WorkspaceCrumb} from './workspace-crumb.js';
|
|
7
|
+
|
|
8
|
+
export function NavBar({hideProjectNavigation = false}: {hideProjectNavigation?: boolean}) {
|
|
9
|
+
const workspace = useActiveWorkspace();
|
|
10
|
+
const {ProjectBreadcrumb} = useChrome();
|
|
11
|
+
return (
|
|
12
|
+
<header className="sticky top-0 z-30 h-56 px-16 flex items-center gap-12 bg-background-subtle-base border-b border-border-neutral-base shrink-0">
|
|
13
|
+
<Link
|
|
14
|
+
to="/"
|
|
15
|
+
aria-label="Shipfox home"
|
|
16
|
+
className="rounded-6 focus-visible:outline-none focus-visible:shadow-button-neutral-focus"
|
|
17
|
+
>
|
|
18
|
+
{hideProjectNavigation ? (
|
|
19
|
+
<>
|
|
20
|
+
<Logo variant="mark" alt="" className="sm:hidden" />
|
|
21
|
+
<Logo variant="wordmark" alt="" className="hidden sm:block" />
|
|
22
|
+
</>
|
|
23
|
+
) : (
|
|
24
|
+
<Logo variant="wordmark" alt="" />
|
|
25
|
+
)}
|
|
26
|
+
</Link>
|
|
27
|
+
<span className="h-20 w-px bg-border-neutral-base" aria-hidden="true" />
|
|
28
|
+
<WorkspaceCrumb workspace={workspace} compact={hideProjectNavigation} />
|
|
29
|
+
{hideProjectNavigation ? undefined : (
|
|
30
|
+
<>
|
|
31
|
+
<span className="text-foreground-neutral-muted" aria-hidden="true">
|
|
32
|
+
/
|
|
33
|
+
</span>
|
|
34
|
+
<ProjectBreadcrumb />
|
|
35
|
+
</>
|
|
36
|
+
)}
|
|
37
|
+
<div className="flex-1" />
|
|
38
|
+
<UserMenu />
|
|
39
|
+
</header>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMemoryHistory,
|
|
3
|
+
createRootRoute,
|
|
4
|
+
createRoute,
|
|
5
|
+
createRouter,
|
|
6
|
+
Outlet,
|
|
7
|
+
RouterProvider,
|
|
8
|
+
} from '@tanstack/react-router';
|
|
9
|
+
import {render, screen} from '@testing-library/react';
|
|
10
|
+
import type {NavTabEntry} from '#contract.js';
|
|
11
|
+
import {NavTabs} from './nav-tabs.js';
|
|
12
|
+
|
|
13
|
+
const entries: readonly NavTabEntry[] = [
|
|
14
|
+
{
|
|
15
|
+
id: 'projects',
|
|
16
|
+
scope: 'workspace',
|
|
17
|
+
label: 'Projects',
|
|
18
|
+
to: '/workspaces/$wid/projects',
|
|
19
|
+
exact: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'settings',
|
|
23
|
+
scope: 'workspace',
|
|
24
|
+
label: 'Settings',
|
|
25
|
+
to: '/workspaces/$wid/settings',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function WorkspaceTabs() {
|
|
30
|
+
return <NavTabs entries={entries} scope="workspace" />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('NavTabs', () => {
|
|
34
|
+
test('distinguishes active and inactive workspace tabs', async () => {
|
|
35
|
+
const rootRoute = createRootRoute({component: Outlet});
|
|
36
|
+
const projectsRoute = createRoute({
|
|
37
|
+
getParentRoute: () => rootRoute,
|
|
38
|
+
path: '/workspaces/$wid/projects',
|
|
39
|
+
component: WorkspaceTabs,
|
|
40
|
+
});
|
|
41
|
+
const settingsRoute = createRoute({
|
|
42
|
+
getParentRoute: () => rootRoute,
|
|
43
|
+
path: '/workspaces/$wid/settings',
|
|
44
|
+
component: WorkspaceTabs,
|
|
45
|
+
});
|
|
46
|
+
const router = createRouter({
|
|
47
|
+
history: createMemoryHistory({initialEntries: ['/workspaces/workspace/settings']}),
|
|
48
|
+
routeTree: rootRoute.addChildren([projectsRoute, settingsRoute]),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
render(<RouterProvider router={router} />);
|
|
52
|
+
|
|
53
|
+
const projectsTab = await screen.findByRole('tab', {name: 'Projects'});
|
|
54
|
+
const settingsTab = screen.getByRole('tab', {name: 'Settings'});
|
|
55
|
+
expect(projectsTab).toHaveClass('text-foreground-neutral-muted');
|
|
56
|
+
expect(projectsTab).toHaveAttribute('aria-selected', 'false');
|
|
57
|
+
expect(settingsTab).toHaveClass('text-foreground-neutral-base');
|
|
58
|
+
expect(settingsTab).toHaveAttribute('aria-selected', 'true');
|
|
59
|
+
});
|
|
60
|
+
});
|