@shipfox/client-shell 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +26 -0
  5. package/LICENSE +21 -0
  6. package/dist/components/main-layout.d.ts +11 -0
  7. package/dist/components/main-layout.d.ts.map +1 -0
  8. package/dist/components/main-layout.js +39 -0
  9. package/dist/components/main-layout.js.map +1 -0
  10. package/dist/components/nav-bar.d.ts +4 -0
  11. package/dist/components/nav-bar.d.ts.map +1 -0
  12. package/dist/components/nav-bar.js +62 -0
  13. package/dist/components/nav-bar.js.map +1 -0
  14. package/dist/components/nav-tabs.d.ts +6 -0
  15. package/dist/components/nav-tabs.d.ts.map +1 -0
  16. package/dist/components/nav-tabs.js +38 -0
  17. package/dist/components/nav-tabs.js.map +1 -0
  18. package/dist/components/not-found-page.d.ts +2 -0
  19. package/dist/components/not-found-page.d.ts.map +1 -0
  20. package/dist/components/not-found-page.js +25 -0
  21. package/dist/components/not-found-page.js.map +1 -0
  22. package/dist/components/settings-nav.d.ts +5 -0
  23. package/dist/components/settings-nav.d.ts.map +1 -0
  24. package/dist/components/settings-nav.js +45 -0
  25. package/dist/components/settings-nav.js.map +1 -0
  26. package/dist/components/user-menu.d.ts +2 -0
  27. package/dist/components/user-menu.d.ts.map +1 -0
  28. package/dist/components/user-menu.js +76 -0
  29. package/dist/components/user-menu.js.map +1 -0
  30. package/dist/components/workspace-crumb.d.ts +7 -0
  31. package/dist/components/workspace-crumb.d.ts.map +1 -0
  32. package/dist/components/workspace-crumb.js +54 -0
  33. package/dist/components/workspace-crumb.js.map +1 -0
  34. package/dist/components/workspace-switcher.d.ts +5 -0
  35. package/dist/components/workspace-switcher.d.ts.map +1 -0
  36. package/dist/components/workspace-switcher.js +90 -0
  37. package/dist/components/workspace-switcher.js.map +1 -0
  38. package/dist/compose/compose-routes.d.ts +6 -0
  39. package/dist/compose/compose-routes.d.ts.map +1 -0
  40. package/dist/compose/compose-routes.js +53 -0
  41. package/dist/compose/compose-routes.js.map +1 -0
  42. package/dist/compose/errors.d.ts +26 -0
  43. package/dist/compose/errors.d.ts.map +1 -0
  44. package/dist/compose/errors.js +39 -0
  45. package/dist/compose/errors.js.map +1 -0
  46. package/dist/compose/merge-config.d.ts +4 -0
  47. package/dist/compose/merge-config.d.ts.map +1 -0
  48. package/dist/compose/merge-config.js +28 -0
  49. package/dist/compose/merge-config.js.map +1 -0
  50. package/dist/compose/normalize-route-path.d.ts +2 -0
  51. package/dist/compose/normalize-route-path.d.ts.map +1 -0
  52. package/dist/compose/normalize-route-path.js +7 -0
  53. package/dist/compose/normalize-route-path.js.map +1 -0
  54. package/dist/compose/validate-providers.d.ts +3 -0
  55. package/dist/compose/validate-providers.d.ts.map +1 -0
  56. package/dist/compose/validate-providers.js +32 -0
  57. package/dist/compose/validate-providers.js.map +1 -0
  58. package/dist/compose/validate-registries.d.ts +4 -0
  59. package/dist/compose/validate-registries.d.ts.map +1 -0
  60. package/dist/compose/validate-registries.js +50 -0
  61. package/dist/compose/validate-registries.js.map +1 -0
  62. package/dist/config/shell-config.d.ts +10 -0
  63. package/dist/config/shell-config.d.ts.map +1 -0
  64. package/dist/config/shell-config.js +12 -0
  65. package/dist/config/shell-config.js.map +1 -0
  66. package/dist/contract.d.ts +39 -0
  67. package/dist/contract.d.ts.map +1 -0
  68. package/dist/contract.js +5 -0
  69. package/dist/contract.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +3 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/runtime/active-workspace.d.ts +4 -0
  75. package/dist/runtime/active-workspace.d.ts.map +1 -0
  76. package/dist/runtime/active-workspace.js +15 -0
  77. package/dist/runtime/active-workspace.js.map +1 -0
  78. package/dist/runtime/anchor-paths.d.ts +9 -0
  79. package/dist/runtime/anchor-paths.d.ts.map +1 -0
  80. package/dist/runtime/anchor-paths.js +18 -0
  81. package/dist/runtime/anchor-paths.js.map +1 -0
  82. package/dist/runtime/anchors.d.ts +19 -0
  83. package/dist/runtime/anchors.d.ts.map +1 -0
  84. package/dist/runtime/anchors.js +125 -0
  85. package/dist/runtime/anchors.js.map +1 -0
  86. package/dist/runtime/assemble-route-tree.d.ts +10 -0
  87. package/dist/runtime/assemble-route-tree.d.ts.map +1 -0
  88. package/dist/runtime/assemble-route-tree.js +30 -0
  89. package/dist/runtime/assemble-route-tree.js.map +1 -0
  90. package/dist/runtime/auth.d.ts +50 -0
  91. package/dist/runtime/auth.d.ts.map +1 -0
  92. package/dist/runtime/auth.js +194 -0
  93. package/dist/runtime/auth.js.map +1 -0
  94. package/dist/runtime/chrome-context.d.ts +10 -0
  95. package/dist/runtime/chrome-context.d.ts.map +1 -0
  96. package/dist/runtime/chrome-context.js +16 -0
  97. package/dist/runtime/chrome-context.js.map +1 -0
  98. package/dist/runtime/compose-client-app.d.ts +13 -0
  99. package/dist/runtime/compose-client-app.d.ts.map +1 -0
  100. package/dist/runtime/compose-client-app.js +93 -0
  101. package/dist/runtime/compose-client-app.js.map +1 -0
  102. package/dist/runtime/define-route.d.ts +19 -0
  103. package/dist/runtime/define-route.d.ts.map +1 -0
  104. package/dist/runtime/define-route.js +12 -0
  105. package/dist/runtime/define-route.js.map +1 -0
  106. package/dist/runtime/index.d.ts +20 -0
  107. package/dist/runtime/index.d.ts.map +1 -0
  108. package/dist/runtime/index.js +21 -0
  109. package/dist/runtime/index.js.map +1 -0
  110. package/dist/runtime/last-workspace.d.ts +4 -0
  111. package/dist/runtime/last-workspace.d.ts.map +1 -0
  112. package/dist/runtime/last-workspace.js +20 -0
  113. package/dist/runtime/last-workspace.js.map +1 -0
  114. package/dist/runtime/nav-order.d.ts +7 -0
  115. package/dist/runtime/nav-order.d.ts.map +1 -0
  116. package/dist/runtime/nav-order.js +8 -0
  117. package/dist/runtime/nav-order.js.map +1 -0
  118. package/dist/runtime/provider-stack.d.ts +14 -0
  119. package/dist/runtime/provider-stack.d.ts.map +1 -0
  120. package/dist/runtime/provider-stack.js +28 -0
  121. package/dist/runtime/provider-stack.js.map +1 -0
  122. package/dist/runtime/registries.d.ts +4 -0
  123. package/dist/runtime/registries.d.ts.map +1 -0
  124. package/dist/runtime/registries.js +23 -0
  125. package/dist/runtime/registries.js.map +1 -0
  126. package/dist/runtime/router-context.d.ts +9 -0
  127. package/dist/runtime/router-context.d.ts.map +1 -0
  128. package/dist/runtime/router-context.js +3 -0
  129. package/dist/runtime/router-context.js.map +1 -0
  130. package/dist/runtime/workspace-setup.d.ts +18 -0
  131. package/dist/runtime/workspace-setup.d.ts.map +1 -0
  132. package/dist/runtime/workspace-setup.js +64 -0
  133. package/dist/runtime/workspace-setup.js.map +1 -0
  134. package/dist/testing/index.d.ts +17 -0
  135. package/dist/testing/index.d.ts.map +1 -0
  136. package/dist/testing/index.js +35 -0
  137. package/dist/testing/index.js.map +1 -0
  138. package/dist/tsconfig.test.tsbuildinfo +1 -0
  139. package/dist/vite/evaluate-features.d.ts +8 -0
  140. package/dist/vite/evaluate-features.d.ts.map +1 -0
  141. package/dist/vite/evaluate-features.js +80 -0
  142. package/dist/vite/evaluate-features.js.map +1 -0
  143. package/dist/vite/generate.d.ts +9 -0
  144. package/dist/vite/generate.d.ts.map +1 -0
  145. package/dist/vite/generate.js +80 -0
  146. package/dist/vite/generate.js.map +1 -0
  147. package/dist/vite/index.d.ts +2 -0
  148. package/dist/vite/index.d.ts.map +1 -0
  149. package/dist/vite/index.js +3 -0
  150. package/dist/vite/index.js.map +1 -0
  151. package/dist/vite/plugin.d.ts +7 -0
  152. package/dist/vite/plugin.d.ts.map +1 -0
  153. package/dist/vite/plugin.js +77 -0
  154. package/dist/vite/plugin.js.map +1 -0
  155. package/package.json +113 -0
  156. package/src/components/main-layout.tsx +45 -0
  157. package/src/components/nav-bar.tsx +41 -0
  158. package/src/components/nav-tabs.test.tsx +60 -0
  159. package/src/components/nav-tabs.tsx +47 -0
  160. package/src/components/not-found-page.tsx +14 -0
  161. package/src/components/settings-nav.tsx +35 -0
  162. package/src/components/user-menu.tsx +59 -0
  163. package/src/components/workspace-crumb.tsx +43 -0
  164. package/src/components/workspace-switcher.tsx +75 -0
  165. package/src/compose/compose-routes.test.ts +112 -0
  166. package/src/compose/compose-routes.ts +60 -0
  167. package/src/compose/errors.ts +54 -0
  168. package/src/compose/merge-config.test.ts +26 -0
  169. package/src/compose/merge-config.ts +27 -0
  170. package/src/compose/normalize-route-path.ts +6 -0
  171. package/src/compose/validate-providers.test.ts +31 -0
  172. package/src/compose/validate-providers.ts +36 -0
  173. package/src/compose/validate-registries.test.ts +92 -0
  174. package/src/compose/validate-registries.ts +61 -0
  175. package/src/config/config-screen.test.tsx +37 -0
  176. package/src/config/shell-config.ts +10 -0
  177. package/src/contract.ts +47 -0
  178. package/src/index.ts +1 -0
  179. package/src/runtime/active-workspace.ts +13 -0
  180. package/src/runtime/anchor-paths.ts +18 -0
  181. package/src/runtime/anchors.tsx +106 -0
  182. package/src/runtime/assemble-route-tree.ts +42 -0
  183. package/src/runtime/auth.tsx +213 -0
  184. package/src/runtime/chrome-context.tsx +22 -0
  185. package/src/runtime/compose-client-app.tsx +122 -0
  186. package/src/runtime/define-route.test.ts +15 -0
  187. package/src/runtime/define-route.ts +33 -0
  188. package/src/runtime/index.ts +19 -0
  189. package/src/runtime/last-workspace.ts +22 -0
  190. package/src/runtime/nav-order.ts +1 -0
  191. package/src/runtime/provider-stack.tsx +39 -0
  192. package/src/runtime/providers.test.tsx +51 -0
  193. package/src/runtime/registries.test.tsx +100 -0
  194. package/src/runtime/registries.ts +42 -0
  195. package/src/runtime/router-context.ts +9 -0
  196. package/src/runtime/routes.test.tsx +48 -0
  197. package/src/runtime/workspace-setup.tsx +61 -0
  198. package/src/testing/index.test.tsx +57 -0
  199. package/src/testing/index.tsx +51 -0
  200. package/src/vite/evaluate-features.test.ts +34 -0
  201. package/src/vite/evaluate-features.ts +93 -0
  202. package/src/vite/generate.test.ts +22 -0
  203. package/src/vite/generate.ts +99 -0
  204. package/src/vite/generated-router.test.tsx +58 -0
  205. package/src/vite/index.ts +1 -0
  206. package/src/vite/plugin.test.ts +181 -0
  207. package/src/vite/plugin.ts +114 -0
  208. package/src/vite/validate.test.ts +166 -0
  209. package/test/default-route-impl.tsx +3 -0
  210. package/test/external/FINDINGS.md +53 -0
  211. package/test/external/README.md +39 -0
  212. package/test/external/fixture/index.html +12 -0
  213. package/test/external/fixture/package.json +32 -0
  214. package/test/external/fixture/src/app.fixture.tsx +72 -0
  215. package/test/external/fixture/src/features/override-impl.tsx +12 -0
  216. package/test/external/fixture/src/features/override.tsx +20 -0
  217. package/test/external/fixture/src/features.collision.ts +15 -0
  218. package/test/external/fixture/src/features.ts +4 -0
  219. package/test/external/fixture/src/link-typecheck.tsx +10 -0
  220. package/test/external/fixture/src/main.tsx +4 -0
  221. package/test/external/fixture/tsconfig.json +15 -0
  222. package/test/external/fixture/vite.config.ts +10 -0
  223. package/test/external/fixture/vitest.config.ts +15 -0
  224. package/test/external/toy-feature/CHANGELOG.md +15 -0
  225. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-check +43 -0
  226. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-format +43 -0
  227. package/test/external/toy-feature/node_modules/.bin/shipfox-biome-lint +43 -0
  228. package/test/external/toy-feature/node_modules/.bin/shipfox-swc +43 -0
  229. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-check +43 -0
  230. package/test/external/toy-feature/node_modules/.bin/shipfox-tsc-emit +43 -0
  231. package/test/external/toy-feature/package.json +57 -0
  232. package/test/external/toy-feature/src/config.ts +5 -0
  233. package/test/external/toy-feature/src/index.ts +64 -0
  234. package/test/external/toy-feature/src/provider.tsx +33 -0
  235. package/test/external/toy-feature/src/routes/insights.tsx +7 -0
  236. package/test/external/toy-feature/src/routes/settings.tsx +7 -0
  237. package/test/external/toy-feature/tsconfig.build.json +5 -0
  238. package/test/external/toy-feature/tsconfig.json +1 -0
  239. package/test/external/verify.mjs +318 -0
  240. package/test/fixtures/features.ts +56 -0
  241. package/test/named-route-impl.tsx +3 -0
  242. package/test/not-route-impl.ts +1 -0
  243. package/test/render.tsx +67 -0
  244. package/test/search-route-impl.tsx +7 -0
  245. package/test/setup.ts +3 -0
  246. package/test/typecheck/shipfox-app.gen.ts +85 -0
  247. package/test/typecheck/types.tsx +18 -0
  248. package/tsconfig.build.json +10 -0
  249. package/tsconfig.build.tsbuildinfo +1 -0
  250. package/tsconfig.json +1 -0
  251. package/tsconfig.test.json +12 -0
  252. package/vitest.config.ts +22 -0
@@ -0,0 +1,166 @@
1
+ import {z} from 'zod';
2
+ import {composeRoutes} from '#compose/compose-routes.js';
3
+ import {mergeConfigShapes} from '#compose/merge-config.js';
4
+ import {validateProviderIds} from '#compose/validate-providers.js';
5
+ import {validateNavigation, validateSettingsSections} from '#compose/validate-registries.js';
6
+ import {defineClientFeature} from '#contract.js';
7
+
8
+ const route = {path: '/projects', parent: 'root' as const, impl: '#test/default-route-impl.js'};
9
+
10
+ describe('composition validation', () => {
11
+ test('accepts non-conflicting feature contributions', () => {
12
+ const features = [
13
+ defineClientFeature({
14
+ id: 'acme.projects',
15
+ routes: [
16
+ route,
17
+ {
18
+ path: '/workspaces/$wid/settings/members',
19
+ parent: 'workspaceSettings',
20
+ impl: '#test/default-route-impl.js',
21
+ },
22
+ ],
23
+ providers: [{id: 'acme-theme', Component: () => null}],
24
+ navigation: [{id: 'projects', scope: 'workspace', label: 'Projects', to: '/projects'}],
25
+ settingsSections: [
26
+ {id: 'members', pathSegment: 'members', label: 'Members', icon: 'users'},
27
+ ],
28
+ configShape: {projectId: z.string()},
29
+ }),
30
+ ];
31
+
32
+ const routes = composeRoutes(features);
33
+
34
+ expect(() => validateProviderIds(features)).not.toThrow();
35
+ expect(() =>
36
+ validateNavigation(
37
+ features,
38
+ routes.map(({path}) => path),
39
+ ),
40
+ ).not.toThrow();
41
+ expect(() =>
42
+ validateSettingsSections(
43
+ features,
44
+ routes.map(({path}) => path),
45
+ ),
46
+ ).not.toThrow();
47
+ expect(() => mergeConfigShapes(features)).not.toThrow();
48
+ });
49
+
50
+ test('reports route collisions with both feature ids', () => {
51
+ const features = [
52
+ defineClientFeature({id: 'shipfox.projects', routes: [route]}),
53
+ defineClientFeature({id: 'acme.projects', routes: [route]}),
54
+ ];
55
+
56
+ expect(() => composeRoutes(features)).toThrow(
57
+ 'Route "/projects" is contributed by both features "shipfox.projects" and "acme.projects". Set override: true to replace it explicitly.',
58
+ );
59
+ });
60
+
61
+ test('reports dangling and competing overrides with their feature ids', () => {
62
+ const dangling = [
63
+ defineClientFeature({id: 'acme.projects', routes: [{...route, override: true}]}),
64
+ ];
65
+ const competing = [
66
+ defineClientFeature({id: 'shipfox.projects', routes: [route]}),
67
+ defineClientFeature({id: 'acme.one', routes: [{...route, override: true}]}),
68
+ defineClientFeature({id: 'acme.two', routes: [{...route, override: true}]}),
69
+ ];
70
+
71
+ expect(() => composeRoutes(dangling)).toThrow(
72
+ 'Route override for "/projects" from feature "acme.projects" has no route to replace.',
73
+ );
74
+ expect(() => composeRoutes(competing)).toThrow(
75
+ 'Route "/projects" has competing overrides from features "acme.one" and "acme.two".',
76
+ );
77
+ });
78
+
79
+ test('reports reserved and duplicate provider ids', () => {
80
+ const reserved = [
81
+ defineClientFeature({id: 'acme.theme', providers: [{id: 'theme', Component: () => null}]}),
82
+ ];
83
+ const duplicate = [
84
+ defineClientFeature({id: 'acme.one', providers: [{id: 'flags', Component: () => null}]}),
85
+ defineClientFeature({id: 'acme.two', providers: [{id: 'flags', Component: () => null}]}),
86
+ ];
87
+
88
+ expect(() => validateProviderIds(reserved)).toThrow(
89
+ 'Provider id "theme" in feature "acme.theme" is reserved by the shell.',
90
+ );
91
+ expect(() => validateProviderIds(duplicate)).toThrow(
92
+ 'Provider id "flags" is contributed by both features "acme.one" and "acme.two".',
93
+ );
94
+ });
95
+
96
+ test('reports duplicate and missing navigation entries', () => {
97
+ const duplicate = [
98
+ defineClientFeature({
99
+ id: 'acme.one',
100
+ navigation: [{id: 'projects', scope: 'workspace', label: 'Projects', to: '/projects'}],
101
+ }),
102
+ defineClientFeature({
103
+ id: 'acme.two',
104
+ navigation: [{id: 'projects', scope: 'workspace', label: 'Projects', to: '/projects'}],
105
+ }),
106
+ ];
107
+ const missing = [
108
+ defineClientFeature({
109
+ id: 'acme.insights',
110
+ navigation: [{id: 'insights', scope: 'workspace', label: 'Insights', to: '/insights'}],
111
+ }),
112
+ ];
113
+
114
+ expect(() => validateNavigation(duplicate, ['/projects'])).toThrow(
115
+ 'Navigation entry "projects" is contributed by both features "acme.one" and "acme.two".',
116
+ );
117
+ expect(() => validateNavigation(missing, [])).toThrow(
118
+ 'Navigation entry "insights" in feature "acme.insights" targets missing route "/insights".',
119
+ );
120
+ });
121
+
122
+ test('reports duplicate and missing settings sections', () => {
123
+ const duplicate = [
124
+ defineClientFeature({
125
+ id: 'acme.one',
126
+ settingsSections: [
127
+ {id: 'members', pathSegment: 'members', label: 'Members', icon: 'users'},
128
+ ],
129
+ }),
130
+ defineClientFeature({
131
+ id: 'acme.two',
132
+ settingsSections: [
133
+ {id: 'members', pathSegment: 'members', label: 'Members', icon: 'users'},
134
+ ],
135
+ }),
136
+ ];
137
+ const missing = [
138
+ defineClientFeature({
139
+ id: 'acme.insights',
140
+ settingsSections: [
141
+ {id: 'insights', pathSegment: 'insights', label: 'Insights', icon: 'chart-line'},
142
+ ],
143
+ }),
144
+ ];
145
+
146
+ expect(() =>
147
+ validateSettingsSections(duplicate, ['/workspaces/$wid/settings/members']),
148
+ ).toThrow(
149
+ 'Settings section "members" is contributed by both features "acme.one" and "acme.two".',
150
+ );
151
+ expect(() => validateSettingsSections(missing, [])).toThrow(
152
+ 'Settings section "insights" in feature "acme.insights" requires route "/workspaces/$wid/settings/insights".',
153
+ );
154
+ });
155
+
156
+ test('reports duplicate config keys', () => {
157
+ const features = [
158
+ defineClientFeature({id: 'acme.one', configShape: {token: z.string()}}),
159
+ defineClientFeature({id: 'acme.two', configShape: {token: z.string()}}),
160
+ ];
161
+
162
+ expect(() => mergeConfigShapes(features)).toThrow(
163
+ 'Config key "token" is contributed by both features "acme.one" and "acme.two". Reuse the same schema instance to intentionally share it.',
164
+ );
165
+ });
166
+ });
@@ -0,0 +1,3 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+
3
+ export default defineRoute({component: () => <div>Default route</div>});
@@ -0,0 +1,53 @@
1
+ # External composition findings
2
+
3
+ Both the linked iteration mode and packed-tarball exit gate passed on 2026-07-16. The verifier
4
+ computed and installed a 12-package `@shipfox/*` runtime closure.
5
+
6
+ ## Declaration portability
7
+
8
+ The packed consumer's `tsc --noEmit` accepted a typed `Link` and `useSearch` for the added
9
+ `/workspaces/$wid/insights` route. The generated router also resolved the toy package's emitted
10
+ default `defineRoute(...)` declarations and the shell's anchor return types from `dist`.
11
+
12
+ The packed verifier also checks that every installed `#*` import map defaults to `./dist/*` and
13
+ that representative shell and toy-feature public entrypoints resolve beneath `dist`. This keeps the
14
+ proof valid even while the tarballs still include source files.
15
+
16
+ The proof exposed two package-boundary gaps before it passed:
17
+
18
+ - Compiled client packages still mapped internal `#*` imports directly to `src`. Their package
19
+ manifests now use `workspace-source` and `development` for source, and `default` for `dist`.
20
+ - The inferred anchor declarations name `@tanstack/router-core`. The shell now declares that direct
21
+ dependency so isolated pnpm consumers can resolve it.
22
+
23
+ The fixture did not need `@storybook/react` at runtime or during its consumer type-check. The
24
+ `ShellProviders` declaration's type-only Storybook import is erased at runtime; the fixture uses the
25
+ same `skipLibCheck` setting as the existing external-consumer verifier.
26
+
27
+ ## Development-mode coverage
28
+
29
+ The gate intentionally proves production builds and default package resolution; it does not start a
30
+ Vite development server. The `development` condition still resolves to the TypeScript source that
31
+ the packages currently ship. Plugin watch and regeneration behavior remains covered by ENG-961's
32
+ focused tests. If published packages stop including source, external development-mode support needs
33
+ a separate product decision and proof rather than making this release gate longer.
34
+
35
+ ## Generated-file developer experience
36
+
37
+ The generated file keeps both forms of route implementation specifier readable:
38
+
39
+ - App-local implementations use a path relative to `src/shipfox-app.gen.ts`, such as
40
+ `./features/override-impl`.
41
+ - Packaged implementations use an exported package subpath, such as
42
+ `@shipfox/client-shell-fixture-feature/routes/insights`.
43
+
44
+ The app's Node-evaluated feature manifest imports local TypeScript modules without a `.js` suffix so
45
+ jiti resolves the source file. Route implementation modules are never evaluated by jiti.
46
+
47
+ ## Collision diagnostic
48
+
49
+ The rejected build returned this diagnostic and a non-zero status:
50
+
51
+ ```text
52
+ Route "/workspaces/$wid/insights" is contributed by both features "fixture.toy-feature" and "fixture.unapproved-collision". Set override: true to replace it explicitly.
53
+ ```
@@ -0,0 +1,39 @@
1
+ # External client runtime fixture
2
+
3
+ This fixture packs the published client runtime closure into a Vite application outside the pnpm
4
+ workspace. It is a manual structural gate and does not add a CI job.
5
+
6
+ ## Prerequisites
7
+
8
+ Build runtime files, declarations, and the closure helper before running either mode:
9
+
10
+ ```sh
11
+ turbo build type:emit \
12
+ --filter='./libs/client/**' \
13
+ --filter=@shipfox/application-release
14
+ ```
15
+
16
+ ## Run the fast linked mode
17
+
18
+ ```sh
19
+ node libs/client/shell/test/external/verify.mjs --link
20
+ ```
21
+
22
+ This copies the Vite template to a temporary directory and links the workspace-built closure. It
23
+ runs a Vite build and resolves every non-pattern declaration entry point from the consumer.
24
+
25
+ ## Run the packed exit gate
26
+
27
+ ```sh
28
+ node libs/client/shell/test/external/verify.mjs
29
+ ```
30
+
31
+ This computes the runtime workspace closure from the client roots in
32
+ `publication-closure.json`, packs every package, and installs only those tarballs for
33
+ `@shipfox/*` dependencies. It repeats the linked checks against default `dist` exports and
34
+ verifies no candidate package came from the workspace or registry.
35
+
36
+ Both modes remove their temporary directories after completion. The behavioral composition fixture
37
+ and required CI gate are part of ENG-995; neither mode needs browser E2E infrastructure or adds
38
+ work to the normal CI test graph. Its source files and Vitest wiring remain in the fixture template
39
+ for that follow-up.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Shipfox client composition fixture</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "shipfox-client-shell-external-fixture",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "test": "vitest run",
9
+ "type": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@tanstack/react-query": "5.101.0",
13
+ "@tanstack/react-router": "1.170.16",
14
+ "@tanstack/router-core": "1.171.13",
15
+ "jotai": "2.20.1",
16
+ "react": "19.2.7",
17
+ "react-dom": "19.2.7",
18
+ "zod": "4.4.3"
19
+ },
20
+ "devDependencies": {
21
+ "@testing-library/jest-dom": "6.9.1",
22
+ "@testing-library/react": "16.3.2",
23
+ "@types/node": "24.13.2",
24
+ "@types/react": "19.2.17",
25
+ "@types/react-dom": "19.2.3",
26
+ "@vitejs/plugin-react": "6.0.2",
27
+ "jsdom": "29.1.1",
28
+ "typescript": "6.0.3",
29
+ "vite": "8.0.16",
30
+ "vitest": "4.1.9"
31
+ }
32
+ }
@@ -0,0 +1,72 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import {getLoadedConfig} from '@shipfox/client-config';
3
+ import type {ProviderProbeEntry} from '@shipfox/client-shell-fixture-feature';
4
+ import {QueryClient} from '@tanstack/react-query';
5
+ import {createMemoryHistory} from '@tanstack/react-router';
6
+ import {act, render, screen, waitFor} from '@testing-library/react';
7
+ import {createClientAppElement} from './main.js';
8
+ import {router} from './shipfox-app.gen.js';
9
+
10
+ test('proves the external composition contract', async () => {
11
+ window.matchMedia = vi.fn((query: string) => ({
12
+ matches: false,
13
+ media: query,
14
+ onchange: null,
15
+ addListener: vi.fn(),
16
+ removeListener: vi.fn(),
17
+ addEventListener: vi.fn(),
18
+ removeEventListener: vi.fn(),
19
+ dispatchEvent: vi.fn(() => false),
20
+ }));
21
+ window.scrollTo = vi.fn();
22
+ window.__SHIPFOX_CONFIG__ = undefined;
23
+ const missingConfig = render(createClientAppElement(), {reactStrictMode: true});
24
+
25
+ expect(await screen.findByRole('heading', {name: 'Configuration error'})).toBeVisible();
26
+ expect(screen.getByText('fixtureGreeting')).toBeVisible();
27
+ missingConfig.unmount();
28
+
29
+ window.__SHIPFOX_CONFIG__ = {FIXTURE_GREETING: 'Hello from the fixture'};
30
+ let providers: readonly ProviderProbeEntry[] = [];
31
+ router.update({
32
+ history: createMemoryHistory({initialEntries: ['/workspaces/workspace/insights']}),
33
+ });
34
+ render(
35
+ createClientAppElement({
36
+ onProviders: (entries) => {
37
+ providers = entries;
38
+ },
39
+ }),
40
+ {reactStrictMode: true},
41
+ );
42
+
43
+ expect(await screen.findByRole('heading', {name: 'Overridden insights'})).toBeVisible();
44
+ expect(screen.queryByRole('heading', {name: 'Toy insights'})).not.toBeInTheDocument();
45
+ expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
46
+ 'Insights first',
47
+ 'Insights second',
48
+ ]);
49
+ await waitFor(() => expect(providers.map(({id}) => id)).toEqual(['toy-feature', 'app-feature']));
50
+ const queryClients = providers.map(({queryClient}) => queryClient);
51
+ expect(queryClients).toHaveLength(2);
52
+ expect(queryClients[0]).toBeInstanceOf(QueryClient);
53
+ expect(queryClients[1]).toBe(queryClients[0]);
54
+ expect(getLoadedConfig<{fixtureGreeting: string}>().fixtureGreeting).toBe(
55
+ 'Hello from the fixture',
56
+ );
57
+
58
+ await act(() =>
59
+ router.navigate({
60
+ to: '/workspaces/$wid/settings/primary',
61
+ params: {wid: 'workspace'},
62
+ }),
63
+ );
64
+
65
+ expect(await screen.findByRole('heading', {name: 'Toy settings'})).toBeVisible();
66
+ expect(
67
+ Array.from(
68
+ screen.getByRole('navigation', {name: 'Workspace settings'}).querySelectorAll('a'),
69
+ (link) => link.textContent,
70
+ ),
71
+ ).toEqual(['Primary settings', 'Secondary settings']);
72
+ });
@@ -0,0 +1,12 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+
3
+ function OverriddenInsights() {
4
+ return <h1>Overridden insights</h1>;
5
+ }
6
+
7
+ export default defineRoute({
8
+ component: OverriddenInsights,
9
+ validateSearch: (search: Record<string, unknown>) => ({
10
+ view: search.view === 'compact' ? ('compact' as const) : ('full' as const),
11
+ }),
12
+ });
@@ -0,0 +1,20 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+ import {ProviderProbe} from '@shipfox/client-shell-fixture-feature';
3
+ import {createElement, type PropsWithChildren} from 'react';
4
+
5
+ function AppFeatureProvider({children}: PropsWithChildren) {
6
+ return createElement(ProviderProbe, {id: 'app-feature'}, children);
7
+ }
8
+
9
+ export const overrideFeature = defineClientFeature({
10
+ id: 'fixture.app-override',
11
+ routes: [
12
+ {
13
+ path: '/workspaces/$wid/insights',
14
+ parent: 'workspaceLayout',
15
+ override: true,
16
+ impl: './features/override-impl',
17
+ },
18
+ ],
19
+ providers: [{id: 'fixture-app-provider', Component: AppFeatureProvider}],
20
+ });
@@ -0,0 +1,15 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+ import {toyFeature} from '@shipfox/client-shell-fixture-feature';
3
+
4
+ const collisionFeature = defineClientFeature({
5
+ id: 'fixture.unapproved-collision',
6
+ routes: [
7
+ {
8
+ path: '/workspaces/$wid/insights',
9
+ parent: 'workspaceLayout',
10
+ impl: './features/override-impl',
11
+ },
12
+ ],
13
+ });
14
+
15
+ export const features = [toyFeature, collisionFeature];
@@ -0,0 +1,4 @@
1
+ import {toyFeature} from '@shipfox/client-shell-fixture-feature';
2
+ import {overrideFeature} from './features/override';
3
+
4
+ export const features = [toyFeature, overrideFeature];
@@ -0,0 +1,10 @@
1
+ import {Link, useSearch} from '@tanstack/react-router';
2
+
3
+ export function InsightsLink() {
4
+ const search = useSearch({from: '/workspaces/$wid/insights'});
5
+ return (
6
+ <Link to="/workspaces/$wid/insights" params={{wid: 'workspace'}} search={{view: search.view}}>
7
+ Insights
8
+ </Link>
9
+ );
10
+ }
@@ -0,0 +1,4 @@
1
+ import {createRoot} from 'react-dom/client';
2
+
3
+ const root = document.getElementById('root');
4
+ if (root) createRoot(root).render(<h1>Shipfox client runtime fixture</h1>);
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "ES2022",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "jsx": "react-jsx",
9
+ "skipLibCheck": true,
10
+ "verbatimModuleSyntax": true,
11
+ "noEmit": true,
12
+ "types": ["node", "vite/client", "vitest/globals"]
13
+ },
14
+ "include": ["src/main.tsx", "types.ts", "vite.config.ts"]
15
+ }
@@ -0,0 +1,10 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import {defineConfig} from 'vite';
3
+
4
+ export default defineConfig({
5
+ build: {chunkSizeWarningLimit: 4_000},
6
+ plugins: [react()],
7
+ resolve: {
8
+ dedupe: ['react', 'react-dom', '@tanstack/react-query', '@tanstack/react-router', 'jotai'],
9
+ },
10
+ });
@@ -0,0 +1,15 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import {defineConfig} from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ resolve: {
7
+ dedupe: ['react', 'react-dom', '@tanstack/react-query', '@tanstack/react-router', 'jotai'],
8
+ },
9
+ test: {
10
+ environment: 'jsdom',
11
+ globals: true,
12
+ include: ['src/app.fixture.tsx'],
13
+ server: {deps: {inline: true}},
14
+ },
15
+ });
@@ -0,0 +1,15 @@
1
+ # @shipfox/client-shell-fixture-feature
2
+
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3d064b8]
8
+ - Updated dependencies [6bc2e45]
9
+ - @shipfox/client-shell@0.2.0
10
+
11
+ ## 0.0.2
12
+
13
+ ### Patch Changes
14
+
15
+ - @shipfox/client-shell@0.0.2
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../@shipfox/biome/bin/biome-check.js" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../@shipfox/biome/bin/biome-check.js" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../@shipfox/biome/bin/biome-check.js" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../@shipfox/biome/bin/biome-check.js" "$@"
40
+ else
41
+ exec node "$basedir/../@shipfox/biome/bin/biome-check.js" "$@"
42
+ fi
43
+ # cmd-shim-target=/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/libs/client/shell/test/external/toy-feature/node_modules/@shipfox/biome/bin/biome-check.js
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../@shipfox/biome/bin/biome-format.js" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../@shipfox/biome/bin/biome-format.js" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../@shipfox/biome/bin/biome-format.js" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../@shipfox/biome/bin/biome-format.js" "$@"
40
+ else
41
+ exec node "$basedir/../@shipfox/biome/bin/biome-format.js" "$@"
42
+ fi
43
+ # cmd-shim-target=/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/libs/client/shell/test/external/toy-feature/node_modules/@shipfox/biome/bin/biome-format.js
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../@shipfox/biome/bin/biome-lint.js" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../@shipfox/biome/bin/biome-lint.js" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../@shipfox/biome/bin/biome-lint.js" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../@shipfox/biome/bin/biome-lint.js" "$@"
40
+ else
41
+ exec node "$basedir/../@shipfox/biome/bin/biome-lint.js" "$@"
42
+ fi
43
+ # cmd-shim-target=/Users/noe.charmet/conductor/workspaces/platform-v1/singapore/libs/client/shell/test/external/toy-feature/node_modules/@shipfox/biome/bin/biome-lint.js