@jskit-ai/kernel 0.1.244 → 0.1.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/appConfig.js +2 -1
- package/client/asyncModuleRecovery.js +2 -4
- package/client/index.d.ts +18 -76
- package/client/index.js +7 -2
- package/client/logging.js +8 -2
- package/client/moduleBootstrap.js +41 -33
- package/client/moduleBootstrap.test.js +35 -0
- package/client/shellBootstrap.js +38 -10
- package/client/shellBootstrap.test.js +85 -6
- package/client/shellRouting.js +16 -61
- package/client/shellRouting.test.js +30 -1
- package/client/vite/clientBootstrapPlugin.js +0 -6
- package/client/vite/clientBootstrapPlugin.test.js +2 -3
- package/internal/node/installedPackages.js +14 -58
- package/package.json +2 -4
- package/server/actions/actionCatalogue.js +13 -34
- package/{shared → server}/actions/actionContributorHelpers.js +0 -5
- package/{shared → server}/actions/actionDefinitions.js +24 -131
- package/{shared → server}/actions/actionDefinitions.test.js +75 -21
- package/server/actions/actionEvents.js +97 -2
- package/server/actions/actionEvents.test.js +79 -0
- package/{shared → server}/actions/executionContext.js +3 -13
- package/server/actions/idempotency.js +24 -0
- package/server/actions/pipeline.js +87 -0
- package/{shared → server}/actions/policies.js +20 -33
- package/{shared → server}/actions/policies.test.js +6 -6
- package/{shared → server}/actions/registry.js +3 -22
- package/{shared → server}/actions/registry.test.js +70 -32
- package/server/actions/requestMeta.js +17 -0
- package/server/http/capabilityHttpRuntime.js +1 -1
- package/server/http/capabilityHttpRuntime.test.js +56 -0
- package/server/http/errorHandling.js +291 -0
- package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
- package/server/http/lib/routeRegistration.js +36 -150
- package/server/http/lib/routeTransport.js +2 -0
- package/server/http/lib/routeValidator.js +5 -47
- package/server/http/lib/routeValidator.test.js +15 -17
- package/server/http/lib/router.js +10 -94
- package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
- package/server/platform/providerRuntime/helpers.js +1 -34
- package/server/runtime/integers.js +1 -5
- package/server/runtime/serviceAuthorization.js +4 -9
- package/server/support/appConfig.js +2 -1
- package/server/support/appConfigFiles.js +1 -38
- package/server/support/appConfigFiles.test.js +3 -39
- package/server/support/index.js +1 -20
- package/shared/capabilities/defineProvider.js +1 -1
- package/shared/capabilities/index.js +1 -1
- package/shared/index.js +1 -1
- package/shared/support/index.js +1 -4
- package/shared/support/mobileConfig.js +99 -0
- package/shared/support/mobileConfig.test.js +113 -0
- package/shared/support/normalize.js +0 -108
- package/shared/support/normalize.test.js +0 -134
- package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
- package/shared/support/returnToPath.js +16 -20
- package/shared/support/returnToPath.test.js +5 -33
- package/shared/support/shellLayoutTargets.js +2 -140
- package/shared/support/shellLayoutTargets.test.js +1 -110
- package/shared/support/sorting.js +1 -9
- package/shared/surface/apiPaths.js +1 -82
- package/shared/surface/paths.js +38 -171
- package/shared/surface/registry.js +2 -1
- package/shared/surface/runtime.js +18 -109
- package/shared/surface/runtime.test.js +57 -0
- package/shared/validators/index.js +1 -4
- package/shared/validators/jsonRestSchemaSupport.js +0 -9
- package/shared/validators/recordIdParamsValidator.js +0 -6
- package/shared/validators/schemaDefinitions.js +4 -30
- package/shared/validators/schemaPayloadValidation.js +2 -6
- package/test/barrelExposure.test.js +5 -8
- package/test/exportsContract.test.js +1 -3
- package/client/mobileLaunchRouting.js +0 -231
- package/client/mobileLaunchRouting.test.js +0 -230
- package/server/http/lib/controller.js +0 -183
- package/server/http/lib/controller.test.js +0 -143
- package/server/http/lib/index.js +0 -5
- package/server/runtime/canonicalJson.js +0 -74
- package/server/runtime/entityChangeEvents.js +0 -307
- package/server/runtime/entityChangeEvents.test.js +0 -274
- package/server/runtime/fastifyBootstrap.js +0 -553
- package/server/runtime/moduleConfig.js +0 -223
- package/server/runtime/moduleConfig.test.js +0 -165
- package/server/runtime/pagination.js +0 -13
- package/server/runtime/realtimeNormalization.js +0 -21
- package/server/runtime/routeUtils.js +0 -20
- package/server/runtime/securityAudit.js +0 -269
- package/server/runtime/securityAudit.test.js +0 -41
- package/server/support/pageTargets.js +0 -772
- package/server/support/pageTargets.test.js +0 -659
- package/server/support/shellOutlets.js +0 -576
- package/server/support/shellOutlets.test.js +0 -336
- package/server/surface/index.js +0 -18
- package/shared/actions/audit.js +0 -7
- package/shared/actions/idempotency.js +0 -62
- package/shared/actions/observability.js +0 -10
- package/shared/actions/pipeline.js +0 -309
- package/shared/actions/requestMeta.js +0 -36
- package/shared/actions/textNormalization.js +0 -3
- package/shared/actions/withActionDefaults.js +0 -34
- /package/{shared → server}/actions/executionContext.test.js +0 -0
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import {
|
|
7
|
-
deriveDefaultSubpagesHost,
|
|
8
|
-
normalizePagesRelativeTargetRoot,
|
|
9
|
-
resolvePageLinkTargetDetails,
|
|
10
|
-
resolvePageTargetDetails
|
|
11
|
-
} from "./pageTargets.js";
|
|
12
|
-
|
|
13
|
-
async function withTempApp(run) {
|
|
14
|
-
const appRoot = await mkdtemp(path.join(tmpdir(), "kernel-page-targets-"));
|
|
15
|
-
try {
|
|
16
|
-
return await run(appRoot);
|
|
17
|
-
} finally {
|
|
18
|
-
await rm(appRoot, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function writeFileInApp(appRoot, relativePath, source) {
|
|
23
|
-
const absoluteFile = path.join(appRoot, relativePath);
|
|
24
|
-
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
25
|
-
await writeFile(absoluteFile, source, "utf8");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function writeConfig(appRoot, source) {
|
|
29
|
-
await writeFileInApp(appRoot, "config/public.js", source);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function writeShellLayout(appRoot, source = "") {
|
|
33
|
-
await writeFileInApp(
|
|
34
|
-
appRoot,
|
|
35
|
-
"src/components/ShellLayout.vue",
|
|
36
|
-
source ||
|
|
37
|
-
`<template>
|
|
38
|
-
<div>
|
|
39
|
-
<ShellOutlet target="shell-layout:top-right" />
|
|
40
|
-
<ShellOutlet
|
|
41
|
-
target="shell-layout:primary-menu"
|
|
42
|
-
default
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
</template>
|
|
46
|
-
`
|
|
47
|
-
);
|
|
48
|
-
await writePlacementTopology(appRoot);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
|
|
52
|
-
const rendererLines = linkRenderer
|
|
53
|
-
? `,
|
|
54
|
-
renderers: {
|
|
55
|
-
link: "${linkRenderer}"
|
|
56
|
-
}`
|
|
57
|
-
: "";
|
|
58
|
-
return `{
|
|
59
|
-
outlet: "${outlet}"${rendererLines}
|
|
60
|
-
}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function renderTopologyEntry({
|
|
64
|
-
id = "",
|
|
65
|
-
owner = "",
|
|
66
|
-
surfaces = ["*"],
|
|
67
|
-
defaultPlacement = false,
|
|
68
|
-
outlet = "",
|
|
69
|
-
linkRenderer = ""
|
|
70
|
-
} = {}) {
|
|
71
|
-
const ownerLine = owner ? ` owner: "${owner}",\n` : "";
|
|
72
|
-
const defaultLine = defaultPlacement ? " default: true,\n" : "";
|
|
73
|
-
return ` {
|
|
74
|
-
id: "${id}",
|
|
75
|
-
${ownerLine} surfaces: ${JSON.stringify(surfaces)},
|
|
76
|
-
${defaultLine} variants: {
|
|
77
|
-
compact: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
78
|
-
medium: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
79
|
-
expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
|
|
80
|
-
}
|
|
81
|
-
}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function writePlacementTopology(appRoot, entries = []) {
|
|
85
|
-
const defaultEntries = [
|
|
86
|
-
renderTopologyEntry({
|
|
87
|
-
id: "shell.primary-nav",
|
|
88
|
-
surfaces: ["*"],
|
|
89
|
-
defaultPlacement: true,
|
|
90
|
-
outlet: "shell-layout:primary-menu",
|
|
91
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
92
|
-
}),
|
|
93
|
-
renderTopologyEntry({
|
|
94
|
-
id: "shell.status",
|
|
95
|
-
surfaces: ["*"],
|
|
96
|
-
outlet: "shell-layout:top-right",
|
|
97
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
98
|
-
})
|
|
99
|
-
];
|
|
100
|
-
await writeFileInApp(
|
|
101
|
-
appRoot,
|
|
102
|
-
"src/placementTopology.js",
|
|
103
|
-
`export default {
|
|
104
|
-
placements: [
|
|
105
|
-
${[...defaultEntries, ...entries].join(",\n")}
|
|
106
|
-
]
|
|
107
|
-
};
|
|
108
|
-
`
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
test("resolvePageTargetDetails derives the surface and route data from an explicit page file", async () => {
|
|
113
|
-
await withTempApp(async (appRoot) => {
|
|
114
|
-
await writeConfig(
|
|
115
|
-
appRoot,
|
|
116
|
-
`export const config = {
|
|
117
|
-
surfaceDefinitions: {
|
|
118
|
-
admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
`
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const pageTarget = await resolvePageTargetDetails({
|
|
125
|
-
appRoot,
|
|
126
|
-
targetFile: "w/[workspaceSlug]/admin/catalog/index/products/index.vue",
|
|
127
|
-
context: "page target"
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
assert.equal(pageTarget.surfaceId, "admin");
|
|
131
|
-
assert.equal(pageTarget.surfacePagesRoot, "w/[workspaceSlug]/admin");
|
|
132
|
-
assert.equal(pageTarget.routeUrlSuffix, "/catalog/products");
|
|
133
|
-
assert.equal(pageTarget.placementId, "ui-generator.page.admin.catalog.products.link");
|
|
134
|
-
assert.deepEqual(pageTarget.visibleRouteSegments, ["catalog", "products"]);
|
|
135
|
-
assert.equal(deriveDefaultSubpagesHost(pageTarget), "catalog-products");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("resolvePageTargetDetails includes surface in placement ids for identical routes on different surfaces", async () => {
|
|
140
|
-
await withTempApp(async (appRoot) => {
|
|
141
|
-
await writeConfig(
|
|
142
|
-
appRoot,
|
|
143
|
-
`export const config = {
|
|
144
|
-
surfaceDefinitions: {
|
|
145
|
-
app: { id: "app", pagesRoot: "app", enabled: true },
|
|
146
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
`
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const appPageTarget = await resolvePageTargetDetails({
|
|
153
|
-
appRoot,
|
|
154
|
-
targetFile: "app/reports/index.vue",
|
|
155
|
-
context: "page target"
|
|
156
|
-
});
|
|
157
|
-
const adminPageTarget = await resolvePageTargetDetails({
|
|
158
|
-
appRoot,
|
|
159
|
-
targetFile: "admin/reports/index.vue",
|
|
160
|
-
context: "page target"
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
assert.equal(appPageTarget.placementId, "ui-generator.page.app.reports.link");
|
|
164
|
-
assert.equal(adminPageTarget.placementId, "ui-generator.page.admin.reports.link");
|
|
165
|
-
assert.notEqual(appPageTarget.placementId, adminPageTarget.placementId);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("resolvePageTargetDetails chooses the most specific matching surface pagesRoot", async () => {
|
|
170
|
-
await withTempApp(async (appRoot) => {
|
|
171
|
-
await writeConfig(
|
|
172
|
-
appRoot,
|
|
173
|
-
`export const config = {
|
|
174
|
-
surfaceDefinitions: {
|
|
175
|
-
app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true },
|
|
176
|
-
admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
`
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const pageTarget = await resolvePageTargetDetails({
|
|
183
|
-
appRoot,
|
|
184
|
-
targetFile: "w/[workspaceSlug]/admin/catalog/index.vue",
|
|
185
|
-
context: "page target"
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
assert.equal(pageTarget.surfaceId, "admin");
|
|
189
|
-
assert.equal(pageTarget.surfacePagesRoot, "w/[workspaceSlug]/admin");
|
|
190
|
-
assert.equal(pageTarget.surfaceRelativeFilePath, "catalog/index.vue");
|
|
191
|
-
assert.equal(pageTarget.routeUrlSuffix, "/catalog");
|
|
192
|
-
assert.equal(pageTarget.placementId, "ui-generator.page.admin.catalog.link");
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("resolvePageTargetDetails derives surface auth requirement from surface access policy", async () => {
|
|
197
|
-
await withTempApp(async (appRoot) => {
|
|
198
|
-
await writeConfig(
|
|
199
|
-
appRoot,
|
|
200
|
-
`export const config = {
|
|
201
|
-
surfaceAccessPolicies: {
|
|
202
|
-
public: {},
|
|
203
|
-
authenticated: {
|
|
204
|
-
requireAuth: true
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
surfaceDefinitions: {
|
|
208
|
-
home: { id: "home", pagesRoot: "home", enabled: true, accessPolicyId: "public" },
|
|
209
|
-
app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
`
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const publicPageTarget = await resolvePageTargetDetails({
|
|
216
|
-
appRoot,
|
|
217
|
-
targetFile: "home/index.vue",
|
|
218
|
-
context: "page target"
|
|
219
|
-
});
|
|
220
|
-
const authenticatedPageTarget = await resolvePageTargetDetails({
|
|
221
|
-
appRoot,
|
|
222
|
-
targetFile: "app/index.vue",
|
|
223
|
-
context: "page target"
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
assert.equal(publicPageTarget.surfaceRequiresAuth, false);
|
|
227
|
-
assert.equal(authenticatedPageTarget.surfaceRequiresAuth, true);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("resolvePageTargetDetails rejects duplicate matching surface pagesRoot definitions", async () => {
|
|
232
|
-
await withTempApp(async (appRoot) => {
|
|
233
|
-
await writeConfig(
|
|
234
|
-
appRoot,
|
|
235
|
-
`export const config = {
|
|
236
|
-
surfaceDefinitions: {
|
|
237
|
-
adminA: { id: "admin-a", pagesRoot: "w/[workspaceSlug]/admin", enabled: true },
|
|
238
|
-
adminB: { id: "admin-b", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
`
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
await assert.rejects(
|
|
245
|
-
() =>
|
|
246
|
-
resolvePageTargetDetails({
|
|
247
|
-
appRoot,
|
|
248
|
-
targetFile: "w/[workspaceSlug]/admin/catalog/index.vue",
|
|
249
|
-
context: "page target"
|
|
250
|
-
}),
|
|
251
|
-
/multiple surfaces share pagesRoot "w\/\[workspaceSlug\]\/admin" \(admin-a, admin-b\)/
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("resolvePageTargetDetails accepts target files with a src/pages prefix", async () => {
|
|
257
|
-
await withTempApp(async (appRoot) => {
|
|
258
|
-
await writeConfig(
|
|
259
|
-
appRoot,
|
|
260
|
-
`export const config = {
|
|
261
|
-
surfaceDefinitions: {
|
|
262
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
`
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const details = await resolvePageTargetDetails({
|
|
269
|
-
appRoot,
|
|
270
|
-
targetFile: "src/pages/admin/reports/index.vue",
|
|
271
|
-
context: "page target"
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
assert.equal(details.targetFilePath.relativePath, "src/pages/admin/reports/index.vue");
|
|
275
|
-
assert.equal(details.surfaceId, "admin");
|
|
276
|
-
assert.equal(details.routeUrlSuffix, "/reports");
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("normalizePagesRelativeTargetRoot accepts route roots with a src/pages prefix", () => {
|
|
281
|
-
assert.equal(
|
|
282
|
-
normalizePagesRelativeTargetRoot("src/pages/admin/customers", {
|
|
283
|
-
context: "crud-ui-generator",
|
|
284
|
-
label: 'option "target-root"'
|
|
285
|
-
}),
|
|
286
|
-
"src/pages/admin/customers"
|
|
287
|
-
);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("resolvePageLinkTargetDetails falls back to the app default placement target", async () => {
|
|
291
|
-
await withTempApp(async (appRoot) => {
|
|
292
|
-
await writeConfig(
|
|
293
|
-
appRoot,
|
|
294
|
-
`export const config = {
|
|
295
|
-
surfaceDefinitions: {
|
|
296
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
`
|
|
300
|
-
);
|
|
301
|
-
await writeShellLayout(appRoot);
|
|
302
|
-
|
|
303
|
-
const details = await resolvePageLinkTargetDetails({
|
|
304
|
-
appRoot,
|
|
305
|
-
targetFile: "admin/reports/index.vue",
|
|
306
|
-
context: "page target"
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
assert.equal(details.pageTarget.surfaceId, "admin");
|
|
310
|
-
assert.equal(details.placementTarget.id, "shell.primary-nav");
|
|
311
|
-
assert.equal(details.componentToken, "");
|
|
312
|
-
assert.equal(details.linkTo, "");
|
|
313
|
-
assert.equal(details.whenLine, "");
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("resolvePageLinkTargetDetails emits an auth guard when the surface policy requires auth", async () => {
|
|
318
|
-
await withTempApp(async (appRoot) => {
|
|
319
|
-
await writeConfig(
|
|
320
|
-
appRoot,
|
|
321
|
-
`export const config = {
|
|
322
|
-
surfaceAccessPolicies: {
|
|
323
|
-
authenticated: {
|
|
324
|
-
requireAuth: true
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
surfaceDefinitions: {
|
|
328
|
-
app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
`
|
|
332
|
-
);
|
|
333
|
-
await writeShellLayout(appRoot);
|
|
334
|
-
|
|
335
|
-
const details = await resolvePageLinkTargetDetails({
|
|
336
|
-
appRoot,
|
|
337
|
-
targetFile: "app/reports/index.vue",
|
|
338
|
-
context: "page target"
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
assert.equal(details.whenLine, " when: ({ auth }) => auth?.authenticated === true\n");
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test("resolvePageLinkTargetDetails prefers an outlet-declared default link token over subpage heuristics", async () => {
|
|
346
|
-
await withTempApp(async (appRoot) => {
|
|
347
|
-
await writeConfig(
|
|
348
|
-
appRoot,
|
|
349
|
-
`export const config = {
|
|
350
|
-
surfaceDefinitions: {
|
|
351
|
-
home: { id: "home", pagesRoot: "home", enabled: true }
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
`
|
|
355
|
-
);
|
|
356
|
-
await writePlacementTopology(appRoot, [
|
|
357
|
-
renderTopologyEntry({
|
|
358
|
-
id: "page.section-nav",
|
|
359
|
-
owner: "home-settings",
|
|
360
|
-
surfaces: ["home"],
|
|
361
|
-
outlet: "home-settings:primary-menu",
|
|
362
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
363
|
-
})
|
|
364
|
-
]);
|
|
365
|
-
await writeFileInApp(
|
|
366
|
-
appRoot,
|
|
367
|
-
"src/pages/home/settings.vue",
|
|
368
|
-
`<template>
|
|
369
|
-
<section>
|
|
370
|
-
<ShellOutlet target="home-settings:primary-menu" />
|
|
371
|
-
<RouterView />
|
|
372
|
-
</section>
|
|
373
|
-
</template>
|
|
374
|
-
`
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const details = await resolvePageLinkTargetDetails({
|
|
378
|
-
appRoot,
|
|
379
|
-
targetFile: "home/settings/pollen-types/index.vue",
|
|
380
|
-
context: "page target"
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
assert.equal(details.parentHost?.id, "home-settings:primary-menu");
|
|
384
|
-
assert.equal(details.placementTarget.id, "page.section-nav");
|
|
385
|
-
assert.equal(details.placementTarget.owner, "home-settings");
|
|
386
|
-
assert.equal(details.componentToken, "");
|
|
387
|
-
assert.equal(details.linkTo, "");
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
test("resolvePageLinkTargetDetails infers owner-scoped placement for sibling file-route children", async () => {
|
|
392
|
-
await withTempApp(async (appRoot) => {
|
|
393
|
-
await writeConfig(
|
|
394
|
-
appRoot,
|
|
395
|
-
`export const config = {
|
|
396
|
-
surfaceDefinitions: {
|
|
397
|
-
home: { id: "home", pagesRoot: "home", enabled: true }
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
`
|
|
401
|
-
);
|
|
402
|
-
await writePlacementTopology(appRoot, [
|
|
403
|
-
renderTopologyEntry({
|
|
404
|
-
id: "page.section-nav",
|
|
405
|
-
owner: "home-settings",
|
|
406
|
-
surfaces: ["home"],
|
|
407
|
-
outlet: "home-settings:primary-menu",
|
|
408
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
409
|
-
})
|
|
410
|
-
]);
|
|
411
|
-
await writeFileInApp(
|
|
412
|
-
appRoot,
|
|
413
|
-
"src/pages/home/settings.vue",
|
|
414
|
-
`<template>
|
|
415
|
-
<section>
|
|
416
|
-
<ShellOutlet target="home-settings:primary-menu" />
|
|
417
|
-
<RouterView />
|
|
418
|
-
</section>
|
|
419
|
-
</template>
|
|
420
|
-
`
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
const details = await resolvePageLinkTargetDetails({
|
|
424
|
-
appRoot,
|
|
425
|
-
targetFile: "home/settings/profile.vue",
|
|
426
|
-
context: "page target"
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
assert.equal(details.parentHost?.id, "home-settings:primary-menu");
|
|
430
|
-
assert.equal(details.placementTarget.id, "page.section-nav");
|
|
431
|
-
assert.equal(details.placementTarget.owner, "home-settings");
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
test("resolvePageLinkTargetDetails prefers owner-scoped topology over a global mapping for the same concrete outlet", async () => {
|
|
436
|
-
await withTempApp(async (appRoot) => {
|
|
437
|
-
await writeConfig(
|
|
438
|
-
appRoot,
|
|
439
|
-
`export const config = {
|
|
440
|
-
surfaceDefinitions: {
|
|
441
|
-
home: { id: "home", pagesRoot: "home", enabled: true }
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
`
|
|
445
|
-
);
|
|
446
|
-
await writePlacementTopology(appRoot, [
|
|
447
|
-
renderTopologyEntry({
|
|
448
|
-
id: "page.section-nav",
|
|
449
|
-
surfaces: ["home"],
|
|
450
|
-
outlet: "home-settings:primary-menu",
|
|
451
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
452
|
-
}),
|
|
453
|
-
renderTopologyEntry({
|
|
454
|
-
id: "page.section-nav",
|
|
455
|
-
owner: "home-settings",
|
|
456
|
-
surfaces: ["home"],
|
|
457
|
-
outlet: "home-settings:primary-menu",
|
|
458
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
459
|
-
})
|
|
460
|
-
]);
|
|
461
|
-
await writeFileInApp(
|
|
462
|
-
appRoot,
|
|
463
|
-
"src/pages/home/settings.vue",
|
|
464
|
-
`<template>
|
|
465
|
-
<section>
|
|
466
|
-
<ShellOutlet target="home-settings:primary-menu" />
|
|
467
|
-
<RouterView />
|
|
468
|
-
</section>
|
|
469
|
-
</template>
|
|
470
|
-
`
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
const details = await resolvePageLinkTargetDetails({
|
|
474
|
-
appRoot,
|
|
475
|
-
targetFile: "home/settings/profile.vue",
|
|
476
|
-
context: "page target"
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
assert.equal(details.placementTarget.id, "page.section-nav");
|
|
480
|
-
assert.equal(details.placementTarget.owner, "home-settings");
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test("resolvePageLinkTargetDetails rejects ambiguous semantic mappings for the same owner outlet", async () => {
|
|
485
|
-
await withTempApp(async (appRoot) => {
|
|
486
|
-
await writeConfig(
|
|
487
|
-
appRoot,
|
|
488
|
-
`export const config = {
|
|
489
|
-
surfaceDefinitions: {
|
|
490
|
-
home: { id: "home", pagesRoot: "home", enabled: true }
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
`
|
|
494
|
-
);
|
|
495
|
-
await writePlacementTopology(appRoot, [
|
|
496
|
-
renderTopologyEntry({
|
|
497
|
-
id: "page.section-nav",
|
|
498
|
-
owner: "home-settings",
|
|
499
|
-
surfaces: ["home"],
|
|
500
|
-
outlet: "home-settings:primary-menu",
|
|
501
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
502
|
-
}),
|
|
503
|
-
renderTopologyEntry({
|
|
504
|
-
id: "page.actions",
|
|
505
|
-
owner: "home-settings",
|
|
506
|
-
surfaces: ["home"],
|
|
507
|
-
outlet: "home-settings:primary-menu",
|
|
508
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
509
|
-
})
|
|
510
|
-
]);
|
|
511
|
-
await writeFileInApp(
|
|
512
|
-
appRoot,
|
|
513
|
-
"src/pages/home/settings.vue",
|
|
514
|
-
`<template>
|
|
515
|
-
<section>
|
|
516
|
-
<ShellOutlet target="home-settings:primary-menu" />
|
|
517
|
-
<RouterView />
|
|
518
|
-
</section>
|
|
519
|
-
</template>
|
|
520
|
-
`
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
await assert.rejects(
|
|
524
|
-
resolvePageLinkTargetDetails({
|
|
525
|
-
appRoot,
|
|
526
|
-
targetFile: "home/settings/profile.vue",
|
|
527
|
-
context: "page target"
|
|
528
|
-
}),
|
|
529
|
-
/found multiple semantic placements mapped to concrete outlet "home-settings:primary-menu": page\.actions \[owner:home-settings\], page\.section-nav \[owner:home-settings\]/
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test("resolvePageLinkTargetDetails inherits a file-route parent subpages host", async () => {
|
|
535
|
-
await withTempApp(async (appRoot) => {
|
|
536
|
-
await writeConfig(
|
|
537
|
-
appRoot,
|
|
538
|
-
`export const config = {
|
|
539
|
-
surfaceDefinitions: {
|
|
540
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
`
|
|
544
|
-
);
|
|
545
|
-
await writeShellLayout(appRoot);
|
|
546
|
-
await writePlacementTopology(appRoot, [
|
|
547
|
-
renderTopologyEntry({
|
|
548
|
-
id: "page.section-nav",
|
|
549
|
-
owner: "contact-view",
|
|
550
|
-
surfaces: ["admin"],
|
|
551
|
-
outlet: "contact-view:sub-pages",
|
|
552
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
553
|
-
})
|
|
554
|
-
]);
|
|
555
|
-
await writeFileInApp(
|
|
556
|
-
appRoot,
|
|
557
|
-
"src/pages/admin/contacts/[contactId].vue",
|
|
558
|
-
`<template>
|
|
559
|
-
<SectionContainerShell>
|
|
560
|
-
<template #tabs>
|
|
561
|
-
<ShellOutlet target="contact-view:sub-pages" />
|
|
562
|
-
</template>
|
|
563
|
-
<RouterView />
|
|
564
|
-
</SectionContainerShell>
|
|
565
|
-
</template>
|
|
566
|
-
`
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
const details = await resolvePageLinkTargetDetails({
|
|
570
|
-
appRoot,
|
|
571
|
-
targetFile: "admin/contacts/[contactId]/notes/index.vue",
|
|
572
|
-
context: "page target"
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
assert.equal(details.parentHost?.id, "contact-view:sub-pages");
|
|
576
|
-
assert.equal(details.placementTarget.id, "page.section-nav");
|
|
577
|
-
assert.equal(details.placementTarget.owner, "contact-view");
|
|
578
|
-
assert.equal(details.componentToken, "");
|
|
579
|
-
assert.equal(details.linkTo, "./notes");
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test("resolvePageLinkTargetDetails honors explicit placement and link overrides", async () => {
|
|
584
|
-
await withTempApp(async (appRoot) => {
|
|
585
|
-
await writeConfig(
|
|
586
|
-
appRoot,
|
|
587
|
-
`export const config = {
|
|
588
|
-
surfaceDefinitions: {
|
|
589
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
`
|
|
593
|
-
);
|
|
594
|
-
await writeShellLayout(appRoot);
|
|
595
|
-
|
|
596
|
-
const details = await resolvePageLinkTargetDetails({
|
|
597
|
-
appRoot,
|
|
598
|
-
targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
|
|
599
|
-
placement: "shell.status",
|
|
600
|
-
componentToken: "custom.link-item",
|
|
601
|
-
linkTo: "./assistant-notes",
|
|
602
|
-
context: "page target"
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
assert.equal(details.placementTarget.id, "shell.status");
|
|
606
|
-
assert.equal(details.componentToken, "custom.link-item");
|
|
607
|
-
assert.equal(details.linkTo, "./assistant-notes");
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
test("resolvePageLinkTargetDetails inherits an index-route parent subpages host for index children", async () => {
|
|
612
|
-
await withTempApp(async (appRoot) => {
|
|
613
|
-
await writeConfig(
|
|
614
|
-
appRoot,
|
|
615
|
-
`export const config = {
|
|
616
|
-
surfaceDefinitions: {
|
|
617
|
-
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
`
|
|
621
|
-
);
|
|
622
|
-
await writeShellLayout(appRoot);
|
|
623
|
-
await writePlacementTopology(appRoot, [
|
|
624
|
-
renderTopologyEntry({
|
|
625
|
-
id: "page.section-nav",
|
|
626
|
-
owner: "customer-view",
|
|
627
|
-
surfaces: ["admin"],
|
|
628
|
-
outlet: "customer-view:sub-pages",
|
|
629
|
-
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
630
|
-
})
|
|
631
|
-
]);
|
|
632
|
-
await writeFileInApp(
|
|
633
|
-
appRoot,
|
|
634
|
-
"src/pages/admin/customers/[customerId]/index.vue",
|
|
635
|
-
`<template>
|
|
636
|
-
<SectionContainerShell>
|
|
637
|
-
<template #tabs>
|
|
638
|
-
<ShellOutlet target="customer-view:sub-pages" />
|
|
639
|
-
</template>
|
|
640
|
-
<RouterView />
|
|
641
|
-
</SectionContainerShell>
|
|
642
|
-
</template>
|
|
643
|
-
`
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
const details = await resolvePageLinkTargetDetails({
|
|
647
|
-
appRoot,
|
|
648
|
-
targetFile: "admin/customers/[customerId]/index/pets/index.vue",
|
|
649
|
-
context: "page target"
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
assert.equal(details.parentHost?.id, "customer-view:sub-pages");
|
|
653
|
-
assert.equal(details.parentHost?.pageFile, "src/pages/admin/customers/[customerId]/index.vue");
|
|
654
|
-
assert.equal(details.placementTarget.id, "page.section-nav");
|
|
655
|
-
assert.equal(details.placementTarget.owner, "customer-view");
|
|
656
|
-
assert.equal(details.componentToken, "");
|
|
657
|
-
assert.equal(details.linkTo, "./pets");
|
|
658
|
-
});
|
|
659
|
-
});
|