@shipfox/client-shell 0.2.0 → 2.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +33 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/dist/vite/evaluate-features.d.ts +0 -1
- package/dist/vite/evaluate-features.d.ts.map +1 -1
- package/dist/vite/evaluate-features.js +60 -47
- package/dist/vite/evaluate-features.js.map +1 -1
- package/dist/vite/generate.js +2 -2
- package/dist/vite/generate.js.map +1 -1
- package/dist/vite/plugin.d.ts.map +1 -1
- package/dist/vite/plugin.js +3 -12
- package/dist/vite/plugin.js.map +1 -1
- package/package.json +28 -49
- package/src/vite/evaluate-features.test.ts +58 -3
- package/src/vite/evaluate-features.ts +71 -50
- package/src/vite/generate.ts +2 -2
- package/src/vite/plugin.test.ts +59 -9
- package/src/vite/plugin.ts +3 -16
- package/test/external/FINDINGS.md +33 -45
- package/test/external/README.md +21 -23
- package/test/external/fixture/package.json +1 -2
- package/test/external/fixture/src/app.fixture.tsx +78 -46
- package/test/external/fixture/src/features/external-settings.tsx +19 -0
- package/test/external/fixture/src/features/login-override.tsx +9 -0
- package/test/external/fixture/src/features.collision.ts +5 -5
- package/test/external/fixture/src/features.ts +49 -3
- package/test/external/fixture/src/link-typecheck.tsx +5 -5
- package/test/external/fixture/src/main.tsx +83 -1
- package/test/external/fixture/src/provider.ts +45 -0
- package/test/external/fixture/tsconfig.json +1 -1
- package/test/external/fixture/vite.config.ts +13 -2
- package/test/external/fixture/vitest.config.ts +3 -1
- package/test/external/verify.mjs +217 -47
- package/test/typecheck/shipfox-app.gen.ts +6 -6
- package/test/typecheck/types.tsx +6 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/test/external/fixture/src/features/override-impl.tsx +0 -12
- package/test/external/fixture/src/features/override.tsx +0 -20
- package/test/external/toy-feature/CHANGELOG.md +0 -15
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-check +0 -43
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-format +0 -43
- package/test/external/toy-feature/node_modules/.bin/shipfox-biome-lint +0 -43
- package/test/external/toy-feature/node_modules/.bin/shipfox-swc +0 -43
- package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-check +0 -43
- package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-emit +0 -43
- package/test/external/toy-feature/package.json +0 -57
- package/test/external/toy-feature/src/config.ts +0 -5
- package/test/external/toy-feature/src/index.ts +0 -64
- package/test/external/toy-feature/src/provider.tsx +0 -33
- package/test/external/toy-feature/src/routes/insights.tsx +0 -7
- package/test/external/toy-feature/src/routes/settings.tsx +0 -7
- package/test/external/toy-feature/tsconfig.build.json +0 -5
- package/test/external/toy-feature/tsconfig.json +0 -1
- 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;
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
38
|
+
async function localFiles(paths) {
|
|
25
39
|
const files = new Set();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
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 {
|
|
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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"}
|
package/dist/vite/generate.js
CHANGED
|
@@ -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}
|
|
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}
|
|
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}
|
|
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;
|
|
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"}
|
package/dist/vite/plugin.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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({
|
package/dist/vite/plugin.js.map
CHANGED
|
@@ -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
|
|
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": "
|
|
4
|
+
"version": "2.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
|
-
"
|
|
24
|
-
|
|
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
|
-
"
|
|
34
|
-
|
|
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
|
-
"
|
|
44
|
-
|
|
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
|
-
"
|
|
54
|
-
|
|
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
|
-
"
|
|
38
|
+
"tsx": "^4.22.4",
|
|
67
39
|
"framer-motion": "^12.23.24",
|
|
68
|
-
"@shipfox/api-auth-dto": "
|
|
69
|
-
"@shipfox/client-
|
|
70
|
-
"@shipfox/
|
|
71
|
-
"@shipfox/
|
|
72
|
-
"@shipfox/react-ui": "0.3.
|
|
40
|
+
"@shipfox/api-auth-dto": "6.0.0",
|
|
41
|
+
"@shipfox/client-api": "1.0.0",
|
|
42
|
+
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
43
|
+
"@shipfox/client-config": "2.0.0",
|
|
44
|
+
"@shipfox/react-ui": "0.3.3"
|
|
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.
|
|
98
|
-
"@shipfox/client-test-setup": "0.0.
|
|
99
|
-
"@shipfox/
|
|
75
|
+
"@shipfox/biome": "1.8.2",
|
|
76
|
+
"@shipfox/client-test-setup": "0.0.3",
|
|
77
|
+
"@shipfox/swc": "1.2.6",
|
|
100
78
|
"@shipfox/ts-config": "1.3.8",
|
|
101
|
-
"@shipfox/
|
|
102
|
-
"@shipfox/
|
|
79
|
+
"@shipfox/typescript": "1.1.7",
|
|
80
|
+
"@shipfox/vitest": "1.2.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 {
|
|
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('
|
|
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
|
-
|
|
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
|
});
|