@jskit-ai/jskit-cli 0.2.26 → 0.2.28

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 (59) hide show
  1. package/package.json +3 -2
  2. package/src/server/cliRuntime/appState.js +226 -0
  3. package/src/server/cliRuntime/capabilitySupport.js +194 -0
  4. package/src/server/cliRuntime/descriptorValidation.js +150 -0
  5. package/src/server/cliRuntime/ioAndMigrations.js +381 -0
  6. package/src/server/cliRuntime/localPackageSupport.js +390 -0
  7. package/src/server/cliRuntime/mutationApplication.js +9 -0
  8. package/src/server/cliRuntime/mutationWhen.js +285 -0
  9. package/src/server/cliRuntime/mutations/fileMutations.js +247 -0
  10. package/src/server/cliRuntime/mutations/installMigrationMutation.js +213 -0
  11. package/src/server/cliRuntime/mutations/mutationPathUtils.js +12 -0
  12. package/src/server/cliRuntime/mutations/surfaceTargets.js +155 -0
  13. package/src/server/cliRuntime/mutations/templateContext.js +171 -0
  14. package/src/server/cliRuntime/mutations/textMutations.js +250 -0
  15. package/src/server/cliRuntime/packageInstallFlow.js +489 -0
  16. package/src/server/cliRuntime/packageIntrospection/exportEntries.js +259 -0
  17. package/src/server/cliRuntime/packageIntrospection/exportedSymbols.js +216 -0
  18. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +98 -0
  19. package/src/server/cliRuntime/packageIntrospection/providerBindingIntrospection.js +377 -0
  20. package/src/server/cliRuntime/packageIntrospection.js +137 -0
  21. package/src/server/cliRuntime/packageOptions.js +299 -0
  22. package/src/server/cliRuntime/packageRegistries.js +343 -0
  23. package/src/server/cliRuntime/packageTemplateResolution.js +131 -0
  24. package/src/server/cliRuntime/viteProxy.js +356 -0
  25. package/src/server/commandHandlers/health.js +292 -0
  26. package/src/server/commandHandlers/list.js +292 -0
  27. package/src/server/commandHandlers/package.js +23 -0
  28. package/src/server/commandHandlers/packageCommands/add.js +282 -0
  29. package/src/server/commandHandlers/packageCommands/create.js +155 -0
  30. package/src/server/commandHandlers/packageCommands/generate.js +116 -0
  31. package/src/server/commandHandlers/packageCommands/migrations.js +155 -0
  32. package/src/server/commandHandlers/packageCommands/position.js +103 -0
  33. package/src/server/commandHandlers/packageCommands/remove.js +181 -0
  34. package/src/server/commandHandlers/packageCommands/update.js +40 -0
  35. package/src/server/commandHandlers/shared.js +314 -0
  36. package/src/server/commandHandlers/show/payloads.js +92 -0
  37. package/src/server/commandHandlers/show/renderBundleText.js +16 -0
  38. package/src/server/commandHandlers/show/renderHelpers.js +82 -0
  39. package/src/server/commandHandlers/show/renderPackageCapabilities.js +124 -0
  40. package/src/server/commandHandlers/show/renderPackageExports.js +203 -0
  41. package/src/server/commandHandlers/show/renderPackageText.js +332 -0
  42. package/src/server/commandHandlers/show.js +114 -0
  43. package/src/server/core/argParser.js +144 -0
  44. package/src/server/{runtimeDeps.js → core/buildCommandDeps.js} +2 -1
  45. package/src/server/core/commandCatalog.js +47 -0
  46. package/src/server/core/createCliRunner.js +150 -0
  47. package/src/server/core/createCommandHandlers.js +43 -0
  48. package/src/server/{runCli.js → core/dispatchCli.js} +14 -1
  49. package/src/server/core/usageHelp.js +344 -0
  50. package/src/server/index.js +1 -1
  51. package/src/server/{optionInterpolation.js → shared/optionInterpolation.js} +12 -1
  52. package/src/server/{pathResolution.js → shared/pathResolution.js} +1 -1
  53. package/src/server/argParser.js +0 -206
  54. package/src/server/cliRuntime.js +0 -4853
  55. package/src/server/commandHandlers.js +0 -2109
  56. /package/src/server/{cliError.js → shared/cliError.js} +0 -0
  57. /package/src/server/{collectionUtils.js → shared/collectionUtils.js} +0 -0
  58. /package/src/server/{outputFormatting.js → shared/outputFormatting.js} +0 -0
  59. /package/src/server/{packageIdHelpers.js → shared/packageIdHelpers.js} +0 -0
@@ -0,0 +1,259 @@
1
+ import path from "node:path";
2
+ import {
3
+ ensureArray,
4
+ ensureObject
5
+ } from "../../shared/collectionUtils.js";
6
+ import { fileExists } from "../ioAndMigrations.js";
7
+ import { normalizeRelativePosixPath } from "../localPackageSupport.js";
8
+
9
+ function collectPackageExportEntries(exportsField) {
10
+ const entries = [];
11
+ const normalizeExportSubpath = (subpath) => {
12
+ const normalized = String(subpath || ".").trim() || ".";
13
+ if (normalized === "." || normalized === "./") {
14
+ return {
15
+ normalized: ".",
16
+ segments: []
17
+ };
18
+ }
19
+
20
+ const withoutPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
21
+ const segments = withoutPrefix.split("/").map((value) => String(value || "").trim()).filter(Boolean);
22
+ return {
23
+ normalized: normalized.startsWith("./") ? normalized : `./${withoutPrefix}`,
24
+ segments
25
+ };
26
+ };
27
+
28
+ const resolveSubpathSortPriority = (subpath) => {
29
+ const normalized = normalizeExportSubpath(subpath);
30
+ const firstSegment = String(normalized.segments[0] || "").trim();
31
+ if (firstSegment === "client") {
32
+ return 0;
33
+ }
34
+ if (firstSegment === "server") {
35
+ return 1;
36
+ }
37
+ if (firstSegment === "shared") {
38
+ return 2;
39
+ }
40
+ if (normalized.normalized === ".") {
41
+ return 3;
42
+ }
43
+ return 10;
44
+ };
45
+
46
+ const appendEntry = (subpath, conditions, target) => {
47
+ const normalizedSubpath = String(subpath || ".").trim() || ".";
48
+ const normalizedTarget = String(target || "").trim();
49
+ if (!normalizedTarget) {
50
+ return;
51
+ }
52
+ const normalizedConditions = ensureArray(conditions).map((value) => String(value || "").trim()).filter(Boolean);
53
+ entries.push({
54
+ subpath: normalizedSubpath,
55
+ condition: normalizedConditions.length > 0 ? normalizedConditions.join(".") : "default",
56
+ target: normalizedTarget
57
+ });
58
+ };
59
+
60
+ const visit = (subpath, value, conditionStack = []) => {
61
+ if (typeof value === "string") {
62
+ appendEntry(subpath, conditionStack, value);
63
+ return;
64
+ }
65
+ if (Array.isArray(value)) {
66
+ for (const item of value) {
67
+ visit(subpath, item, conditionStack);
68
+ }
69
+ return;
70
+ }
71
+ if (!value || typeof value !== "object") {
72
+ return;
73
+ }
74
+ for (const [conditionName, nested] of Object.entries(value)) {
75
+ visit(subpath, nested, [...conditionStack, conditionName]);
76
+ }
77
+ };
78
+
79
+ if (typeof exportsField === "string" || Array.isArray(exportsField)) {
80
+ visit(".", exportsField, []);
81
+ } else if (exportsField && typeof exportsField === "object") {
82
+ const root = ensureObject(exportsField);
83
+ const rootKeys = Object.keys(root);
84
+ const hasSubpathKeys = rootKeys.some((key) => key.startsWith("."));
85
+ if (hasSubpathKeys) {
86
+ for (const [subpath, value] of Object.entries(root)) {
87
+ visit(subpath, value, []);
88
+ }
89
+ } else {
90
+ visit(".", root, []);
91
+ }
92
+ }
93
+
94
+ const deduplicated = [];
95
+ const seen = new Set();
96
+ for (const entry of entries) {
97
+ const key = `${entry.subpath}::${entry.condition}::${entry.target}`;
98
+ if (seen.has(key)) {
99
+ continue;
100
+ }
101
+ seen.add(key);
102
+ deduplicated.push(entry);
103
+ }
104
+ return deduplicated.sort((left, right) => {
105
+ const leftPriority = resolveSubpathSortPriority(left.subpath);
106
+ const rightPriority = resolveSubpathSortPriority(right.subpath);
107
+ if (leftPriority !== rightPriority) {
108
+ return leftPriority - rightPriority;
109
+ }
110
+
111
+ const leftParts = normalizeExportSubpath(left.subpath);
112
+ const rightParts = normalizeExportSubpath(right.subpath);
113
+ const leftRoot = String(leftParts.segments[0] || "");
114
+ const rightRoot = String(rightParts.segments[0] || "");
115
+ const rootComparison = leftRoot.localeCompare(rightRoot);
116
+ if (rootComparison !== 0) {
117
+ return rootComparison;
118
+ }
119
+
120
+ const depthComparison = leftParts.segments.length - rightParts.segments.length;
121
+ if (depthComparison !== 0) {
122
+ return depthComparison;
123
+ }
124
+
125
+ const subpathComparison = left.subpath.localeCompare(right.subpath);
126
+ if (subpathComparison !== 0) {
127
+ return subpathComparison;
128
+ }
129
+ const conditionComparison = left.condition.localeCompare(right.condition);
130
+ if (conditionComparison !== 0) {
131
+ return conditionComparison;
132
+ }
133
+ return left.target.localeCompare(right.target);
134
+ });
135
+ }
136
+
137
+ async function describePackageExports({ packageRoot, packageJson }) {
138
+ const rootDir = String(packageRoot || "").trim();
139
+ if (!rootDir) {
140
+ return [];
141
+ }
142
+
143
+ const exportsField = ensureObject(packageJson).exports;
144
+ const entries = collectPackageExportEntries(exportsField);
145
+ const records = [];
146
+
147
+ for (const entry of entries) {
148
+ const subpath = String(entry.subpath || ".").trim() || ".";
149
+ const condition = String(entry.condition || "default").trim() || "default";
150
+ const target = String(entry.target || "").trim();
151
+ const isPattern = subpath.includes("*") || target.includes("*");
152
+ const isRelativeTarget = target.startsWith("./");
153
+ let targetExists = null;
154
+ if (isRelativeTarget && !isPattern) {
155
+ const absoluteTargetPath = path.resolve(rootDir, target);
156
+ targetExists = await fileExists(absoluteTargetPath);
157
+ }
158
+
159
+ let targetType = "external";
160
+ if (isPattern) {
161
+ targetType = "pattern";
162
+ } else if (isRelativeTarget) {
163
+ targetType = "file";
164
+ }
165
+
166
+ records.push({
167
+ subpath,
168
+ condition,
169
+ target,
170
+ targetType,
171
+ targetExists
172
+ });
173
+ }
174
+
175
+ return records;
176
+ }
177
+
178
+ function formatPackageSubpathImport(packageId, subpath) {
179
+ const normalizedPackageId = String(packageId || "").trim();
180
+ const normalizedSubpath = String(subpath || "").trim();
181
+ if (!normalizedPackageId) {
182
+ return normalizedSubpath;
183
+ }
184
+ if (!normalizedSubpath || normalizedSubpath === ".") {
185
+ return normalizedPackageId;
186
+ }
187
+ if (normalizedSubpath.startsWith("./")) {
188
+ return `${normalizedPackageId}/${normalizedSubpath.slice(2)}`;
189
+ }
190
+ if (normalizedSubpath.startsWith("/")) {
191
+ return `${normalizedPackageId}${normalizedSubpath}`;
192
+ }
193
+ return `${normalizedPackageId}/${normalizedSubpath}`;
194
+ }
195
+
196
+ function deriveCanonicalExportTargetForSubpath(subpath) {
197
+ const normalizedSubpath = String(subpath || "").trim();
198
+ if (!normalizedSubpath) {
199
+ return "";
200
+ }
201
+ if (normalizedSubpath === ".") {
202
+ return "./src/index.js";
203
+ }
204
+ if (!normalizedSubpath.startsWith("./")) {
205
+ return "";
206
+ }
207
+
208
+ const bareSubpath = normalizedSubpath.slice(2);
209
+ if (!bareSubpath) {
210
+ return "";
211
+ }
212
+ if (bareSubpath === "client" || bareSubpath === "server" || bareSubpath === "shared") {
213
+ return `./src/${bareSubpath}/index.js`;
214
+ }
215
+
216
+ const roots = ["client", "server", "shared"];
217
+ for (const root of roots) {
218
+ if (!bareSubpath.startsWith(`${root}/`)) {
219
+ continue;
220
+ }
221
+ const suffix = bareSubpath.slice(root.length + 1);
222
+ if (!suffix) {
223
+ return "";
224
+ }
225
+ const hasJsExtension = /\.(?:c|m)?js$/.test(suffix);
226
+ const normalizedSuffix = hasJsExtension ? suffix : `${suffix}.js`;
227
+ return `./src/${root}/${normalizedSuffix}`;
228
+ }
229
+
230
+ return "";
231
+ }
232
+
233
+ function shouldShowPackageExportTarget({ subpath, target, targetType }) {
234
+ if (String(targetType || "").trim() !== "file") {
235
+ return true;
236
+ }
237
+
238
+ const canonicalTarget = deriveCanonicalExportTargetForSubpath(subpath);
239
+ if (!canonicalTarget) {
240
+ return true;
241
+ }
242
+
243
+ const normalizeTarget = (value) => {
244
+ const raw = String(value || "").trim();
245
+ if (!raw) {
246
+ return "";
247
+ }
248
+ const withoutPrefix = raw.startsWith("./") ? raw.slice(2) : raw;
249
+ return `./${normalizeRelativePosixPath(withoutPrefix)}`;
250
+ };
251
+
252
+ return normalizeTarget(target) !== normalizeTarget(canonicalTarget);
253
+ }
254
+
255
+ export {
256
+ describePackageExports,
257
+ formatPackageSubpathImport,
258
+ shouldShowPackageExportTarget
259
+ };
@@ -0,0 +1,216 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ ensureArray,
5
+ ensureObject,
6
+ sortStrings
7
+ } from "../../shared/collectionUtils.js";
8
+ import { fileExists } from "../ioAndMigrations.js";
9
+ import { normalizeRelativePosixPath } from "../localPackageSupport.js";
10
+
11
+ function parseNamedExportSpecifiers(specifierSource) {
12
+ const source = String(specifierSource || "");
13
+ return source
14
+ .split(",")
15
+ .map((entry) => entry.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/g, "").trim())
16
+ .filter(Boolean)
17
+ .map((entry) => entry.replace(/\s+/g, " "))
18
+ .map((entry) => {
19
+ const aliasMatch = /^(.+?)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/.exec(entry);
20
+ if (aliasMatch) {
21
+ return aliasMatch[2];
22
+ }
23
+ return entry;
24
+ })
25
+ .filter((entry) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(entry));
26
+ }
27
+
28
+ function parseExportedSymbolsFromSource(source) {
29
+ const text = String(source || "");
30
+ const symbols = new Set();
31
+ const starReExports = new Set();
32
+ const namedReExports = new Set();
33
+
34
+ const namespaceStarPattern = /export\s+\*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+["']([^"']+)["']\s*;?/g;
35
+ let match = namespaceStarPattern.exec(text);
36
+ while (match) {
37
+ symbols.add(String(match[1] || "").trim());
38
+ starReExports.add(String(match[2] || "").trim());
39
+ match = namespaceStarPattern.exec(text);
40
+ }
41
+
42
+ const starPattern = /export\s+\*\s+from\s+["']([^"']+)["']\s*;?/g;
43
+ match = starPattern.exec(text);
44
+ while (match) {
45
+ starReExports.add(String(match[1] || "").trim());
46
+ match = starPattern.exec(text);
47
+ }
48
+
49
+ const namedPattern = /export\s*\{([\s\S]*?)\}\s*(?:from\s*["']([^"']+)["'])?\s*;?/g;
50
+ match = namedPattern.exec(text);
51
+ while (match) {
52
+ const listSource = String(match[1] || "");
53
+ for (const symbol of parseNamedExportSpecifiers(listSource)) {
54
+ symbols.add(symbol);
55
+ }
56
+ if (match[2]) {
57
+ namedReExports.add(String(match[2] || "").trim());
58
+ }
59
+ match = namedPattern.exec(text);
60
+ }
61
+
62
+ const functionPattern = /export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
63
+ match = functionPattern.exec(text);
64
+ while (match) {
65
+ symbols.add(String(match[1] || "").trim());
66
+ match = functionPattern.exec(text);
67
+ }
68
+
69
+ const classPattern = /export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
70
+ match = classPattern.exec(text);
71
+ while (match) {
72
+ symbols.add(String(match[1] || "").trim());
73
+ match = classPattern.exec(text);
74
+ }
75
+
76
+ const variablePattern = /export\s+(?:const|let|var)\s+([\s\S]*?);/g;
77
+ match = variablePattern.exec(text);
78
+ while (match) {
79
+ const declaration = String(match[1] || "");
80
+ const names = declaration.split(",").map((entry) => String(entry || "").trim());
81
+ for (const name of names) {
82
+ const declarationMatch = /^([A-Za-z_$][A-Za-z0-9_$]*)\b/.exec(name);
83
+ if (declarationMatch) {
84
+ symbols.add(String(declarationMatch[1] || "").trim());
85
+ }
86
+ }
87
+ match = variablePattern.exec(text);
88
+ }
89
+
90
+ const hasDefaultExport = /\bexport\s+default\b/.test(text);
91
+ return {
92
+ symbols: sortStrings([...symbols]),
93
+ starReExports: sortStrings([...starReExports]),
94
+ namedReExports: sortStrings([...namedReExports]),
95
+ hasDefaultExport
96
+ };
97
+ }
98
+
99
+ function classifyExportedSymbols(symbols = []) {
100
+ const source = ensureArray(symbols).map((value) => String(value || "").trim()).filter(Boolean);
101
+ const providers = [];
102
+ const constants = [];
103
+ const functions = [];
104
+ const classesOrTypes = [];
105
+ const internals = [];
106
+ const others = [];
107
+
108
+ for (const symbol of source) {
109
+ if (/Provider$/.test(symbol)) {
110
+ providers.push(symbol);
111
+ continue;
112
+ }
113
+ if (/^__/.test(symbol)) {
114
+ internals.push(symbol);
115
+ continue;
116
+ }
117
+ if (/^[A-Z0-9_]+$/.test(symbol)) {
118
+ constants.push(symbol);
119
+ continue;
120
+ }
121
+ if (/^[a-z]/.test(symbol)) {
122
+ functions.push(symbol);
123
+ continue;
124
+ }
125
+ if (/^[A-Z]/.test(symbol)) {
126
+ classesOrTypes.push(symbol);
127
+ continue;
128
+ }
129
+ others.push(symbol);
130
+ }
131
+
132
+ return {
133
+ providers: sortStrings(providers),
134
+ constants: sortStrings(constants),
135
+ functions: sortStrings(functions),
136
+ classesOrTypes: sortStrings(classesOrTypes),
137
+ internals: sortStrings(internals),
138
+ others: sortStrings(others)
139
+ };
140
+ }
141
+
142
+ async function collectExportFileSymbolSummaries({ packageRoot, packageExports, notes }) {
143
+ const rootDir = String(packageRoot || "").trim();
144
+ if (!rootDir) {
145
+ return [];
146
+ }
147
+
148
+ const exportTargets = new Map();
149
+ for (const entry of ensureArray(packageExports)) {
150
+ const record = ensureObject(entry);
151
+ if (record.targetType !== "file" || record.targetExists !== true) {
152
+ continue;
153
+ }
154
+
155
+ const target = String(record.target || "").trim();
156
+ if (!target.startsWith("./")) {
157
+ continue;
158
+ }
159
+ const normalizedTarget = normalizeRelativePosixPath(target.replace(/^\.\//, ""));
160
+ const basename = path.posix.basename(normalizedTarget);
161
+ if (!/\.(?:js|mjs|cjs)$/i.test(basename)) {
162
+ continue;
163
+ }
164
+
165
+ if (!exportTargets.has(normalizedTarget)) {
166
+ exportTargets.set(normalizedTarget, {
167
+ file: normalizedTarget,
168
+ subpaths: new Set(),
169
+ conditions: new Set()
170
+ });
171
+ }
172
+ const bucket = exportTargets.get(normalizedTarget);
173
+ bucket.subpaths.add(String(record.subpath || ".").trim() || ".");
174
+ const condition = String(record.condition || "default").trim() || "default";
175
+ if (condition !== "default") {
176
+ bucket.conditions.add(condition);
177
+ }
178
+ }
179
+
180
+ const summaries = [];
181
+ for (const [relativeTargetPath, bucket] of exportTargets.entries()) {
182
+ const absoluteTargetPath = path.resolve(rootDir, relativeTargetPath);
183
+ if (!(await fileExists(absoluteTargetPath))) {
184
+ ensureArray(notes).push(`Export file missing: ${relativeTargetPath}`);
185
+ continue;
186
+ }
187
+
188
+ let source = "";
189
+ try {
190
+ source = await readFile(absoluteTargetPath, "utf8");
191
+ } catch (error) {
192
+ ensureArray(notes).push(
193
+ `Failed to read export file ${relativeTargetPath}: ${String(error?.message || error || "unknown error")}`
194
+ );
195
+ continue;
196
+ }
197
+
198
+ const summary = parseExportedSymbolsFromSource(source);
199
+ summaries.push({
200
+ file: normalizeRelativePosixPath(relativeTargetPath),
201
+ subpaths: sortStrings([...bucket.subpaths]),
202
+ conditions: sortStrings([...bucket.conditions]),
203
+ symbols: ensureArray(summary.symbols),
204
+ hasDefaultExport: Boolean(summary.hasDefaultExport),
205
+ starReExports: ensureArray(summary.starReExports),
206
+ namedReExports: ensureArray(summary.namedReExports)
207
+ });
208
+ }
209
+
210
+ return summaries.sort((left, right) => String(left.file || "").localeCompare(String(right.file || "")));
211
+ }
212
+
213
+ export {
214
+ classifyExportedSymbols,
215
+ collectExportFileSymbolSummaries
216
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ ensureArray,
3
+ ensureObject
4
+ } from "../../shared/collectionUtils.js";
5
+
6
+ function normalizePlacementOutlets(value) {
7
+ const outlets = [];
8
+ const source = ensureArray(value);
9
+ for (const entry of source) {
10
+ const record = ensureObject(entry);
11
+ const host = String(record.host || "").trim();
12
+ const position = String(record.position || "").trim();
13
+ if (!host || !position) {
14
+ continue;
15
+ }
16
+
17
+ const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
18
+ const description = String(record.description || "").trim();
19
+ const sourceLabel = String(record.source || "").trim();
20
+ outlets.push(
21
+ Object.freeze({
22
+ host,
23
+ position,
24
+ surfaces: Object.freeze(surfaces),
25
+ description,
26
+ source: sourceLabel
27
+ })
28
+ );
29
+ }
30
+
31
+ return Object.freeze(
32
+ [...outlets].sort((left, right) => {
33
+ const hostCompare = left.host.localeCompare(right.host);
34
+ if (hostCompare !== 0) {
35
+ return hostCompare;
36
+ }
37
+ return left.position.localeCompare(right.position);
38
+ })
39
+ );
40
+ }
41
+
42
+ function normalizePlacementContributions(value) {
43
+ const contributions = [];
44
+ for (const entry of ensureArray(value)) {
45
+ const record = ensureObject(entry);
46
+ const id = String(record.id || "").trim();
47
+ const host = String(record.host || "").trim();
48
+ const position = String(record.position || "").trim();
49
+ if (!id || !host || !position) {
50
+ continue;
51
+ }
52
+
53
+ const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
54
+ const componentToken = String(record.componentToken || "").trim();
55
+ const when = String(record.when || "").trim();
56
+ const description = String(record.description || "").trim();
57
+ const source = String(record.source || "").trim();
58
+ const parsedOrder = Number(record.order);
59
+ const order = Number.isFinite(parsedOrder) ? Math.trunc(parsedOrder) : null;
60
+ contributions.push(
61
+ Object.freeze({
62
+ id,
63
+ host,
64
+ position,
65
+ surfaces: Object.freeze(surfaces),
66
+ order,
67
+ componentToken,
68
+ when,
69
+ description,
70
+ source
71
+ })
72
+ );
73
+ }
74
+
75
+ return Object.freeze(
76
+ [...contributions].sort((left, right) => {
77
+ const hostCompare = left.host.localeCompare(right.host);
78
+ if (hostCompare !== 0) {
79
+ return hostCompare;
80
+ }
81
+ const positionCompare = left.position.localeCompare(right.position);
82
+ if (positionCompare !== 0) {
83
+ return positionCompare;
84
+ }
85
+ const leftOrder = Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
86
+ const rightOrder = Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
87
+ if (leftOrder !== rightOrder) {
88
+ return leftOrder - rightOrder;
89
+ }
90
+ return left.id.localeCompare(right.id);
91
+ })
92
+ );
93
+ }
94
+
95
+ export {
96
+ normalizePlacementContributions,
97
+ normalizePlacementOutlets
98
+ };