@shipfox/client-shell 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/tsconfig.test.tsbuildinfo +1 -1
  4. package/dist/vite/evaluate-features.d.ts +0 -1
  5. package/dist/vite/evaluate-features.d.ts.map +1 -1
  6. package/dist/vite/evaluate-features.js +60 -47
  7. package/dist/vite/evaluate-features.js.map +1 -1
  8. package/dist/vite/generate.js +2 -2
  9. package/dist/vite/generate.js.map +1 -1
  10. package/dist/vite/plugin.d.ts.map +1 -1
  11. package/dist/vite/plugin.js +3 -12
  12. package/dist/vite/plugin.js.map +1 -1
  13. package/package.json +28 -49
  14. package/src/vite/evaluate-features.test.ts +58 -3
  15. package/src/vite/evaluate-features.ts +71 -50
  16. package/src/vite/generate.ts +2 -2
  17. package/src/vite/plugin.test.ts +59 -9
  18. package/src/vite/plugin.ts +3 -16
  19. package/test/external/FINDINGS.md +33 -45
  20. package/test/external/README.md +21 -23
  21. package/test/external/fixture/package.json +1 -2
  22. package/test/external/fixture/src/app.fixture.tsx +78 -46
  23. package/test/external/fixture/src/features/external-settings.tsx +19 -0
  24. package/test/external/fixture/src/features/login-override.tsx +9 -0
  25. package/test/external/fixture/src/features.collision.ts +5 -5
  26. package/test/external/fixture/src/features.ts +49 -3
  27. package/test/external/fixture/src/link-typecheck.tsx +5 -5
  28. package/test/external/fixture/src/main.tsx +83 -1
  29. package/test/external/fixture/src/provider.ts +45 -0
  30. package/test/external/fixture/tsconfig.json +1 -1
  31. package/test/external/fixture/vite.config.ts +13 -2
  32. package/test/external/fixture/vitest.config.ts +3 -1
  33. package/test/external/verify.mjs +233 -47
  34. package/test/typecheck/shipfox-app.gen.ts +6 -6
  35. package/test/typecheck/types.tsx +6 -1
  36. package/tsconfig.build.tsbuildinfo +1 -1
  37. package/test/external/fixture/src/features/override-impl.tsx +0 -12
  38. package/test/external/fixture/src/features/override.tsx +0 -20
  39. package/test/external/toy-feature/CHANGELOG.md +0 -15
  40. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-check +0 -43
  41. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-format +0 -43
  42. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-lint +0 -43
  43. package/test/external/toy-feature/node_modules/.bin/shipfox-swc +0 -43
  44. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-check +0 -43
  45. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-emit +0 -43
  46. package/test/external/toy-feature/package.json +0 -57
  47. package/test/external/toy-feature/src/config.ts +0 -5
  48. package/test/external/toy-feature/src/index.ts +0 -64
  49. package/test/external/toy-feature/src/provider.tsx +0 -33
  50. package/test/external/toy-feature/src/routes/insights.tsx +0 -7
  51. package/test/external/toy-feature/src/routes/settings.tsx +0 -7
  52. package/test/external/toy-feature/tsconfig.build.json +0 -5
  53. package/test/external/toy-feature/tsconfig.json +0 -1
  54. package/test/not-route-impl.ts +0 -1
@@ -1,17 +1,30 @@
1
1
  import {spawn} from 'node:child_process';
2
2
  import {globSync} from 'node:fs';
3
- import {cp, mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile} from 'node:fs/promises';
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 linkMode = process.argv.includes('--link');
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 (process.argv.some((argument) => argument.startsWith('--') && argument !== '--link')) {
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 = entryPoints
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, closure, workspacePackages);
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 validateDefaultPackageResolution(fixtureRoot, runtimeEntryPoints);
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 run('pnpm', ['exec', 'tsc', '--noEmit'], fixtureRoot);
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} external client runtime packages and ${entryPoints.length} public entry points in ${linkMode ? 'linked' : 'packed-tarball'} mode.\n`,
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
- await mkdir(tarballRoot);
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 run('pnpm', ['pack', '--out', tarball], workspacePackage.directory, {capture: true});
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 configureFixture(root, packageSpecs) {
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
- manifest.dependencies = {...manifest.dependencies, ...packageSpecs};
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 installedManifestPath = join(root, 'node_modules', name, 'package.json');
135
- const installedManifest = JSON.parse(await readFile(installedManifestPath, 'utf8'));
136
- const expectedManifest = requiredPackage(packages, name).manifest;
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
- const workspaceRange = findWorkspaceRange(installedManifest);
143
- if (workspaceRange)
144
- throw new Error(`Packed ${name} contains a workspace range at ${workspaceRange}`);
145
- validateDefaultInternalImports(name, installedManifest);
146
- const installedRoot = await realpath(join(root, 'node_modules', name));
147
- if (!installedRoot.startsWith(realFixtureRoot)) {
148
- throw new Error(`Packed ${name} resolved outside the external consumer`);
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 validateDefaultPackageResolution(root, specifiers) {
167
- const resolution = await run(
168
- process.execPath,
169
- [
170
- '--input-type=module',
171
- '--eval',
172
- `const specifiers = ${JSON.stringify(specifiers)};
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
- root,
177
- {capture: true},
178
- );
179
- const resolved = JSON.parse(resolution.stdout);
180
- for (const specifier of specifiers) {
181
- const resolvedPath = fileURLToPath(resolved[specifier]);
182
- if (!resolvedPath.split(sep).includes('dist')) {
183
- throw new Error(`Packed ${specifier} resolved to source instead of dist: ${resolvedPath}`);
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,44 @@ 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
+ const expectedImports = [
354
+ '@shipfox/client-auth/routes/index',
355
+ '@shipfox/client-invitations/routes/accept',
356
+ '@shipfox/client-integrations/routes/github',
357
+ '@shipfox/client-projects/routes/home',
358
+ '@shipfox/client-workflows/routes/runs',
359
+ '@shipfox/client-agent/routes/model-provider',
360
+ '@shipfox/client-workspace-settings/routes/members',
361
+ './features/login-override',
362
+ './features/external-settings',
363
+ ];
364
+ for (const expected of expectedImports) {
365
+ if (!generated.includes(expected)) {
366
+ throw new Error(`Generated default composition did not include ${JSON.stringify(expected)}.`);
367
+ }
368
+ }
369
+
370
+ for (const match of generated.matchAll(/\bfrom\s+['"]([^'"]+)['"]/gu)) {
371
+ const specifier = match[1];
372
+ if (specifier.startsWith('.') || specifier.startsWith('/')) continue;
373
+ const packageName = packageNameFromSpecifier(specifier);
374
+ if (!declaredDependencies.has(packageName)) {
375
+ throw new Error(
376
+ `Generated default composition imports undeclared package ${JSON.stringify(packageName)} from ${JSON.stringify(specifier)}.`,
377
+ );
378
+ }
379
+ }
380
+ }
381
+
382
+ function packageNameFromSpecifier(specifier) {
383
+ if (!specifier.startsWith('@')) return specifier.split('/')[0];
384
+ return specifier.split('/').slice(0, 2).join('/');
385
+ }
386
+
201
387
  function requiredPackage(packages, name) {
202
388
  const workspacePackage = packages.get(name);
203
389
  if (!workspacePackage) throw new Error(`Missing workspace package: ${name}`);
@@ -303,7 +489,7 @@ function run(command, args, cwd, options = {}) {
303
489
  stderr += chunk;
304
490
  });
305
491
  child.on('error', reject);
306
- child.on('exit', (code) => {
492
+ child.on('close', (code) => {
307
493
  const result = {code: code ?? 1, stdout, stderr};
308
494
  if (code === 0 || allowFailure) resolvePromise(result);
309
495
  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 route0Impl from "#test/search-route-impl.js";
7
- import route1Impl from "#test/default-route-impl.js";
8
- import route2Impl from "#test/named-route-impl.js";
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(route0Impl, "#test/search-route-impl.js", "/workspaces/$wid/projects/$pid/overview"),
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(route1Impl, "#test/default-route-impl.js", "/workspaces/$wid/settings/members"),
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(route2Impl, "#test/named-route-impl.js", "/workspaces/$wid/insights"),
60
+ ...routeOptions(route2Module.default, "#test/named-route-impl.js", "/workspaces/$wid/insights"),
61
61
  });
62
62
 
63
63
  const projectLayout = skeleton.projectLayout.addChildren([route0]);
@@ -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'}}>