@jskit-ai/shell-web 0.1.162 → 0.1.164

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 (46) hide show
  1. package/fixtures/adaptive-shell/src/ScreenPage.vue +1 -1
  2. package/package.json +8 -253
  3. package/patterns/application-shell/PATTERN.md +66 -0
  4. package/{templates → patterns/application-shell/example}/expected-existing/src/pages/home/index.vue +8 -8
  5. package/{templates → patterns/application-shell/example}/src/pages/home/index.vue +8 -8
  6. package/{templates → patterns/application-shell/example}/src/pages/home/settings/general/index.vue +1 -1
  7. package/{templates → patterns/application-shell/example}/src/pages/home/settings.vue +8 -8
  8. package/patterns/page-and-placement/PATTERN.md +76 -0
  9. package/patterns/page-and-placement/example/packages/main/src/client/providers/MainClientProvider.js +14 -0
  10. package/patterns/page-and-placement/example/src/components/SyncStatusElement.vue +16 -0
  11. package/patterns/page-and-placement/example/src/pages/home/reports/activity.vue +8 -0
  12. package/patterns/page-and-placement/example/src/pages/home/reports/index.vue +7 -0
  13. package/patterns/page-and-placement/example/src/pages/home/reports/overview.vue +8 -0
  14. package/patterns/page-and-placement/example/src/pages/home/reports.vue +33 -0
  15. package/patterns/page-and-placement/example/src/placement.js +52 -0
  16. package/patterns/page-and-placement/example/src/placementTopology.js +29 -0
  17. package/src/client/bootstrap/bootstrapPayloadHandlerRegistry.js +30 -42
  18. package/src/client/bootstrap/index.js +2 -3
  19. package/src/client/index.js +2 -11
  20. package/src/client/placement/runtime.js +9 -12
  21. package/src/client/providers/ShellWebClientProvider.js +95 -129
  22. package/src/client/requestRecovery/runtime.js +9 -29
  23. package/src/client/runtime/bootstrapRuntime.js +18 -29
  24. package/src/test/adaptiveShellSmoke.js +3 -9
  25. package/test/bootstrapRuntime.test.js +10 -61
  26. package/test/pageAndPlacementPattern.test.js +38 -0
  27. package/test/placementRuntime.test.js +36 -50
  28. package/test/playwrightContract.test.js +2 -12
  29. package/test/provider.test.js +126 -737
  30. package/test/settingsPlacementContract.test.js +75 -126
  31. package/src/server/support/localLinkItemScaffolds.js +0 -80
  32. package/test/bootstrapClaimContract.test.js +0 -76
  33. package/test/linkItemScaffoldContract.test.js +0 -248
  34. /package/{templates → patterns/application-shell/example}/expected-existing/src/App.vue +0 -0
  35. /package/{templates → patterns/application-shell/example}/expected-existing/src/pages/home.vue +0 -0
  36. /package/{templates → patterns/application-shell/example}/src/App.vue +0 -0
  37. /package/{templates → patterns/application-shell/example}/src/components/ShellLayout.vue +0 -0
  38. /package/{templates → patterns/application-shell/example}/src/components/menus/MenuLinkItem.vue +0 -0
  39. /package/{templates → patterns/application-shell/example}/src/components/menus/SurfaceAwareMenuLinkItem.vue +0 -0
  40. /package/{templates → patterns/application-shell/example}/src/components/menus/TabLinkItem.vue +0 -0
  41. /package/{templates → patterns/application-shell/example}/src/error.js +0 -0
  42. /package/{templates → patterns/application-shell/example}/src/pages/home/settings/index.vue +0 -0
  43. /package/{templates → patterns/application-shell/example}/src/pages/home.vue +0 -0
  44. /package/{templates → patterns/application-shell/example}/src/placement.js +0 -0
  45. /package/{templates → patterns/application-shell/example}/src/placementTopology.js +0 -0
  46. /package/{templates → patterns/application-shell/example}/tests/e2e/adaptive-shell.spec.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { registerBootstrapPayloadHandler } from "../src/client/bootstrap/bootstrapPayloadHandlerRegistry.js";
3
+ import { createBootstrapPayloadHandlerRegistry } from "../src/client/bootstrap/bootstrapPayloadHandlerRegistry.js";
4
4
  import { createShellBootstrapRuntime } from "../src/client/runtime/bootstrapRuntime.js";
5
5
 
6
6
  function createPlacementRuntime(initialContext = {}) {
@@ -32,52 +32,6 @@ function createPlacementRuntime(initialContext = {}) {
32
32
  };
33
33
  }
34
34
 
35
- function createAppDouble({ placementRuntime, realtimeSocket = null } = {}) {
36
- const singletons = new Map();
37
- const singletonInstances = new Map();
38
- return {
39
- singleton(token, factory) {
40
- singletons.set(token, factory);
41
- },
42
- tag(token, tagName) {
43
- const current = this._tags.get(tagName) || [];
44
- current.push(token);
45
- this._tags.set(tagName, current);
46
- },
47
- resolveTag(tagName) {
48
- return (this._tags.get(tagName) || []).map((token) => this.make(token));
49
- },
50
- _tags: new Map(),
51
- has(token) {
52
- if (token === "runtime.web-placement.client") {
53
- return true;
54
- }
55
- if (token === "runtime.realtime.client.socket") {
56
- return Boolean(realtimeSocket);
57
- }
58
- return singletons.has(token) || singletonInstances.has(token);
59
- },
60
- make(token) {
61
- if (token === "runtime.web-placement.client") {
62
- return placementRuntime;
63
- }
64
- if (token === "runtime.realtime.client.socket") {
65
- return realtimeSocket;
66
- }
67
- if (singletonInstances.has(token)) {
68
- return singletonInstances.get(token);
69
- }
70
- const factory = singletons.get(token);
71
- if (!factory) {
72
- throw new Error(`Unknown token ${String(token)}`);
73
- }
74
- const instance = factory(this);
75
- singletonInstances.set(token, instance);
76
- return instance;
77
- }
78
- };
79
- }
80
-
81
35
  test("shell bootstrap runtime refreshes /api/bootstrap on init and applies registered handlers", async () => {
82
36
  const placementRuntime = createPlacementRuntime({
83
37
  auth: {}
@@ -97,9 +51,8 @@ test("shell bootstrap runtime refreshes /api/bootstrap on init and applies regis
97
51
  const calls = [];
98
52
  const observedRequests = [];
99
53
  const observedResolveMeta = [];
100
- const app = createAppDouble({ placementRuntime });
101
- registerBootstrapPayloadHandler(app, "test.bootstrap.request", () =>
102
- Object.freeze({
54
+ const handlers = createBootstrapPayloadHandlerRegistry();
55
+ handlers.register(Object.freeze({
103
56
  handlerId: "test.bootstrap.request",
104
57
  order: -10,
105
58
  resolveBootstrapRequest() {
@@ -115,10 +68,8 @@ test("shell bootstrap runtime refreshes /api/bootstrap on init and applies regis
115
68
  applyBootstrapPayload({ request }) {
116
69
  observedRequests.push(request);
117
70
  }
118
- })
119
- );
120
- registerBootstrapPayloadHandler(app, "test.bootstrap.request-meta", () =>
121
- Object.freeze({
71
+ }));
72
+ handlers.register(Object.freeze({
122
73
  handlerId: "test.bootstrap.request-meta",
123
74
  order: -5,
124
75
  resolveBootstrapRequest({ request }) {
@@ -126,10 +77,8 @@ test("shell bootstrap runtime refreshes /api/bootstrap on init and applies regis
126
77
  return {};
127
78
  },
128
79
  applyBootstrapPayload() {}
129
- })
130
- );
131
- registerBootstrapPayloadHandler(app, "test.bootstrap.surfaceAccess", () =>
132
- Object.freeze({
80
+ }));
81
+ handlers.register(Object.freeze({
133
82
  handlerId: "test.bootstrap.surfaceAccess",
134
83
  order: 0,
135
84
  applyBootstrapPayload({ payload, placementRuntime: targetRuntime }) {
@@ -137,11 +86,11 @@ test("shell bootstrap runtime refreshes /api/bootstrap on init and applies regis
137
86
  surfaceAccess: payload.surfaceAccess || {}
138
87
  });
139
88
  }
140
- })
141
- );
89
+ }));
142
90
 
143
91
  const runtime = createShellBootstrapRuntime({
144
- app,
92
+ handlers,
93
+ placementRuntime,
145
94
  fetchImplementation: async (url) => {
146
95
  calls.push(String(url || ""));
147
96
  const payload = payloads.shift() || {};
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+ import { parse as parseSfc } from "@vue/compiler-sfc";
5
+
6
+ const patternRoot = new URL("../patterns/page-and-placement/example/", import.meta.url);
7
+ const vueFiles = [
8
+ "src/pages/home/reports.vue",
9
+ "src/pages/home/reports/index.vue",
10
+ "src/pages/home/reports/overview.vue",
11
+ "src/pages/home/reports/activity.vue",
12
+ "src/components/SyncStatusElement.vue"
13
+ ];
14
+ const jsFiles = [
15
+ "src/placement.js",
16
+ "src/placementTopology.js",
17
+ "packages/main/src/client/providers/MainClientProvider.js"
18
+ ];
19
+
20
+ test("page and placement pattern is executable, semantic application source", async () => {
21
+ const sources = [];
22
+ for (const relativePath of vueFiles) {
23
+ const source = await readFile(new URL(relativePath, patternRoot), "utf8");
24
+ sources.push(source);
25
+ const parsed = parseSfc(source, { filename: relativePath });
26
+ assert.equal(parsed.errors.length, 0, `${relativePath}: ${parsed.errors.join("\n")}`);
27
+ }
28
+ for (const relativePath of jsFiles) {
29
+ const source = await readFile(new URL(relativePath, patternRoot), "utf8");
30
+ sources.push(source);
31
+ assert.match(source, /(?:export|addPlacement|registerMainClientComponent)/u, relativePath);
32
+ }
33
+
34
+ const combined = sources.join("\n");
35
+ assert.match(combined, /createPlacementRegistry|addPlacementTopology|ShellOutlet/u);
36
+ assert.doesNotMatch(combined, /ui-generator|generated-ui|scaffold|receipt|provenance/u);
37
+ assert.doesNotMatch(combined, /replace this|ready for your/u);
38
+ });
@@ -3,22 +3,16 @@ import test from "node:test";
3
3
  import { definePlacement } from "../src/client/placement/validators.js";
4
4
  import { createWebPlacementRuntime } from "../src/client/placement/runtime.js";
5
5
 
6
- function createAppStub({ tokens = {}, contextContributors = [] } = {}) {
6
+ function createComponentsStub({ tokens = {} } = {}) {
7
7
  return {
8
8
  has(token) {
9
9
  return Object.prototype.hasOwnProperty.call(tokens, token);
10
10
  },
11
- make(token) {
11
+ get(token) {
12
12
  if (!this.has(token)) {
13
13
  throw new Error(`Unknown token: ${String(token)}`);
14
14
  }
15
15
  return tokens[token];
16
- },
17
- resolveTag(tagName) {
18
- if (tagName === "web-placement.context.client") {
19
- return contextContributors;
20
- }
21
- return [];
22
16
  }
23
17
  };
24
18
  }
@@ -60,14 +54,14 @@ function semanticTopologyEntry({
60
54
  }
61
55
 
62
56
  test("web placement runtime resolves semantic targets through topology variants", () => {
63
- const app = createAppStub({
57
+ const components = createComponentsStub({
64
58
  tokens: {
65
59
  "component.bottom": () => null,
66
60
  "component.menu": () => null
67
61
  }
68
62
  });
69
63
 
70
- const runtime = createWebPlacementRuntime({ app });
64
+ const runtime = createWebPlacementRuntime({ components });
71
65
  runtime.replacePlacementTopology([
72
66
  semanticTopologyEntry({
73
67
  id: "shell.primary-nav",
@@ -109,13 +103,13 @@ test("web placement runtime resolves semantic targets through topology variants"
109
103
  });
110
104
 
111
105
  test("web placement runtime accepts append-only topology objects", () => {
112
- const app = createAppStub({
106
+ const components = createComponentsStub({
113
107
  tokens: {
114
108
  "component.menu": () => null
115
109
  }
116
110
  });
117
111
 
118
- const runtime = createWebPlacementRuntime({ app });
112
+ const runtime = createWebPlacementRuntime({ components });
119
113
  runtime.replacePlacementTopology({
120
114
  placements: [
121
115
  semanticTopologyEntry({
@@ -146,7 +140,7 @@ test("web placement runtime accepts append-only topology objects", () => {
146
140
  });
147
141
 
148
142
  test("web placement runtime filters by surface/host/position, resolves component tokens, and sorts by order", () => {
149
- const app = createAppStub({
143
+ const components = createComponentsStub({
150
144
  tokens: {
151
145
  "component.alerts": () => null,
152
146
  "component.profile": () => null,
@@ -154,7 +148,7 @@ test("web placement runtime filters by surface/host/position, resolves component
154
148
  }
155
149
  });
156
150
 
157
- const runtime = createWebPlacementRuntime({ app });
151
+ const runtime = createWebPlacementRuntime({ components });
158
152
  runtime.replacePlacements([
159
153
  definePlacement({
160
154
  id: "test.menu",
@@ -195,14 +189,14 @@ test("web placement runtime filters by surface/host/position, resolves component
195
189
  });
196
190
 
197
191
  test("web placement runtime preserves source order when placements share the same order", () => {
198
- const app = createAppStub({
192
+ const components = createComponentsStub({
199
193
  tokens: {
200
194
  "component.beta": () => null,
201
195
  "component.alpha": () => null
202
196
  }
203
197
  });
204
198
 
205
- const runtime = createWebPlacementRuntime({ app });
199
+ const runtime = createWebPlacementRuntime({ components });
206
200
  runtime.replacePlacements([
207
201
  definePlacement({
208
202
  id: "test.beta",
@@ -228,19 +222,19 @@ test("web placement runtime preserves source order when placements share the sam
228
222
  });
229
223
 
230
224
  test("web placement runtime applies context contributors and placement when() predicates", () => {
231
- const app = createAppStub({
225
+ const components = createComponentsStub({
232
226
  tokens: {
233
227
  "component.guest": () => null,
234
228
  "component.authenticated": () => null
235
- },
229
+ }
230
+ });
231
+
232
+ const runtime = createWebPlacementRuntime({
233
+ components,
236
234
  contextContributors: [
237
- () => ({
238
- auth: { authenticated: true }
239
- })
235
+ () => ({ auth: { authenticated: true } })
240
236
  ]
241
237
  });
242
-
243
- const runtime = createWebPlacementRuntime({ app });
244
238
  runtime.replacePlacements([
245
239
  definePlacement({
246
240
  id: "guest.item",
@@ -265,20 +259,18 @@ test("web placement runtime applies context contributors and placement when() pr
265
259
  });
266
260
 
267
261
  test("web placement runtime uses runtime context and local context overrides contributor values", () => {
268
- const app = createAppStub({
262
+ const components = createComponentsStub({
269
263
  tokens: {
270
264
  "component.allowed": () => null
271
- },
265
+ }
266
+ });
267
+
268
+ const runtime = createWebPlacementRuntime({
269
+ components,
272
270
  contextContributors: [
273
- () => ({
274
- auth: {
275
- authenticated: false
276
- }
277
- })
271
+ () => ({ auth: { authenticated: false } })
278
272
  ]
279
273
  });
280
-
281
- const runtime = createWebPlacementRuntime({ app });
282
274
  runtime.replacePlacements([
283
275
  definePlacement({
284
276
  id: "allowed",
@@ -311,8 +303,8 @@ test("web placement runtime uses runtime context and local context overrides con
311
303
  });
312
304
 
313
305
  test("web placement runtime notifies subscribers on placement and context updates", () => {
314
- const app = createAppStub();
315
- const runtime = createWebPlacementRuntime({ app });
306
+ const components = createComponentsStub();
307
+ const runtime = createWebPlacementRuntime({ components });
316
308
  const events = [];
317
309
  const unsubscribe = runtime.subscribe((event) => {
318
310
  events.push(event.type);
@@ -336,8 +328,8 @@ test("web placement runtime notifies subscribers on placement and context update
336
328
  });
337
329
 
338
330
  test("web placement runtime rejects duplicate placement ids", () => {
339
- const app = createAppStub();
340
- const runtime = createWebPlacementRuntime({ app });
331
+ const components = createComponentsStub();
332
+ const runtime = createWebPlacementRuntime({ components });
341
333
 
342
334
  assert.throws(() => {
343
335
  runtime.replacePlacements([
@@ -360,11 +352,11 @@ test("web placement runtime rejects duplicate placement ids", () => {
360
352
  });
361
353
 
362
354
  test("web placement runtime skips throwing component tokens and logs resolution errors once", () => {
363
- const app = {
355
+ const components = {
364
356
  has(token) {
365
357
  return token === "component.bad" || token === "component.good";
366
358
  },
367
- make(token) {
359
+ get(token) {
368
360
  if (token === "component.bad") {
369
361
  throw new Error("bad component token");
370
362
  }
@@ -372,15 +364,12 @@ test("web placement runtime skips throwing component tokens and logs resolution
372
364
  return () => null;
373
365
  }
374
366
  throw new Error(`Unknown token: ${String(token)}`);
375
- },
376
- resolveTag() {
377
- return [];
378
367
  }
379
368
  };
380
369
 
381
370
  const errors = [];
382
371
  const runtime = createWebPlacementRuntime({
383
- app,
372
+ components,
384
373
  logger: {
385
374
  warn() {},
386
375
  error(payload, message) {
@@ -417,11 +406,11 @@ test("web placement runtime skips throwing component tokens and logs resolution
417
406
 
418
407
  test("web placement runtime clears failed token cache when placements are replaced", () => {
419
408
  let shouldThrow = true;
420
- const app = {
409
+ const components = {
421
410
  has(token) {
422
411
  return token === "component.toggle";
423
412
  },
424
- make(token) {
413
+ get(token) {
425
414
  if (token !== "component.toggle") {
426
415
  throw new Error(`Unknown token: ${String(token)}`);
427
416
  }
@@ -429,14 +418,11 @@ test("web placement runtime clears failed token cache when placements are replac
429
418
  throw new Error("toggle failure");
430
419
  }
431
420
  return () => null;
432
- },
433
- resolveTag() {
434
- return [];
435
421
  }
436
422
  };
437
423
 
438
424
  const runtime = createWebPlacementRuntime({
439
- app,
425
+ components,
440
426
  logger: {
441
427
  warn() {},
442
428
  error() {}
@@ -475,14 +461,14 @@ test("web placement runtime clears failed token cache when placements are replac
475
461
  });
476
462
 
477
463
  test("web placement runtime follows explicit surface targeting without role indirection", () => {
478
- const app = createAppStub({
464
+ const components = createComponentsStub({
479
465
  tokens: {
480
466
  "component.global": () => null,
481
467
  "component.app": () => null,
482
468
  "component.admin": () => null
483
469
  }
484
470
  });
485
- const runtime = createWebPlacementRuntime({ app });
471
+ const runtime = createWebPlacementRuntime({ components });
486
472
  runtime.replacePlacements([
487
473
  definePlacement({
488
474
  id: "global.banner",
@@ -9,17 +9,6 @@ import {
9
9
 
10
10
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
11
 
12
- test("shell-web owns the exact Playwright dependency used by its generated smoke", async () => {
13
- const packageJson = JSON.parse(await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
14
- const rootPackage = JSON.parse(await readFile(path.resolve(PACKAGE_ROOT, "../../package.json"), "utf8"));
15
-
16
- assert.equal(packageJson.jskit.mutations.dependencies.dev["@playwright/test"], "1.61.1");
17
- assert.equal(
18
- packageJson.jskit.mutations.dependencies.dev["@playwright/test"],
19
- rootPackage.devDependencies["@playwright/test"]
20
- );
21
- });
22
-
23
12
  test("adaptive shell smoke navigates through Playwright baseURL", async () => {
24
13
  const source = await readFile(path.join(PACKAGE_ROOT, "src/test/adaptiveShellSmoke.js"), "utf8");
25
14
 
@@ -69,7 +58,8 @@ test("adaptive shell smoke follows rendered layout state and waits for drawer tr
69
58
  assert.match(source, /data-rail-width/u);
70
59
  assert.match(source, /expect\.poll/u);
71
60
  assert.match(source, /toBeFocused/u);
72
- assert.match(source, /v-navigation-drawer__scrim/u);
61
+ assert.match(source, /getByRole\("button", \{ name: "Close navigation menu" \}\)/u);
62
+ assert.doesNotMatch(source, /v-navigation-drawer__scrim/u);
73
63
  assert.match(source, /page\.keyboard\.press\("Escape"\)[\s\S]*data-presentation", "drawer"/u);
74
64
  assert.match(source, /centers\.push\(box\.x \+ box\.width \/ 2\)/u);
75
65
  assert.match(source, /fit\.endGap - configuredSpacing/u);