@shipfox/client-shell 1.0.0 → 3.0.1

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 (103) hide show
  1. package/assets/apple-touch-icon.png +0 -0
  2. package/assets/favicon-96x96.png +0 -0
  3. package/assets/favicon.ico +0 -0
  4. package/assets/favicon.svg +1 -0
  5. package/assets/site.webmanifest +22 -0
  6. package/assets/web-app-manifest-192x192.png +0 -0
  7. package/assets/web-app-manifest-512x512.png +0 -0
  8. package/dist/tsconfig.test.tsbuildinfo +1 -1
  9. package/dist/vite/index.d.ts +1 -0
  10. package/dist/vite/index.d.ts.map +1 -1
  11. package/dist/vite/index.js +1 -0
  12. package/dist/vite/index.js.map +1 -1
  13. package/dist/vite/manifest.d.ts +3 -0
  14. package/dist/vite/manifest.d.ts.map +1 -0
  15. package/dist/vite/manifest.js +140 -0
  16. package/dist/vite/manifest.js.map +1 -0
  17. package/package.json +15 -9
  18. package/.turbo/turbo-build.log +0 -2
  19. package/.turbo/turbo-type$colon$emit.log +0 -1
  20. package/.turbo/turbo-type.log +0 -1
  21. package/CHANGELOG.md +0 -41
  22. package/src/components/main-layout.tsx +0 -45
  23. package/src/components/nav-bar.tsx +0 -41
  24. package/src/components/nav-tabs.test.tsx +0 -60
  25. package/src/components/nav-tabs.tsx +0 -47
  26. package/src/components/not-found-page.tsx +0 -14
  27. package/src/components/settings-nav.tsx +0 -35
  28. package/src/components/user-menu.tsx +0 -59
  29. package/src/components/workspace-crumb.tsx +0 -43
  30. package/src/components/workspace-switcher.tsx +0 -75
  31. package/src/compose/compose-routes.test.ts +0 -112
  32. package/src/compose/compose-routes.ts +0 -60
  33. package/src/compose/errors.ts +0 -54
  34. package/src/compose/merge-config.test.ts +0 -26
  35. package/src/compose/merge-config.ts +0 -27
  36. package/src/compose/normalize-route-path.ts +0 -6
  37. package/src/compose/validate-providers.test.ts +0 -31
  38. package/src/compose/validate-providers.ts +0 -36
  39. package/src/compose/validate-registries.test.ts +0 -92
  40. package/src/compose/validate-registries.ts +0 -61
  41. package/src/config/config-screen.test.tsx +0 -37
  42. package/src/config/shell-config.ts +0 -10
  43. package/src/contract.ts +0 -47
  44. package/src/index.ts +0 -1
  45. package/src/runtime/active-workspace.ts +0 -13
  46. package/src/runtime/anchor-paths.ts +0 -18
  47. package/src/runtime/anchors.tsx +0 -106
  48. package/src/runtime/assemble-route-tree.ts +0 -42
  49. package/src/runtime/auth.tsx +0 -213
  50. package/src/runtime/chrome-context.tsx +0 -22
  51. package/src/runtime/compose-client-app.tsx +0 -122
  52. package/src/runtime/define-route.test.ts +0 -15
  53. package/src/runtime/define-route.ts +0 -33
  54. package/src/runtime/index.ts +0 -19
  55. package/src/runtime/last-workspace.ts +0 -22
  56. package/src/runtime/nav-order.ts +0 -1
  57. package/src/runtime/provider-stack.tsx +0 -39
  58. package/src/runtime/providers.test.tsx +0 -51
  59. package/src/runtime/registries.test.tsx +0 -100
  60. package/src/runtime/registries.ts +0 -42
  61. package/src/runtime/router-context.ts +0 -9
  62. package/src/runtime/routes.test.tsx +0 -48
  63. package/src/runtime/workspace-setup.tsx +0 -61
  64. package/src/testing/index.test.tsx +0 -57
  65. package/src/testing/index.tsx +0 -51
  66. package/src/vite/evaluate-features.test.ts +0 -89
  67. package/src/vite/evaluate-features.ts +0 -114
  68. package/src/vite/generate.test.ts +0 -22
  69. package/src/vite/generate.ts +0 -99
  70. package/src/vite/generated-router.test.tsx +0 -58
  71. package/src/vite/index.ts +0 -1
  72. package/src/vite/plugin.test.ts +0 -231
  73. package/src/vite/plugin.ts +0 -101
  74. package/src/vite/validate.test.ts +0 -166
  75. package/test/default-route-impl.tsx +0 -3
  76. package/test/external/FINDINGS.md +0 -41
  77. package/test/external/README.md +0 -37
  78. package/test/external/fixture/index.html +0 -12
  79. package/test/external/fixture/package.json +0 -31
  80. package/test/external/fixture/src/app.fixture.tsx +0 -104
  81. package/test/external/fixture/src/features/external-settings.tsx +0 -19
  82. package/test/external/fixture/src/features/login-override.tsx +0 -9
  83. package/test/external/fixture/src/features.collision.ts +0 -15
  84. package/test/external/fixture/src/features.ts +0 -50
  85. package/test/external/fixture/src/link-typecheck.tsx +0 -10
  86. package/test/external/fixture/src/main.tsx +0 -86
  87. package/test/external/fixture/src/provider.ts +0 -45
  88. package/test/external/fixture/tsconfig.json +0 -15
  89. package/test/external/fixture/vite.config.ts +0 -21
  90. package/test/external/fixture/vitest.config.ts +0 -17
  91. package/test/external/verify.mjs +0 -504
  92. package/test/fixtures/features.ts +0 -56
  93. package/test/named-route-impl.tsx +0 -3
  94. package/test/render.tsx +0 -67
  95. package/test/search-route-impl.tsx +0 -7
  96. package/test/setup.ts +0 -3
  97. package/test/typecheck/shipfox-app.gen.ts +0 -85
  98. package/test/typecheck/types.tsx +0 -23
  99. package/tsconfig.build.json +0 -10
  100. package/tsconfig.build.tsbuildinfo +0 -1
  101. package/tsconfig.json +0 -1
  102. package/tsconfig.test.json +0 -12
  103. package/vitest.config.ts +0 -22
@@ -1,504 +0,0 @@
1
- import {spawn} from 'node:child_process';
2
- import {globSync} from 'node:fs';
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';
14
- import {tmpdir} from 'node:os';
15
- import {basename, dirname, join, relative, resolve, sep} from 'node:path';
16
- import {fileURLToPath, pathToFileURL} from 'node:url';
17
-
18
- const repositoryRoot = resolve(fileURLToPath(new URL('../../../../..', import.meta.url)));
19
- const externalRoot = fileURLToPath(new URL('.', import.meta.url));
20
- const fixtureTemplate = join(externalRoot, 'fixture');
21
- const REGISTRY_SHIPFOX_PACKAGE_PATTERN = /^@shipfox\+[^@]+@\d/u;
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');
26
-
27
- if (arguments_.some((argument) => argument.startsWith('--') && argument !== '--link')) {
28
- throw new Error('Usage: node verify.mjs [--link]');
29
- }
30
-
31
- const {
32
- computePublicationClosure,
33
- entryPointSupportsRuntimeImport,
34
- entryPointSupportsTypeResolution,
35
- listPublicPackageEntryPoints,
36
- readPublicationClosureConfig,
37
- readWorkspacePackages,
38
- validatePublicationState,
39
- } = await import(
40
- pathToFileURL(join(repositoryRoot, 'tools/application-release/dist/package-closure.js')).href
41
- );
42
- const {productionizeManifest} = await import(
43
- pathToFileURL(join(repositoryRoot, 'tools/utils/src/productionize.js')).href
44
- );
45
- const config = readPublicationClosureConfig(join(repositoryRoot, 'publication-closure.json'));
46
- const workspacePackages = readWorkspacePackages(repositoryRoot);
47
- validatePublicationState(workspacePackages, config, repositoryRoot);
48
- const clientRoots = config.roots.filter((root) => root.startsWith('@shipfox/client-'));
49
- const fixtureSupportPackages = ['@shipfox/client-config'];
50
- const consumerPackages = [...clientRoots, ...fixtureSupportPackages];
51
- const closure = computePublicationClosure(workspacePackages, clientRoots);
52
- const entryPoints = closure.flatMap((name) =>
53
- listConcreteEntryPoints(name, requiredPackage(workspacePackages, name)),
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);
61
- const runtimeEntryPoints = entryPoints
62
- .filter(({target}) => entryPointSupportsRuntimeImport(target))
63
- .map(({specifier}) => specifier);
64
- const typeEntryPoints = consumerTypeEntryPoints
65
- .filter(({target}) => entryPointSupportsTypeResolution(target))
66
- .map(({specifier}) => specifier);
67
- const temporaryRoot = await mkdtemp(join(tmpdir(), 'shipfox-client-composition-'));
68
- const fixtureRoot = join(temporaryRoot, 'fixture');
69
-
70
- try {
71
- await cp(fixtureTemplate, fixtureRoot, {recursive: true});
72
- const packageSpecs = linkMode
73
- ? linkedPackageSpecs(closure, workspacePackages)
74
- : await packedPackageSpecs(closure, workspacePackages, temporaryRoot);
75
- await configureFixture(fixtureRoot, consumerPackages, packageSpecs);
76
- await writeTypeFixture(fixtureRoot, typeEntryPoints);
77
- await run('pnpm', ['install', '--prefer-offline', '--ignore-scripts'], fixtureRoot, {
78
- capture: true,
79
- });
80
-
81
- if (linkMode) await validateLinkedPackages(fixtureRoot, consumerPackages, workspacePackages);
82
- else {
83
- await validateInstalledPackages(fixtureRoot, closure, workspacePackages);
84
- await validateNoRegistryShipfoxPackages(fixtureRoot);
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);
93
- }
94
-
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
- }
115
-
116
- process.stdout.write(
117
- `Verified the default client composition, ${closure.length} runtime packages, and ${entryPoints.length} public entry points in ${linkMode ? 'linked' : 'packed-tarball'} mode.\n`,
118
- );
119
- process.stdout.write(`Collision: ${EXPECTED_COLLISION_DIAGNOSTIC}\n`);
120
- } finally {
121
- await rm(temporaryRoot, {recursive: true, force: true});
122
- }
123
-
124
- function linkedPackageSpecs(names, packages) {
125
- return Object.fromEntries(
126
- names.map((name) => {
127
- const workspacePackage = requiredPackage(packages, name);
128
- return [name, `link:${workspacePackage.directory}`];
129
- }),
130
- );
131
- }
132
-
133
- async function packedPackageSpecs(names, packages, root) {
134
- const tarballRoot = join(root, 'tarballs');
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
- ]);
141
- const tarballs = await mapWithConcurrency(names, 4, async (name) => {
142
- const workspacePackage = requiredPackage(packages, name);
143
- const tarball = join(tarballRoot, `${safePackageName(name)}.tgz`);
144
- await packProductionizedPackage(workspacePackage, tarball, stagingRoot);
145
- return [name, `file:${tarball}`];
146
- });
147
- return Object.fromEntries(tarballs);
148
- }
149
-
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) {
165
- const manifestPath = join(root, 'package.json');
166
- const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
167
- const consumerDependencies = Object.fromEntries(
168
- consumerPackages.map((name) => [name, packageSpecs[name]]),
169
- );
170
- manifest.dependencies = {...manifest.dependencies, ...consumerDependencies};
171
- await Promise.all([
172
- writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`),
173
- writeFile(
174
- join(root, 'pnpm-workspace.yaml'),
175
- `packages:\n - .\noverrides:\n${Object.entries(packageSpecs)
176
- .map(([name, specifier]) => ` ${JSON.stringify(name)}: ${JSON.stringify(specifier)}`)
177
- .join('\n')}\n`,
178
- ),
179
- ]);
180
- }
181
-
182
- async function writeTypeFixture(root, entryPoints) {
183
- await writeFile(
184
- join(root, 'types.ts'),
185
- `${entryPoints
186
- .map(
187
- (specifier, index) => `import type * as Entry${index} from ${JSON.stringify(specifier)};`,
188
- )
189
- .join('\n')}\n\n${entryPoints
190
- .map((_, index) => `void (0 as unknown as typeof Entry${index});`)
191
- .join('\n')}\n`,
192
- );
193
- }
194
-
195
- async function validateLinkedPackages(root, names, packages) {
196
- for (const name of names) {
197
- const installedRoot = await realpath(join(root, 'node_modules', name));
198
- const expectedRoot = await realpath(requiredPackage(packages, name).directory);
199
- if (installedRoot !== expectedRoot) {
200
- throw new Error(`Linked ${name} resolved to ${installedRoot}; expected ${expectedRoot}`);
201
- }
202
- }
203
- }
204
-
205
- async function validateInstalledPackages(root, names, packages) {
206
- const realFixtureRoot = await realpath(root);
207
- for (const name of names) {
208
- for (const packageRoot of installedPackageRoots(root, name)) {
209
- const installedManifest = JSON.parse(
210
- await readFile(join(packageRoot, 'package.json'), 'utf8'),
211
- );
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
- }
226
- }
227
- }
228
- }
229
-
230
- function validateDefaultInternalImports(name, manifest) {
231
- const internalImports = manifest.imports?.['#*'];
232
- if (internalImports === undefined) return;
233
- if (internalImports === './dist/*') return;
234
- if (
235
- !internalImports ||
236
- typeof internalImports !== 'object' ||
237
- Array.isArray(internalImports) ||
238
- internalImports.default !== './dist/*'
239
- ) {
240
- throw new Error(`Packed ${name} does not map the default #* condition to ./dist/*`);
241
- }
242
- }
243
-
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)};
255
- const resolved = Object.fromEntries(specifiers.map((specifier) => [specifier, import.meta.resolve(specifier)]));
256
- process.stdout.write(JSON.stringify(resolved));`,
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')));
286
- }
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;
334
- }
335
-
336
- async function validateNoRegistryShipfoxPackages(root) {
337
- const virtualStore = await readdir(join(root, 'node_modules/.pnpm'), {withFileTypes: true});
338
- const registryPackages = virtualStore
339
- .filter((entry) => entry.isDirectory())
340
- .map((entry) => entry.name)
341
- .filter((name) => REGISTRY_SHIPFOX_PACKAGE_PATTERN.test(name));
342
- if (registryPackages.length) {
343
- throw new Error(
344
- `External consumer used registry Shipfox packages: ${registryPackages.join(', ')}`,
345
- );
346
- }
347
- }
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
-
387
- function requiredPackage(packages, name) {
388
- const workspacePackage = packages.get(name);
389
- if (!workspacePackage) throw new Error(`Missing workspace package: ${name}`);
390
- return workspacePackage;
391
- }
392
-
393
- function listConcreteEntryPoints(name, workspacePackage) {
394
- return listPublicPackageEntryPoints(name, workspacePackage.manifest.exports).flatMap(
395
- (entryPoint) => {
396
- if (!entryPoint.specifier.includes('*')) return entryPoint;
397
- return concretePatternEntryPoints(entryPoint, workspacePackage.directory);
398
- },
399
- );
400
- }
401
-
402
- function concretePatternEntryPoints(entryPoint, packageDirectory) {
403
- const runtimeTarget = defaultRuntimeTarget(entryPoint.target);
404
- if (!runtimeTarget?.includes('*')) {
405
- throw new Error(
406
- `Wildcard export ${entryPoint.specifier} must define a default JavaScript target.`,
407
- );
408
- }
409
-
410
- const targetPattern = runtimeTarget.slice(2);
411
- const globPattern = targetPattern.replace('*', '**/*');
412
- const capturePattern = new RegExp(
413
- `^${targetPattern
414
- .split('*')
415
- .map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
416
- .join('(.+)')}$`,
417
- );
418
- const concreteEntryPoints = globSync(join(packageDirectory, globPattern)).map((path) => {
419
- const relativePath = relative(packageDirectory, path).split(sep).join('/');
420
- const match = capturePattern.exec(relativePath);
421
- if (!match) {
422
- throw new Error(
423
- `Wildcard export ${entryPoint.specifier} resolved ${relativePath} outside ${runtimeTarget}.`,
424
- );
425
- }
426
- return {...entryPoint, specifier: entryPoint.specifier.replace('*', match[1])};
427
- });
428
-
429
- if (!concreteEntryPoints.length) {
430
- throw new Error(
431
- `Wildcard export ${entryPoint.specifier} did not resolve any files matching ${runtimeTarget}.`,
432
- );
433
- }
434
-
435
- return concreteEntryPoints;
436
- }
437
-
438
- function defaultRuntimeTarget(target) {
439
- if (typeof target === 'string') return target.endsWith('.js') ? target : undefined;
440
- if (!target || typeof target !== 'object' || Array.isArray(target)) return undefined;
441
- const defaultTarget = target.default;
442
- return defaultTarget === undefined ? undefined : defaultRuntimeTarget(defaultTarget);
443
- }
444
-
445
- function findWorkspaceRange(value, path = 'package.json') {
446
- if (typeof value === 'string') return value.startsWith('workspace:') ? path : undefined;
447
- if (!value || typeof value !== 'object') return undefined;
448
- for (const [key, child] of Object.entries(value)) {
449
- const found = findWorkspaceRange(child, `${path}.${key}`);
450
- if (found) return found;
451
- }
452
- return undefined;
453
- }
454
-
455
- function safePackageName(name) {
456
- return name.replace('@shipfox/', '').replaceAll('/', '-');
457
- }
458
-
459
- async function mapWithConcurrency(values, concurrency, mapper) {
460
- const results = new Array(values.length);
461
- let nextIndex = 0;
462
-
463
- async function worker() {
464
- while (nextIndex < values.length) {
465
- const index = nextIndex;
466
- nextIndex += 1;
467
- results[index] = await mapper(values[index], index);
468
- }
469
- }
470
-
471
- await Promise.all(Array.from({length: concurrency}, () => worker()));
472
- return results;
473
- }
474
-
475
- function run(command, args, cwd, options = {}) {
476
- const {allowFailure = false, capture = false, env = {}} = options;
477
- return new Promise((resolvePromise, reject) => {
478
- const child = spawn(command, args, {
479
- cwd,
480
- env: {...process.env, ...env},
481
- stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
482
- });
483
- let stdout = '';
484
- let stderr = '';
485
- child.stdout?.on('data', (chunk) => {
486
- stdout += chunk;
487
- });
488
- child.stderr?.on('data', (chunk) => {
489
- stderr += chunk;
490
- });
491
- child.on('error', reject);
492
- child.on('close', (code) => {
493
- const result = {code: code ?? 1, stdout, stderr};
494
- if (code === 0 || allowFailure) resolvePromise(result);
495
- else {
496
- reject(
497
- new Error(
498
- `${basename(command)} ${args.join(' ')} exited with code ${code}${capture ? `\n${stdout}\n${stderr}` : ''}`,
499
- ),
500
- );
501
- }
502
- });
503
- });
504
- }
@@ -1,56 +0,0 @@
1
- import {defineClientFeature} from '#contract.js';
2
-
3
- export const features = [
4
- defineClientFeature({
5
- id: 'shipfox.projects',
6
- routes: [
7
- {
8
- path: '/workspaces/$wid/projects/$pid/overview',
9
- parent: 'projectLayout',
10
- impl: '#test/default-route-impl.js',
11
- },
12
- {
13
- path: '/workspaces/$wid/settings/members',
14
- parent: 'workspaceSettings',
15
- impl: '#test/default-route-impl.js',
16
- },
17
- ],
18
- navigation: [
19
- {
20
- id: 'projects',
21
- scope: 'workspace',
22
- label: 'Projects',
23
- to: '/workspaces/$wid/projects/$pid/overview',
24
- order: 100,
25
- },
26
- ],
27
- settingsSections: [
28
- {id: 'members', pathSegment: 'members', label: 'Members', icon: 'users', order: 100},
29
- ],
30
- }),
31
- defineClientFeature({
32
- id: 'acme.insights',
33
- routes: [
34
- {
35
- path: '/workspaces/$wid/insights',
36
- parent: 'workspaceLayout',
37
- impl: '#test/named-route-impl.js',
38
- },
39
- {
40
- path: '/workspaces/$wid/projects/$pid/overview',
41
- parent: 'projectLayout',
42
- override: true,
43
- impl: '#test/search-route-impl.js',
44
- },
45
- ],
46
- navigation: [
47
- {
48
- id: 'insights',
49
- scope: 'workspace',
50
- label: 'Insights',
51
- to: '/workspaces/$wid/insights',
52
- order: 200,
53
- },
54
- ],
55
- }),
56
- ] as const;
@@ -1,3 +0,0 @@
1
- import {defineRoute} from '@shipfox/client-shell/runtime';
2
-
3
- export default defineRoute({component: () => <div>Named route</div>});
package/test/render.tsx DELETED
@@ -1,67 +0,0 @@
1
- import {QueryClient} from '@tanstack/react-query';
2
- import {createMemoryHistory, createRouter, Outlet, RouterProvider} from '@tanstack/react-router';
3
- import {render} from '@testing-library/react';
4
- import {createStore} from 'jotai';
5
- import {composeRoutes} from '#compose/compose-routes.js';
6
- import type {ClientFeature} from '#contract.js';
7
- import {assembleRouteTree, type ResolveRouteImpl} from '#runtime/assemble-route-tree.js';
8
- import {type AuthStateValue, authStateAtom} from '#runtime/auth.js';
9
- import {ChromeProvider, type ChromeSlots} from '#runtime/chrome-context.js';
10
- import {ShellProviderStack} from '#runtime/provider-stack.js';
11
- import {navigationEntries, settingsEntries} from '#runtime/registries.js';
12
-
13
- export async function renderComposedShell({
14
- features,
15
- initialPath,
16
- resolveImpl,
17
- }: {
18
- features: readonly ClientFeature[];
19
- initialPath: string;
20
- resolveImpl: ResolveRouteImpl;
21
- }): Promise<{
22
- router: unknown;
23
- queryClient: QueryClient;
24
- store: ReturnType<typeof createStore>;
25
- }> {
26
- const routeTree = await assembleRouteTree(composeRoutes(features), {
27
- resolveImpl,
28
- navigation: navigationEntries(features),
29
- settingsSections: settingsEntries(features),
30
- });
31
- const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
32
- const store = createStore();
33
- const auth: AuthStateValue = {
34
- status: 'authenticated',
35
- workspaces: [{id: 'workspace', name: 'Workspace', membershipId: 'membership'}],
36
- isLoading: false,
37
- isAuthenticated: true,
38
- hasWorkspace: true,
39
- };
40
- const chrome: ChromeSlots = {
41
- ProjectBreadcrumb: () => null,
42
- ProjectLayoutGuard: Outlet,
43
- };
44
- store.set(authStateAtom, auth);
45
- const router = createRouter({
46
- history: createMemoryHistory({initialEntries: [initialPath]}),
47
- routeTree,
48
- context: {
49
- auth,
50
- queryClient,
51
- workspaceSetup: async () => ({hideProjectNavigation: false}),
52
- },
53
- });
54
- render(
55
- <ChromeProvider chrome={chrome}>
56
- <ShellProviderStack
57
- features={features}
58
- queryClient={queryClient}
59
- store={store}
60
- auth={{effects: false}}
61
- >
62
- <RouterProvider router={router} />
63
- </ShellProviderStack>
64
- </ChromeProvider>,
65
- );
66
- return {router, queryClient, store};
67
- }
@@ -1,7 +0,0 @@
1
- import {defineRoute} from '@shipfox/client-shell/runtime';
2
-
3
- export default defineRoute({
4
- component: () => <div>Search route</div>,
5
- validateSearch: (search: Record<string, unknown>) =>
6
- ({tab: search.tab === 'activity' ? 'activity' : 'overview'}) as const,
7
- });
package/test/setup.ts DELETED
@@ -1,3 +0,0 @@
1
- import {installClientDomTestEnv} from '@shipfox/client-test-setup';
2
-
3
- installClientDomTestEnv();