@jskit-ai/kernel 0.1.32 → 0.1.34

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.
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { loadInstalledPackageDescriptor } from "../../internal/node/installedPackageDescriptor.js";
4
5
  import { normalizeObject, normalizeText } from "../../shared/support/normalize.js";
5
6
  import { resolveRequiredAppRoot, toPosixPath } from "./path.js";
@@ -7,8 +8,10 @@ import {
7
8
  describeShellOutletTargets,
8
9
  discoverShellOutletTargetsFromVueSource,
9
10
  findShellOutletTargetById,
10
- normalizeShellOutletTargetId
11
+ normalizeShellOutletTargetId,
12
+ normalizeShellOutletTargetRecord
11
13
  } from "../../shared/support/shellLayoutTargets.js";
14
+ import { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
12
15
 
13
16
  const VUE_DISCOVERY_IGNORED_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
14
17
  const LOCK_FILE_RELATIVE_PATH = ".jskit/lock.json";
@@ -52,22 +55,15 @@ function normalizeAppRouteOutletTarget({
52
55
  outlet = {},
53
56
  sourcePath = ""
54
57
  } = {}) {
55
- const outletRecord = normalizeObject(outlet);
56
- const outletTargetId = normalizeShellOutletTargetId(
57
- `${normalizeText(outletRecord.host)}:${normalizeText(outletRecord.position)}`
58
- );
59
- if (!outletTargetId) {
58
+ const normalizedTarget = normalizeShellOutletTargetRecord(outlet, {
59
+ context: sourcePath || "route meta"
60
+ });
61
+ if (!normalizedTarget) {
60
62
  return null;
61
63
  }
62
-
63
- const separatorIndex = outletTargetId.indexOf(":");
64
- const host = outletTargetId.slice(0, separatorIndex);
65
- const position = outletTargetId.slice(separatorIndex + 1);
66
64
  return Object.freeze({
67
- id: outletTargetId,
68
- host,
69
- position,
70
- default: isDefaultEnabled(outletRecord.default),
65
+ ...normalizedTarget,
66
+ default: isDefaultEnabled(normalizedTarget.default),
71
67
  sourcePath
72
68
  });
73
69
  }
@@ -201,32 +197,67 @@ function normalizePackageOutletTarget({
201
197
  return null;
202
198
  }
203
199
 
204
- const outletRecord = normalizeObject(outlet);
205
- const outletTargetId = normalizeShellOutletTargetId(
206
- `${normalizeText(outletRecord.host)}:${normalizeText(outletRecord.position)}`
207
- );
208
- if (!outletTargetId) {
200
+ const normalizedTarget = normalizeShellOutletTargetRecord(outlet, {
201
+ context: `package:${normalizedPackageId}`
202
+ });
203
+ if (!normalizedTarget) {
209
204
  return null;
210
205
  }
211
206
 
212
- const separatorIndex = outletTargetId.indexOf(":");
213
- const host = outletTargetId.slice(0, separatorIndex);
214
- const position = outletTargetId.slice(separatorIndex + 1);
207
+ const outletRecord = normalizeObject(outlet);
215
208
  const source = normalizeText(outletRecord.source);
216
209
  const sourcePath = source
217
210
  ? `package:${normalizedPackageId}:${toPosixPath(source)}`
218
211
  : `package:${normalizedPackageId}${descriptorPath ? `:${toPosixPath(descriptorPath)}` : ""}`;
219
212
 
220
213
  return Object.freeze({
221
- id: outletTargetId,
222
- host,
223
- position,
214
+ ...normalizedTarget,
224
215
  default: false,
225
216
  sourcePath,
226
217
  sourcePackageId: normalizedPackageId
227
218
  });
228
219
  }
229
220
 
221
+ async function loadOutletDefaultOverrides(appRoot = "") {
222
+ const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
223
+ context: "discoverShellOutletTargetsFromApp"
224
+ });
225
+ let appConfig = {};
226
+ try {
227
+ appConfig = normalizeObject(
228
+ await loadAppConfigFromModuleUrl({
229
+ moduleUrl: pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href
230
+ })
231
+ );
232
+ } catch {
233
+ return {};
234
+ }
235
+ return normalizeObject(normalizeObject(appConfig.ui).outletDefaults);
236
+ }
237
+
238
+ function applyOutletDefaultOverrides(target = {}, outletDefaultOverrides = {}) {
239
+ const targetRecord = normalizeObject(target);
240
+ const outletTargetId = normalizeShellOutletTargetId(targetRecord.id);
241
+ if (!outletTargetId) {
242
+ return targetRecord;
243
+ }
244
+
245
+ const overrideRecord = outletDefaultOverrides?.[outletTargetId];
246
+ const normalizedOverrideToken =
247
+ typeof overrideRecord === "string"
248
+ ? normalizeText(overrideRecord)
249
+ : normalizeText(normalizeObject(overrideRecord).linkComponentToken) ||
250
+ normalizeText(normalizeObject(overrideRecord)["link-component-token"]);
251
+ if (!normalizedOverrideToken) {
252
+ return targetRecord;
253
+ }
254
+
255
+ return Object.freeze({
256
+ ...targetRecord,
257
+ defaultLinkComponentToken: normalizedOverrideToken
258
+ });
259
+ }
260
+
230
261
  async function collectInstalledPackageOutletTargets(appRoot) {
231
262
  const installedPackageStates = await readInstalledPackageStates(appRoot);
232
263
  const packageIds = Object.keys(installedPackageStates).sort((left, right) => left.localeCompare(right));
@@ -263,6 +294,7 @@ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" }
263
294
  const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
264
295
  context: "discoverShellOutletTargetsFromApp"
265
296
  });
297
+ const outletDefaultOverrides = await loadOutletDefaultOverrides(resolvedAppRoot);
266
298
 
267
299
  const sourceDirectory = path.resolve(resolvedAppRoot, String(sourceRoot || "src"));
268
300
  const targetById = new Map();
@@ -331,10 +363,10 @@ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" }
331
363
 
332
364
  const targets = [...targetById.values()].sort((left, right) => left.id.localeCompare(right.id));
333
365
  const normalizedTargets = targets.map((target) =>
334
- Object.freeze({
366
+ applyOutletDefaultOverrides({
335
367
  ...target,
336
368
  default: target.id === defaultTargetId
337
- })
369
+ }, outletDefaultOverrides)
338
370
  );
339
371
 
340
372
  return Object.freeze({
@@ -348,7 +380,7 @@ async function resolveShellOutletPlacementTargetFromApp({ appRoot, placement = "
348
380
  const requestedPlacementOption = normalizeText(placement);
349
381
  const requestedPlacementTargetId = normalizeShellOutletTargetId(requestedPlacementOption);
350
382
  if (requestedPlacementOption && !requestedPlacementTargetId) {
351
- throw new Error(`${resolvedContext} option "placement" must be in "host:position" format.`);
383
+ throw new Error(`${resolvedContext} option "placement" must be a target in "host:position" format.`);
352
384
  }
353
385
 
354
386
  const discovered = await discoverShellOutletTargetsFromApp({ appRoot, sourceRoot: "src" });
@@ -380,8 +412,8 @@ async function resolveShellOutletPlacementTargetFromApp({ appRoot, placement = "
380
412
  const availableTargets = describeShellOutletTargets(targets);
381
413
  throw new Error(
382
414
  `${resolvedContext} could not resolve a default ShellOutlet target from app Vue outlets. ` +
383
- `Set one outlet as default (e.g. <ShellOutlet host="shell-layout" position="primary-menu" default />) ` +
384
- `or pass "--placement host:position". Available targets: ${availableTargets || "<none>"}.`
415
+ `Set one outlet as default (e.g. <ShellOutlet target="shell-layout:primary-menu" default />) ` +
416
+ `or pass "--placement shell-layout:primary-menu". Available targets: ${availableTargets || "<none>"}.`
385
417
  );
386
418
  }
387
419
 
@@ -30,8 +30,8 @@ test("resolveShellOutletPlacementTargetFromApp reads outlets across app Vue file
30
30
  "src/components/ShellLayout.vue",
31
31
  `<template>
32
32
  <div>
33
- <ShellOutlet host="shell-layout" position="primary-menu" />
34
- <ShellOutlet host="shell-layout" position="top-right" />
33
+ <ShellOutlet target="shell-layout:primary-menu" />
34
+ <ShellOutlet target="shell-layout:top-right" />
35
35
  </div>
36
36
  </template>
37
37
  `
@@ -41,7 +41,7 @@ test("resolveShellOutletPlacementTargetFromApp reads outlets across app Vue file
41
41
  "src/pages/admin/workspace/settings/index.vue",
42
42
  `<template>
43
43
  <section>
44
- <ShellOutlet host="admin-settings" position="forms" default />
44
+ <ShellOutlet target="admin-settings:forms" default />
45
45
  </section>
46
46
  </template>
47
47
  `
@@ -52,8 +52,7 @@ test("resolveShellOutletPlacementTargetFromApp reads outlets across app Vue file
52
52
  context: "ui-generator"
53
53
  });
54
54
 
55
- assert.equal(target.host, "admin-settings");
56
- assert.equal(target.position, "forms");
55
+ assert.equal(target.id, "admin-settings:forms");
57
56
  });
58
57
  });
59
58
 
@@ -64,7 +63,7 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
64
63
  "src/components/ShellLayout.vue",
65
64
  `<template>
66
65
  <div>
67
- <ShellOutlet host="shell-layout" position="primary-menu" default />
66
+ <ShellOutlet target="shell-layout:primary-menu" default />
68
67
  </div>
69
68
  </template>
70
69
  `
@@ -98,7 +97,11 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
98
97
  ui: {
99
98
  placements: {
100
99
  outlets: [
101
- { host: "workspace-tools", position: "primary-menu", source: "src/client/components/UsersWorkspaceToolsWidget.vue" }
100
+ {
101
+ target: "workspace-tools:primary-menu",
102
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
103
+ source: "src/client/components/UsersWorkspaceToolsWidget.vue"
104
+ }
102
105
  ]
103
106
  }
104
107
  }
@@ -114,9 +117,8 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
114
117
  );
115
118
  assert.deepEqual(discovered.targets[1], {
116
119
  id: "workspace-tools:primary-menu",
117
- host: "workspace-tools",
118
- position: "primary-menu",
119
120
  default: false,
121
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
120
122
  sourcePath: "package:@example/users-web:src/client/components/UsersWorkspaceToolsWidget.vue",
121
123
  sourcePackageId: "@example/users-web"
122
124
  });
@@ -130,6 +132,68 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
130
132
  });
131
133
  });
132
134
 
135
+ test("discoverShellOutletTargetsFromApp applies app config default-link overrides by target id", async () => {
136
+ await withTempApp(async (appRoot) => {
137
+ await writeFileInApp(
138
+ appRoot,
139
+ "config/public.js",
140
+ `export const config = {
141
+ ui: {
142
+ outletDefaults: {
143
+ "workspace-tools:primary-menu": {
144
+ linkComponentToken: "local.main.ui.surface-aware-menu-link-item"
145
+ }
146
+ }
147
+ }
148
+ };
149
+ `
150
+ );
151
+ await writeFileInApp(
152
+ appRoot,
153
+ ".jskit/lock.json",
154
+ `${JSON.stringify(
155
+ {
156
+ lockVersion: 1,
157
+ installedPackages: {
158
+ "@example/users-web": {
159
+ packageId: "@example/users-web",
160
+ source: {
161
+ type: "npm-installed-package",
162
+ descriptorPath: "node_modules/@example/users-web/package.descriptor.mjs"
163
+ }
164
+ }
165
+ }
166
+ },
167
+ null,
168
+ 2
169
+ )}\n`
170
+ );
171
+ await writeFileInApp(
172
+ appRoot,
173
+ "node_modules/@example/users-web/package.descriptor.mjs",
174
+ `export default {
175
+ packageId: "@example/users-web",
176
+ metadata: {
177
+ ui: {
178
+ placements: {
179
+ outlets: [
180
+ {
181
+ target: "workspace-tools:primary-menu",
182
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item"
183
+ }
184
+ ]
185
+ }
186
+ }
187
+ }
188
+ };
189
+ `
190
+ );
191
+
192
+ const discovered = await discoverShellOutletTargetsFromApp({ appRoot });
193
+ assert.equal(discovered.targets[0].defaultLinkComponentToken, "local.main.ui.surface-aware-menu-link-item");
194
+ });
195
+ });
196
+
133
197
  test("discoverShellOutletTargetsFromApp returns targets with sourcePath and default marker", async () => {
134
198
  await withTempApp(async (appRoot) => {
135
199
  await writeFileInApp(
@@ -137,7 +201,7 @@ test("discoverShellOutletTargetsFromApp returns targets with sourcePath and defa
137
201
  "src/components/ShellLayout.vue",
138
202
  `<template>
139
203
  <div>
140
- <ShellOutlet host="shell-layout" position="primary-menu" />
204
+ <ShellOutlet target="shell-layout:primary-menu" />
141
205
  </div>
142
206
  </template>
143
207
  `
@@ -147,7 +211,7 @@ test("discoverShellOutletTargetsFromApp returns targets with sourcePath and defa
147
211
  "src/pages/admin/toolbox/index.vue",
148
212
  `<template>
149
213
  <section>
150
- <ShellOutlet host="admin-toolbox" position="widgets" default />
214
+ <ShellOutlet target="admin-toolbox:widgets" default />
151
215
  </section>
152
216
  </template>
153
217
  `
@@ -158,16 +222,14 @@ test("discoverShellOutletTargetsFromApp returns targets with sourcePath and defa
158
222
  assert.deepEqual(discovered.targets, [
159
223
  {
160
224
  id: "admin-toolbox:widgets",
161
- host: "admin-toolbox",
162
- position: "widgets",
163
225
  default: true,
226
+ defaultLinkComponentToken: "",
164
227
  sourcePath: "src/pages/admin/toolbox/index.vue"
165
228
  },
166
229
  {
167
230
  id: "shell-layout:primary-menu",
168
- host: "shell-layout",
169
- position: "primary-menu",
170
231
  default: false,
232
+ defaultLinkComponentToken: "",
171
233
  sourcePath: "src/components/ShellLayout.vue"
172
234
  }
173
235
  ]);
@@ -188,8 +250,7 @@ test("discoverShellOutletTargetsFromApp discovers route meta placement outlets",
188
250
  "placements": {
189
251
  "outlets": [
190
252
  {
191
- "host": "contact-tools",
192
- "position": "sub-pages"
253
+ "target": "contact-tools:sub-pages"
193
254
  }
194
255
  ]
195
256
  }
@@ -204,9 +265,8 @@ test("discoverShellOutletTargetsFromApp discovers route meta placement outlets",
204
265
  assert.deepEqual(discovered.targets, [
205
266
  {
206
267
  id: "contact-tools:sub-pages",
207
- host: "contact-tools",
208
- position: "sub-pages",
209
268
  default: false,
269
+ defaultLinkComponentToken: "",
210
270
  sourcePath: "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/contact-tools.vue"
211
271
  }
212
272
  ]);
@@ -227,8 +287,8 @@ test("resolveShellOutletPlacementTargetFromApp supports explicit placement overr
227
287
  "src/components/ShellLayout.vue",
228
288
  `<template>
229
289
  <div>
230
- <ShellOutlet host="shell-layout" position="primary-menu" default />
231
- <ShellOutlet host="shell-layout" position="top-right" />
290
+ <ShellOutlet target="shell-layout:primary-menu" default />
291
+ <ShellOutlet target="shell-layout:top-right" />
232
292
  </div>
233
293
  </template>
234
294
  `
@@ -240,8 +300,7 @@ test("resolveShellOutletPlacementTargetFromApp supports explicit placement overr
240
300
  placement: "shell-layout:top-right"
241
301
  });
242
302
 
243
- assert.equal(target.host, "shell-layout");
244
- assert.equal(target.position, "top-right");
303
+ assert.equal(target.id, "shell-layout:top-right");
245
304
  });
246
305
  });
247
306
 
@@ -252,7 +311,7 @@ test("resolveShellOutletPlacementTargetFromApp validates placement format", asyn
252
311
  "src/components/ShellLayout.vue",
253
312
  `<template>
254
313
  <div>
255
- <ShellOutlet host="shell-layout" position="primary-menu" default />
314
+ <ShellOutlet target="shell-layout:primary-menu" default />
256
315
  </div>
257
316
  </template>
258
317
  `
@@ -265,7 +324,7 @@ test("resolveShellOutletPlacementTargetFromApp validates placement format", asyn
265
324
  context: "ui-generator",
266
325
  placement: "invalid-placement"
267
326
  }),
268
- /option "placement" must be in "host:position" format/
327
+ /option "placement" must be a target in "host:position" format/
269
328
  );
270
329
  });
271
330
  });
@@ -277,7 +336,7 @@ test("resolveShellOutletPlacementTargetFromApp throws when multiple default outl
277
336
  "src/components/ShellLayout.vue",
278
337
  `<template>
279
338
  <div>
280
- <ShellOutlet host="shell-layout" position="primary-menu" default />
339
+ <ShellOutlet target="shell-layout:primary-menu" default />
281
340
  </div>
282
341
  </template>
283
342
  `
@@ -287,7 +346,7 @@ test("resolveShellOutletPlacementTargetFromApp throws when multiple default outl
287
346
  "src/pages/admin/workspace/settings/index.vue",
288
347
  `<template>
289
348
  <section>
290
- <ShellOutlet host="admin-settings" position="forms" default />
349
+ <ShellOutlet target="admin-settings:forms" default />
291
350
  </section>
292
351
  </template>
293
352
  `
@@ -180,6 +180,34 @@ function normalizePositiveInteger(value, { fallback = 0 } = {}) {
180
180
  return numeric;
181
181
  }
182
182
 
183
+ function normalizeCanonicalRecordIdText(value, { fallback = null } = {}) {
184
+ if (value == null) {
185
+ return fallback;
186
+ }
187
+
188
+ const normalized = String(value).trim();
189
+ return /^[1-9][0-9]*$/.test(normalized) ? normalized : fallback;
190
+ }
191
+
192
+ function normalizeRecordId(value, { fallback = null } = {}) {
193
+ if (value == null) {
194
+ return fallback;
195
+ }
196
+
197
+ if (typeof value === "string") {
198
+ return normalizeCanonicalRecordIdText(value, { fallback });
199
+ }
200
+
201
+ if (typeof value === "bigint") {
202
+ if (value < 1n) {
203
+ return fallback;
204
+ }
205
+ return normalizeCanonicalRecordIdText(value, { fallback });
206
+ }
207
+
208
+ return fallback;
209
+ }
210
+
183
211
  function normalizeOpaqueId(value, { fallback = null } = {}) {
184
212
  if (value == null) {
185
213
  return fallback;
@@ -191,7 +219,7 @@ function normalizeOpaqueId(value, { fallback = null } = {}) {
191
219
  }
192
220
 
193
221
  if (typeof value === "number") {
194
- return Number.isFinite(value) ? value : fallback;
222
+ return Number.isFinite(value) ? String(value) : fallback;
195
223
  }
196
224
 
197
225
  if (typeof value === "bigint") {
@@ -244,6 +272,8 @@ export {
244
272
  normalizeUniqueTextList,
245
273
  normalizeInteger,
246
274
  normalizePositiveInteger,
275
+ normalizeCanonicalRecordIdText,
276
+ normalizeRecordId,
247
277
  normalizeOpaqueId,
248
278
  normalizeOneOf,
249
279
  ensureNonEmptyText
@@ -3,11 +3,13 @@ import assert from "node:assert/strict";
3
3
  import {
4
4
  hasValue,
5
5
  normalizeBoolean,
6
+ normalizeCanonicalRecordIdText,
6
7
  normalizeFiniteInteger,
7
8
  normalizeFiniteNumber,
8
9
  normalizeIfInSource,
9
10
  normalizeIfPresent,
10
11
  normalizeOrNull,
12
+ normalizeRecordId,
11
13
  normalizeOpaqueId,
12
14
  normalizePositiveInteger,
13
15
  normalizeOneOf,
@@ -172,10 +174,27 @@ test("normalizeOrNull normalizes non-nullish values and coerces nullish to null"
172
174
  );
173
175
  });
174
176
 
177
+ test("normalizeRecordId accepts canonical string and bigint identifiers only", () => {
178
+ const unsafeNumericId = Number(9007199254740993n);
179
+ assert.equal(normalizeRecordId(" 7 "), "7");
180
+ assert.equal(normalizeRecordId(10n), "10");
181
+ assert.equal(normalizeRecordId(7), null);
182
+ assert.equal(normalizeRecordId(unsafeNumericId), null);
183
+ assert.equal(normalizeRecordId(""), null);
184
+ assert.equal(normalizeRecordId(null), null);
185
+ });
186
+
187
+ test("normalizeCanonicalRecordIdText validates canonical positive decimal identifiers", () => {
188
+ assert.equal(normalizeCanonicalRecordIdText(" 7 "), "7");
189
+ assert.equal(normalizeCanonicalRecordIdText("007"), null);
190
+ assert.equal(normalizeCanonicalRecordIdText("0"), null);
191
+ assert.equal(normalizeCanonicalRecordIdText("abc"), null);
192
+ });
193
+
175
194
  test("normalizeOpaqueId preserves opaque identifiers", () => {
176
195
  assert.equal(normalizeOpaqueId(" user-123 "), "user-123");
177
- assert.equal(normalizeOpaqueId(7), 7);
178
- assert.equal(normalizeOpaqueId(0), 0);
196
+ assert.equal(normalizeOpaqueId(7), "7");
197
+ assert.equal(normalizeOpaqueId(0), "0");
179
198
  assert.equal(normalizeOpaqueId(10n), "10");
180
199
  assert.equal(normalizeOpaqueId(""), null);
181
200
  assert.equal(normalizeOpaqueId(null), null);
@@ -1,4 +1,7 @@
1
- import { normalizeText } from "./normalize.js";
1
+ import {
2
+ normalizeObject,
3
+ normalizeText
4
+ } from "./normalize.js";
2
5
 
3
6
  const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/g;
4
7
  const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
@@ -40,6 +43,21 @@ function normalizeShellOutletTargetId(value = "") {
40
43
  return `${host}:${position}`;
41
44
  }
42
45
 
46
+ function resolveShellOutletTargetParts(
47
+ {
48
+ target = ""
49
+ } = {}
50
+ ) {
51
+ const normalizedTargetId = normalizeShellOutletTargetId(target);
52
+ if (!normalizedTargetId) {
53
+ return null;
54
+ }
55
+
56
+ return Object.freeze({
57
+ id: normalizedTargetId
58
+ });
59
+ }
60
+
43
61
  function findShellOutletTargetById(targets = [], targetId = "") {
44
62
  const entries = Array.isArray(targets) ? targets : [];
45
63
  const normalizedTargetId = normalizeShellOutletTargetId(targetId);
@@ -70,6 +88,42 @@ function isDefaultAttributeEnabled(value) {
70
88
  return normalized !== "false" && normalized !== "0" && normalized !== "no" && normalized !== "off";
71
89
  }
72
90
 
91
+ function normalizeShellOutletTargetRecord(
92
+ value = {},
93
+ {
94
+ context = "shell layout"
95
+ } = {}
96
+ ) {
97
+ const record = normalizeObject(value);
98
+ const resolvedContext = normalizeText(context) || "shell layout";
99
+ if (Object.hasOwn(record, "host") || Object.hasOwn(record, "position")) {
100
+ throw new Error(
101
+ `${resolvedContext} must declare ShellOutlet targets with "target" only. ` +
102
+ `Legacy "host" and "position" attributes are not supported.`
103
+ );
104
+ }
105
+
106
+ const targetParts = resolveShellOutletTargetParts(
107
+ {
108
+ target: record.target
109
+ },
110
+ { context: resolvedContext }
111
+ );
112
+ if (!targetParts) {
113
+ return null;
114
+ }
115
+
116
+ return Object.freeze({
117
+ ...targetParts,
118
+ default:
119
+ Object.hasOwn(record, "default") &&
120
+ isDefaultAttributeEnabled(record.default),
121
+ defaultLinkComponentToken:
122
+ normalizeText(record.defaultLinkComponentToken) ||
123
+ normalizeText(record["default-link-component-token"])
124
+ });
125
+ }
126
+
73
127
  function discoverShellOutletTargetsFromVueSource(source = "", { context = "shell layout" } = {}) {
74
128
  const sourceText = String(source || "");
75
129
  const resolvedContext = normalizeText(context) || "shell layout";
@@ -78,39 +132,26 @@ function discoverShellOutletTargetsFromVueSource(source = "", { context = "shell
78
132
 
79
133
  for (const tagMatch of sourceText.matchAll(SHELL_OUTLET_TAG_PATTERN)) {
80
134
  const attributes = parseTagAttributes(tagMatch[1]);
81
- const host = normalizeText(attributes.host);
82
- const position = normalizeText(attributes.position);
83
- if (!host || !position) {
135
+ const normalizedTarget = normalizeShellOutletTargetRecord(attributes, {
136
+ context: resolvedContext
137
+ });
138
+ if (!normalizedTarget) {
84
139
  continue;
85
140
  }
86
-
87
- const id = normalizeShellOutletTargetId(`${host}:${position}`);
88
- if (!id) {
89
- continue;
90
- }
91
- if (targetById.has(id)) {
92
- throw new Error(`${resolvedContext} contains duplicate ShellOutlet target "${id}".`);
141
+ if (targetById.has(normalizedTarget.id)) {
142
+ throw new Error(`${resolvedContext} contains duplicate ShellOutlet target "${normalizedTarget.id}".`);
93
143
  }
94
144
 
95
- const hasDefaultAttribute = Object.hasOwn(attributes, "default") && isDefaultAttributeEnabled(attributes.default);
96
- if (hasDefaultAttribute) {
145
+ if (normalizedTarget.default) {
97
146
  if (defaultTargetId) {
98
147
  throw new Error(
99
- `${resolvedContext} defines multiple default ShellOutlet targets: "${defaultTargetId}" and "${id}".`
148
+ `${resolvedContext} defines multiple default ShellOutlet targets: "${defaultTargetId}" and "${normalizedTarget.id}".`
100
149
  );
101
150
  }
102
- defaultTargetId = id;
151
+ defaultTargetId = normalizedTarget.id;
103
152
  }
104
153
 
105
- targetById.set(
106
- id,
107
- Object.freeze({
108
- id,
109
- host,
110
- position,
111
- default: hasDefaultAttribute
112
- })
113
- );
154
+ targetById.set(normalizedTarget.id, normalizedTarget);
114
155
  }
115
156
 
116
157
  return Object.freeze({
@@ -123,5 +164,7 @@ export {
123
164
  describeShellOutletTargets,
124
165
  discoverShellOutletTargetsFromVueSource,
125
166
  findShellOutletTargetById,
126
- normalizeShellOutletTargetId
167
+ normalizeShellOutletTargetId,
168
+ normalizeShellOutletTargetRecord,
169
+ resolveShellOutletTargetParts
127
170
  };