@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
package/test/external/verify.mjs
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
2
|
import {globSync} from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cp,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
realpath,
|
|
10
|
+
rm,
|
|
11
|
+
symlink,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from 'node:fs/promises';
|
|
4
14
|
import {tmpdir} from 'node:os';
|
|
5
|
-
import {basename, join, relative, resolve, sep} from 'node:path';
|
|
15
|
+
import {basename, dirname, join, relative, resolve, sep} from 'node:path';
|
|
6
16
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
7
17
|
|
|
8
18
|
const repositoryRoot = resolve(fileURLToPath(new URL('../../../../..', import.meta.url)));
|
|
9
19
|
const externalRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
10
20
|
const fixtureTemplate = join(externalRoot, 'fixture');
|
|
11
21
|
const REGISTRY_SHIPFOX_PACKAGE_PATTERN = /^@shipfox\+[^@]+@\d/u;
|
|
12
|
-
const
|
|
22
|
+
const EXPECTED_COLLISION_DIAGNOSTIC =
|
|
23
|
+
'Route "/auth/login" is contributed by both features "shipfox.auth" and "fixture.unapproved-collision". Set override: true to replace it explicitly.';
|
|
24
|
+
const arguments_ = process.argv.slice(2).filter((argument) => argument !== '--');
|
|
25
|
+
const linkMode = arguments_.includes('--link');
|
|
13
26
|
|
|
14
|
-
if (
|
|
27
|
+
if (arguments_.some((argument) => argument.startsWith('--') && argument !== '--link')) {
|
|
15
28
|
throw new Error('Usage: node verify.mjs [--link]');
|
|
16
29
|
}
|
|
17
30
|
|
|
@@ -26,18 +39,29 @@ const {
|
|
|
26
39
|
} = await import(
|
|
27
40
|
pathToFileURL(join(repositoryRoot, 'tools/application-release/dist/package-closure.js')).href
|
|
28
41
|
);
|
|
42
|
+
const {productionizeManifest} = await import(
|
|
43
|
+
pathToFileURL(join(repositoryRoot, 'tools/utils/src/productionize.js')).href
|
|
44
|
+
);
|
|
29
45
|
const config = readPublicationClosureConfig(join(repositoryRoot, 'publication-closure.json'));
|
|
30
46
|
const workspacePackages = readWorkspacePackages(repositoryRoot);
|
|
31
47
|
validatePublicationState(workspacePackages, config, repositoryRoot);
|
|
32
48
|
const clientRoots = config.roots.filter((root) => root.startsWith('@shipfox/client-'));
|
|
49
|
+
const fixtureSupportPackages = ['@shipfox/client-config'];
|
|
50
|
+
const consumerPackages = [...clientRoots, ...fixtureSupportPackages];
|
|
33
51
|
const closure = computePublicationClosure(workspacePackages, clientRoots);
|
|
34
52
|
const entryPoints = closure.flatMap((name) =>
|
|
35
53
|
listConcreteEntryPoints(name, requiredPackage(workspacePackages, name)),
|
|
36
54
|
);
|
|
55
|
+
const consumerTypeEntryPoints = clientRoots.flatMap((name) =>
|
|
56
|
+
listConcreteEntryPoints(name, requiredPackage(workspacePackages, name)),
|
|
57
|
+
);
|
|
58
|
+
const closureTypeEntryPoints = entryPoints
|
|
59
|
+
.filter(({target}) => entryPointSupportsTypeResolution(target))
|
|
60
|
+
.map(({specifier}) => specifier);
|
|
37
61
|
const runtimeEntryPoints = entryPoints
|
|
38
62
|
.filter(({target}) => entryPointSupportsRuntimeImport(target))
|
|
39
63
|
.map(({specifier}) => specifier);
|
|
40
|
-
const typeEntryPoints =
|
|
64
|
+
const typeEntryPoints = consumerTypeEntryPoints
|
|
41
65
|
.filter(({target}) => entryPointSupportsTypeResolution(target))
|
|
42
66
|
.map(({specifier}) => specifier);
|
|
43
67
|
const temporaryRoot = await mkdtemp(join(tmpdir(), 'shipfox-client-composition-'));
|
|
@@ -48,23 +72,51 @@ try {
|
|
|
48
72
|
const packageSpecs = linkMode
|
|
49
73
|
? linkedPackageSpecs(closure, workspacePackages)
|
|
50
74
|
: await packedPackageSpecs(closure, workspacePackages, temporaryRoot);
|
|
51
|
-
await configureFixture(fixtureRoot, packageSpecs);
|
|
75
|
+
await configureFixture(fixtureRoot, consumerPackages, packageSpecs);
|
|
52
76
|
await writeTypeFixture(fixtureRoot, typeEntryPoints);
|
|
53
|
-
await run('pnpm', ['install', '--prefer-offline', '--ignore-scripts'], fixtureRoot
|
|
77
|
+
await run('pnpm', ['install', '--prefer-offline', '--ignore-scripts'], fixtureRoot, {
|
|
78
|
+
capture: true,
|
|
79
|
+
});
|
|
54
80
|
|
|
55
|
-
if (linkMode) await validateLinkedPackages(fixtureRoot,
|
|
81
|
+
if (linkMode) await validateLinkedPackages(fixtureRoot, consumerPackages, workspacePackages);
|
|
56
82
|
else {
|
|
57
83
|
await validateInstalledPackages(fixtureRoot, closure, workspacePackages);
|
|
58
84
|
await validateNoRegistryShipfoxPackages(fixtureRoot);
|
|
59
|
-
await
|
|
85
|
+
await validatePackageResolution(fixtureRoot, runtimeEntryPoints, {
|
|
86
|
+
conditionLabel: 'default',
|
|
87
|
+
});
|
|
88
|
+
await validatePackageResolution(fixtureRoot, runtimeEntryPoints, {
|
|
89
|
+
nodeArgs: ['--conditions=development'],
|
|
90
|
+
conditionLabel: 'development',
|
|
91
|
+
});
|
|
92
|
+
await validateFullClosureTypeDeclarations(fixtureRoot, closureTypeEntryPoints);
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
await run('pnpm', ['exec', 'vite', 'build'], fixtureRoot);
|
|
63
|
-
await
|
|
95
|
+
await run('pnpm', ['exec', 'vite', 'build'], fixtureRoot, {capture: true});
|
|
96
|
+
await validateGeneratedModule(fixtureRoot);
|
|
97
|
+
await run('pnpm', ['exec', 'vitest', 'run', '--mode', 'production'], fixtureRoot, {
|
|
98
|
+
capture: true,
|
|
99
|
+
env: {NODE_ENV: 'production'},
|
|
100
|
+
});
|
|
101
|
+
await run('pnpm', ['exec', 'tsc', '--noEmit'], fixtureRoot, {capture: true});
|
|
102
|
+
|
|
103
|
+
const collision = await run('pnpm', ['exec', 'vite', 'build'], fixtureRoot, {
|
|
104
|
+
capture: true,
|
|
105
|
+
allowFailure: true,
|
|
106
|
+
env: {SHIPFOX_COMPOSITION_COLLISION: '1'},
|
|
107
|
+
});
|
|
108
|
+
if (collision.code === 0) throw new Error('Collision fixture unexpectedly built successfully.');
|
|
109
|
+
const collisionOutput = `${collision.stdout}\n${collision.stderr}`;
|
|
110
|
+
if (!collisionOutput.includes(EXPECTED_COLLISION_DIAGNOSTIC)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Collision output did not include the contract diagnostic:\n${EXPECTED_COLLISION_DIAGNOSTIC}\n\n${collisionOutput}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
64
115
|
|
|
65
116
|
process.stdout.write(
|
|
66
|
-
`Verified ${closure.length}
|
|
117
|
+
`Verified the default client composition, ${closure.length} runtime packages, and ${entryPoints.length} public entry points in ${linkMode ? 'linked' : 'packed-tarball'} mode.\n`,
|
|
67
118
|
);
|
|
119
|
+
process.stdout.write(`Collision: ${EXPECTED_COLLISION_DIAGNOSTIC}\n`);
|
|
68
120
|
} finally {
|
|
69
121
|
await rm(temporaryRoot, {recursive: true, force: true});
|
|
70
122
|
}
|
|
@@ -80,20 +132,42 @@ function linkedPackageSpecs(names, packages) {
|
|
|
80
132
|
|
|
81
133
|
async function packedPackageSpecs(names, packages, root) {
|
|
82
134
|
const tarballRoot = join(root, 'tarballs');
|
|
83
|
-
|
|
135
|
+
const stagingRoot = join(root, 'packages');
|
|
136
|
+
await Promise.all([
|
|
137
|
+
mkdir(tarballRoot),
|
|
138
|
+
mkdir(stagingRoot),
|
|
139
|
+
cp(join(repositoryRoot, 'pnpm-workspace.yaml'), join(stagingRoot, 'pnpm-workspace.yaml')),
|
|
140
|
+
]);
|
|
84
141
|
const tarballs = await mapWithConcurrency(names, 4, async (name) => {
|
|
85
142
|
const workspacePackage = requiredPackage(packages, name);
|
|
86
143
|
const tarball = join(tarballRoot, `${safePackageName(name)}.tgz`);
|
|
87
|
-
await
|
|
144
|
+
await packProductionizedPackage(workspacePackage, tarball, stagingRoot);
|
|
88
145
|
return [name, `file:${tarball}`];
|
|
89
146
|
});
|
|
90
147
|
return Object.fromEntries(tarballs);
|
|
91
148
|
}
|
|
92
149
|
|
|
93
|
-
async function
|
|
150
|
+
async function packProductionizedPackage(workspacePackage, tarball, stagingRoot) {
|
|
151
|
+
const packageRoot = join(stagingRoot, safePackageName(workspacePackage.manifest.name));
|
|
152
|
+
await cp(workspacePackage.directory, packageRoot, {recursive: true});
|
|
153
|
+
|
|
154
|
+
const manifestPath = join(packageRoot, 'package.json');
|
|
155
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
156
|
+
const productionized = productionizeManifest(manifest);
|
|
157
|
+
if (productionized !== manifest) {
|
|
158
|
+
await writeFile(manifestPath, `${JSON.stringify(productionized, null, 2)}\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await run('pnpm', ['pack', '--out', tarball], packageRoot, {capture: true});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function configureFixture(root, consumerPackages, packageSpecs) {
|
|
94
165
|
const manifestPath = join(root, 'package.json');
|
|
95
166
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
96
|
-
|
|
167
|
+
const consumerDependencies = Object.fromEntries(
|
|
168
|
+
consumerPackages.map((name) => [name, packageSpecs[name]]),
|
|
169
|
+
);
|
|
170
|
+
manifest.dependencies = {...manifest.dependencies, ...consumerDependencies};
|
|
97
171
|
await Promise.all([
|
|
98
172
|
writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`),
|
|
99
173
|
writeFile(
|
|
@@ -131,21 +205,24 @@ async function validateLinkedPackages(root, names, packages) {
|
|
|
131
205
|
async function validateInstalledPackages(root, names, packages) {
|
|
132
206
|
const realFixtureRoot = await realpath(root);
|
|
133
207
|
for (const name of names) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (installedManifest.version !== expectedManifest.version) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Packed ${name} has version ${installedManifest.version}; expected ${expectedManifest.version}`,
|
|
208
|
+
for (const packageRoot of installedPackageRoots(root, name)) {
|
|
209
|
+
const installedManifest = JSON.parse(
|
|
210
|
+
await readFile(join(packageRoot, 'package.json'), 'utf8'),
|
|
140
211
|
);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
212
|
+
const expectedManifest = requiredPackage(packages, name).manifest;
|
|
213
|
+
if (installedManifest.version !== expectedManifest.version) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Packed ${name} has version ${installedManifest.version}; expected ${expectedManifest.version}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const workspaceRange = findWorkspaceRange(installedManifest);
|
|
219
|
+
if (workspaceRange)
|
|
220
|
+
throw new Error(`Packed ${name} contains a workspace range at ${workspaceRange}`);
|
|
221
|
+
validateDefaultInternalImports(name, installedManifest);
|
|
222
|
+
const installedRoot = await realpath(packageRoot);
|
|
223
|
+
if (!installedRoot.startsWith(realFixtureRoot)) {
|
|
224
|
+
throw new Error(`Packed ${name} resolved outside the external consumer`);
|
|
225
|
+
}
|
|
149
226
|
}
|
|
150
227
|
}
|
|
151
228
|
}
|
|
@@ -153,6 +230,7 @@ async function validateInstalledPackages(root, names, packages) {
|
|
|
153
230
|
function validateDefaultInternalImports(name, manifest) {
|
|
154
231
|
const internalImports = manifest.imports?.['#*'];
|
|
155
232
|
if (internalImports === undefined) return;
|
|
233
|
+
if (internalImports === './dist/*') return;
|
|
156
234
|
if (
|
|
157
235
|
!internalImports ||
|
|
158
236
|
typeof internalImports !== 'object' ||
|
|
@@ -163,26 +241,96 @@ function validateDefaultInternalImports(name, manifest) {
|
|
|
163
241
|
}
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
async function
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
244
|
+
async function validatePackageResolution(root, specifiers, {nodeArgs = [], conditionLabel}) {
|
|
245
|
+
const specifiersByPackage = Map.groupBy(specifiers, packageNameFromSpecifier);
|
|
246
|
+
for (const [packageName, packageSpecifiers] of specifiersByPackage) {
|
|
247
|
+
for (const packageRoot of installedPackageRoots(root, packageName)) {
|
|
248
|
+
const resolution = await run(
|
|
249
|
+
process.execPath,
|
|
250
|
+
[
|
|
251
|
+
...nodeArgs,
|
|
252
|
+
'--input-type=module',
|
|
253
|
+
'--eval',
|
|
254
|
+
`const specifiers = ${JSON.stringify(packageSpecifiers)};
|
|
173
255
|
const resolved = Object.fromEntries(specifiers.map((specifier) => [specifier, import.meta.resolve(specifier)]));
|
|
174
256
|
process.stdout.write(JSON.stringify(resolved));`,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
257
|
+
],
|
|
258
|
+
packageRoot,
|
|
259
|
+
{capture: true},
|
|
260
|
+
);
|
|
261
|
+
const resolved = JSON.parse(resolution.stdout);
|
|
262
|
+
for (const specifier of packageSpecifiers) {
|
|
263
|
+
const resolvedPath = fileURLToPath(resolved[specifier]);
|
|
264
|
+
if (!resolvedPath.split(sep).includes('dist')) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Packed ${specifier} resolved to source instead of dist under the ${conditionLabel} condition: ${resolvedPath}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function validateFullClosureTypeDeclarations(root, specifiers) {
|
|
275
|
+
const typeScriptCli = join(root, 'node_modules', 'typescript', 'bin', 'tsc');
|
|
276
|
+
const specifiersByPackage = Map.groupBy(specifiers, packageNameFromSpecifier);
|
|
277
|
+
const auditRoot = join(root, 'full-closure-types');
|
|
278
|
+
const typeFixtures = [];
|
|
279
|
+
let auditIndex = 0;
|
|
280
|
+
for (const [packageName, packageSpecifiers] of specifiersByPackage) {
|
|
281
|
+
for (const packageRoot of installedPackageRoots(root, packageName)) {
|
|
282
|
+
const packageAuditRoot = join(auditRoot, String(auditIndex));
|
|
283
|
+
auditIndex += 1;
|
|
284
|
+
await writeTypeAudit(packageAuditRoot, packageName, packageRoot, packageSpecifiers);
|
|
285
|
+
typeFixtures.push(relative(auditRoot, join(packageAuditRoot, 'types.ts')));
|
|
184
286
|
}
|
|
185
287
|
}
|
|
288
|
+
|
|
289
|
+
await writeTypeAuditConfig(auditRoot, typeFixtures);
|
|
290
|
+
await run(process.execPath, [typeScriptCli, '--project', 'tsconfig.json'], auditRoot, {
|
|
291
|
+
capture: true,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function writeTypeAudit(root, packageName, packageRoot, specifiers) {
|
|
296
|
+
const packageLink = join(root, 'node_modules', ...packageName.split('/'));
|
|
297
|
+
await mkdir(dirname(packageLink), {recursive: true});
|
|
298
|
+
await symlink(packageRoot, packageLink, 'dir');
|
|
299
|
+
await writeTypeFixture(root, specifiers);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function writeTypeAuditConfig(root, typeFixtures) {
|
|
303
|
+
await writeFile(
|
|
304
|
+
join(root, 'tsconfig.json'),
|
|
305
|
+
`${JSON.stringify(
|
|
306
|
+
{
|
|
307
|
+
compilerOptions: {
|
|
308
|
+
strict: true,
|
|
309
|
+
target: 'ES2022',
|
|
310
|
+
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
311
|
+
module: 'ESNext',
|
|
312
|
+
moduleResolution: 'bundler',
|
|
313
|
+
jsx: 'react-jsx',
|
|
314
|
+
skipLibCheck: false,
|
|
315
|
+
verbatimModuleSyntax: true,
|
|
316
|
+
noEmit: true,
|
|
317
|
+
},
|
|
318
|
+
files: typeFixtures,
|
|
319
|
+
},
|
|
320
|
+
null,
|
|
321
|
+
2,
|
|
322
|
+
)}\n`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function installedPackageRoots(root, packageName) {
|
|
327
|
+
const packageRoots = globSync(
|
|
328
|
+
join(root, 'node_modules/.pnpm/**/node_modules', packageName),
|
|
329
|
+
).sort();
|
|
330
|
+
if (!packageRoots.length) {
|
|
331
|
+
throw new Error(`Packed closure package is not installed: ${packageName}`);
|
|
332
|
+
}
|
|
333
|
+
return packageRoots;
|
|
186
334
|
}
|
|
187
335
|
|
|
188
336
|
async function validateNoRegistryShipfoxPackages(root) {
|
|
@@ -198,6 +346,28 @@ async function validateNoRegistryShipfoxPackages(root) {
|
|
|
198
346
|
}
|
|
199
347
|
}
|
|
200
348
|
|
|
349
|
+
async function validateGeneratedModule(root) {
|
|
350
|
+
const generated = await readFile(join(root, 'src/shipfox-app.gen.ts'), 'utf8');
|
|
351
|
+
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
352
|
+
const declaredDependencies = new Set(Object.keys(manifest.dependencies));
|
|
353
|
+
|
|
354
|
+
for (const match of generated.matchAll(/\bfrom\s+['"]([^'"]+)['"]/gu)) {
|
|
355
|
+
const specifier = match[1];
|
|
356
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) continue;
|
|
357
|
+
const packageName = packageNameFromSpecifier(specifier);
|
|
358
|
+
if (!declaredDependencies.has(packageName)) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Generated default composition imports undeclared package ${JSON.stringify(packageName)} from ${JSON.stringify(specifier)}.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function packageNameFromSpecifier(specifier) {
|
|
367
|
+
if (!specifier.startsWith('@')) return specifier.split('/')[0];
|
|
368
|
+
return specifier.split('/').slice(0, 2).join('/');
|
|
369
|
+
}
|
|
370
|
+
|
|
201
371
|
function requiredPackage(packages, name) {
|
|
202
372
|
const workspacePackage = packages.get(name);
|
|
203
373
|
if (!workspacePackage) throw new Error(`Missing workspace package: ${name}`);
|
|
@@ -303,7 +473,7 @@ function run(command, args, cwd, options = {}) {
|
|
|
303
473
|
stderr += chunk;
|
|
304
474
|
});
|
|
305
475
|
child.on('error', reject);
|
|
306
|
-
child.on('
|
|
476
|
+
child.on('close', (code) => {
|
|
307
477
|
const result = {code: code ?? 1, stdout, stderr};
|
|
308
478
|
if (code === 0 || allowFailure) resolvePromise(result);
|
|
309
479
|
else {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// biome-ignore-all assist/source/organizeImports: generated imports follow route order.
|
|
4
4
|
import {createRoute, createRouter} from '@tanstack/react-router';
|
|
5
5
|
import {buildAnchorSkeleton, isRouteImpl, type RouteImpl, type RouterContext} from '@shipfox/client-shell/runtime';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import * as route0Module from "#test/search-route-impl.js";
|
|
7
|
+
import * as route1Module from "#test/default-route-impl.js";
|
|
8
|
+
import * as route2Module from "#test/named-route-impl.js";
|
|
9
9
|
|
|
10
10
|
function routeOptions<T extends RouteImpl>(routeImpl: T, impl: string, path: string): T['options'] {
|
|
11
11
|
if (!isRouteImpl(routeImpl)) {
|
|
@@ -45,19 +45,19 @@ const skeleton = buildAnchorSkeleton({
|
|
|
45
45
|
const route0 = createRoute({
|
|
46
46
|
getParentRoute: () => skeleton.projectLayout,
|
|
47
47
|
path: "/overview",
|
|
48
|
-
...routeOptions(
|
|
48
|
+
...routeOptions(route0Module.default, "#test/search-route-impl.js", "/workspaces/$wid/projects/$pid/overview"),
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
const route1 = createRoute({
|
|
52
52
|
getParentRoute: () => skeleton.workspaceSettings,
|
|
53
53
|
path: "/members",
|
|
54
|
-
...routeOptions(
|
|
54
|
+
...routeOptions(route1Module.default, "#test/default-route-impl.js", "/workspaces/$wid/settings/members"),
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
const route2 = createRoute({
|
|
58
58
|
getParentRoute: () => skeleton.workspaceLayout,
|
|
59
59
|
path: "/insights",
|
|
60
|
-
...routeOptions(
|
|
60
|
+
...routeOptions(route2Module.default, "#test/named-route-impl.js", "/workspaces/$wid/insights"),
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const projectLayout = skeleton.projectLayout.addChildren([route0]);
|
package/test/typecheck/types.tsx
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {Link, useSearch} from '@tanstack/react-router';
|
|
1
|
+
import {getRouteApi, Link, useSearch} from '@tanstack/react-router';
|
|
2
2
|
import './shipfox-app.gen.js';
|
|
3
3
|
|
|
4
4
|
export function CompositionTypes(): React.ReactNode {
|
|
5
5
|
const search = useSearch({from: '/workspaces/$wid/projects/$pid/overview'});
|
|
6
6
|
const tab: 'activity' | 'overview' = search.tab;
|
|
7
7
|
|
|
8
|
+
getRouteApi('/workspaces/$wid/projects/$pid/overview');
|
|
9
|
+
|
|
10
|
+
// @ts-expect-error The generated route tree rejects unknown route ids.
|
|
11
|
+
getRouteApi('/not-a-route');
|
|
12
|
+
|
|
8
13
|
return (
|
|
9
14
|
<>
|
|
10
15
|
<Link to="/workspaces/$wid/insights" params={{wid: 'workspace'}}>
|