@jskit-ai/jskit-cli 0.2.11 → 0.2.13
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/package.json +2 -2
- package/src/server/argParser.js +187 -0
- package/src/server/cliError.js +2 -1
- package/src/server/cliRuntime.js +4359 -0
- package/src/server/index.js +1 -4604
- package/src/server/optionInterpolation.js +19 -7
- package/src/server/runCli.js +103 -0
- package/src/server/runtimeDeps.js +55 -0
|
@@ -0,0 +1,4359 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
constants as fsConstants,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
mkdir,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import process from "node:process";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { createCliError } from "./cliError.js";
|
|
18
|
+
import {
|
|
19
|
+
ensureArray,
|
|
20
|
+
ensureObject,
|
|
21
|
+
sortStrings
|
|
22
|
+
} from "./collectionUtils.js";
|
|
23
|
+
import {
|
|
24
|
+
createColorFormatter,
|
|
25
|
+
resolveWrapWidth,
|
|
26
|
+
writeWrappedItems
|
|
27
|
+
} from "./outputFormatting.js";
|
|
28
|
+
import {
|
|
29
|
+
BUNDLES_ROOT,
|
|
30
|
+
CATALOG_PACKAGES_PATH,
|
|
31
|
+
CLI_PACKAGE_ROOT,
|
|
32
|
+
MODULES_ROOT,
|
|
33
|
+
WORKSPACE_ROOT
|
|
34
|
+
} from "./pathResolution.js";
|
|
35
|
+
import {
|
|
36
|
+
appendTextSnippet,
|
|
37
|
+
escapeRegExp,
|
|
38
|
+
interpolateOptionValue,
|
|
39
|
+
normalizeSkipChecks,
|
|
40
|
+
promptForRequiredOption
|
|
41
|
+
} from "./optionInterpolation.js";
|
|
42
|
+
import { createCommandHandlers } from "./commandHandlers.js";
|
|
43
|
+
import { parseArgs, printUsage } from "./argParser.js";
|
|
44
|
+
import { createCommandHandlerDeps } from "./runtimeDeps.js";
|
|
45
|
+
import { createRunCli } from "./runCli.js";
|
|
46
|
+
|
|
47
|
+
const LOCK_RELATIVE_PATH = ".jskit/lock.json";
|
|
48
|
+
const LOCK_VERSION = 1;
|
|
49
|
+
const VITE_DEV_PROXY_CONFIG_RELATIVE_PATH = ".jskit/vite.dev.proxy.json";
|
|
50
|
+
const VITE_DEV_PROXY_CONFIG_VERSION = 1;
|
|
51
|
+
const PUBLIC_APP_CONFIG_RELATIVE_PATH = "config/public.js";
|
|
52
|
+
const SERVER_APP_CONFIG_RELATIVE_PATH = "config/server.js";
|
|
53
|
+
const PACKAGE_INSTALL_MODE_INSTALLABLE = "installable";
|
|
54
|
+
const PACKAGE_INSTALL_MODE_CLONE_ONLY = "clone-only";
|
|
55
|
+
const PACKAGE_INSTALL_MODES = Object.freeze([PACKAGE_INSTALL_MODE_INSTALLABLE, PACKAGE_INSTALL_MODE_CLONE_ONLY]);
|
|
56
|
+
const WORKSPACE_VISIBILITY_LEVELS = Object.freeze(["workspace", "workspace_user"]);
|
|
57
|
+
const WORKSPACE_VISIBILITY_SET = new Set(WORKSPACE_VISIBILITY_LEVELS);
|
|
58
|
+
const MATERIALIZED_PACKAGE_ROOTS = new Map();
|
|
59
|
+
const MATERIALIZED_PACKAGE_TEMP_DIRECTORIES = new Set();
|
|
60
|
+
const BUILTIN_CAPABILITY_PROVIDERS = Object.freeze({
|
|
61
|
+
"runtime.actions": Object.freeze(["@jskit-ai/kernel"])
|
|
62
|
+
});
|
|
63
|
+
const SETTINGS_FIELDS_CONTRACT_TARGETS = Object.freeze({
|
|
64
|
+
"packages/main/src/shared/resources/consoleSettingsFields.js": Object.freeze({
|
|
65
|
+
contractId: "users.settings-fields.console.v1",
|
|
66
|
+
marker: "@jskit-contract users.settings-fields.console.v1",
|
|
67
|
+
requiredSnippets: Object.freeze([
|
|
68
|
+
"defineField",
|
|
69
|
+
"resetConsoleSettingsFields"
|
|
70
|
+
])
|
|
71
|
+
}),
|
|
72
|
+
"packages/main/src/shared/resources/workspaceSettingsFields.js": Object.freeze({
|
|
73
|
+
contractId: "users.settings-fields.workspace.v1",
|
|
74
|
+
marker: "@jskit-contract users.settings-fields.workspace.v1",
|
|
75
|
+
requiredSnippets: Object.freeze([
|
|
76
|
+
"defineField",
|
|
77
|
+
"resetWorkspaceSettingsFields"
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function normalizeMutationExtension(value) {
|
|
83
|
+
const extension = String(value || "").trim();
|
|
84
|
+
if (!extension) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
if (extension.startsWith(".")) {
|
|
88
|
+
return extension;
|
|
89
|
+
}
|
|
90
|
+
return `.${extension}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeFileMutationRecord(value) {
|
|
94
|
+
const record = ensureObject(value);
|
|
95
|
+
const op = String(record.op || "copy-file").trim().toLowerCase() || "copy-file";
|
|
96
|
+
return {
|
|
97
|
+
op,
|
|
98
|
+
from: String(record.from || "").trim(),
|
|
99
|
+
to: String(record.to || "").trim(),
|
|
100
|
+
toSurface: String(record.toSurface || "").trim(),
|
|
101
|
+
toSurfacePath: String(record.toSurfacePath || "").trim(),
|
|
102
|
+
toSurfaceRoot: record.toSurfaceRoot === true,
|
|
103
|
+
toDir: String(record.toDir || "").trim(),
|
|
104
|
+
slug: String(record.slug || "").trim(),
|
|
105
|
+
extension: normalizeMutationExtension(record.extension),
|
|
106
|
+
preserveOnRemove: record.preserveOnRemove === true,
|
|
107
|
+
id: String(record.id || "").trim(),
|
|
108
|
+
category: String(record.category || "").trim(),
|
|
109
|
+
reason: String(record.reason || "").trim(),
|
|
110
|
+
when: normalizeMutationWhen(record.when)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeMutationWhen(value) {
|
|
115
|
+
const source = ensureObject(value);
|
|
116
|
+
const option = String(source.option || "").trim();
|
|
117
|
+
const config = String(source.config || "").trim();
|
|
118
|
+
const equals = String(source.equals || "").trim();
|
|
119
|
+
const notEquals = String(source.notEquals || "").trim();
|
|
120
|
+
const includes = ensureArray(source.in).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
121
|
+
const excludes = ensureArray(source.notIn).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
122
|
+
|
|
123
|
+
if (!option && !config) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
option,
|
|
129
|
+
config,
|
|
130
|
+
equals,
|
|
131
|
+
notEquals,
|
|
132
|
+
includes,
|
|
133
|
+
excludes
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readObjectPath(source, rawPath) {
|
|
138
|
+
const valueSource = ensureObject(source);
|
|
139
|
+
const fullPath = String(rawPath || "").trim();
|
|
140
|
+
if (!fullPath) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(valueSource, fullPath)) {
|
|
145
|
+
return valueSource[fullPath];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const segments = fullPath
|
|
149
|
+
.split(".")
|
|
150
|
+
.map((entry) => String(entry || "").trim())
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
if (segments.length < 1) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let cursor = valueSource;
|
|
157
|
+
for (const segment of segments) {
|
|
158
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, segment)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
cursor = cursor[segment];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return cursor;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeWhenSourceValue(value) {
|
|
171
|
+
if (value == null) {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
if (typeof value === "string") {
|
|
175
|
+
return value.trim();
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
178
|
+
return String(value).trim();
|
|
179
|
+
}
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveWhenConfigValue(configContext = {}, configPath = "") {
|
|
184
|
+
const normalizedPath = String(configPath || "").trim();
|
|
185
|
+
if (!normalizedPath) {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const publicConfig = ensureObject(configContext.public);
|
|
190
|
+
const serverConfig = ensureObject(configContext.server);
|
|
191
|
+
const mergedConfig = ensureObject(configContext.merged);
|
|
192
|
+
if (normalizedPath === "public") {
|
|
193
|
+
return publicConfig;
|
|
194
|
+
}
|
|
195
|
+
if (normalizedPath === "server") {
|
|
196
|
+
return serverConfig;
|
|
197
|
+
}
|
|
198
|
+
if (normalizedPath === "merged") {
|
|
199
|
+
return mergedConfig;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (normalizedPath.startsWith("public.")) {
|
|
203
|
+
return readObjectPath(publicConfig, normalizedPath.slice("public.".length));
|
|
204
|
+
}
|
|
205
|
+
if (normalizedPath.startsWith("server.")) {
|
|
206
|
+
return readObjectPath(serverConfig, normalizedPath.slice("server.".length));
|
|
207
|
+
}
|
|
208
|
+
if (normalizedPath.startsWith("merged.")) {
|
|
209
|
+
return readObjectPath(mergedConfig, normalizedPath.slice("merged.".length));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return readObjectPath(mergedConfig, normalizedPath);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function shouldApplyMutationWhen(
|
|
216
|
+
when,
|
|
217
|
+
{
|
|
218
|
+
options = {},
|
|
219
|
+
configContext = {},
|
|
220
|
+
packageId = "",
|
|
221
|
+
mutationContext = "mutation"
|
|
222
|
+
} = {}
|
|
223
|
+
) {
|
|
224
|
+
if (!when || typeof when !== "object") {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const optionName = String(when.option || "").trim();
|
|
229
|
+
const configPath = String(when.config || "").trim();
|
|
230
|
+
if (!optionName && !configPath) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (optionName && configPath) {
|
|
234
|
+
const packagePrefix = packageId ? `${packageId} ` : "";
|
|
235
|
+
throw createCliError(
|
|
236
|
+
`Invalid ${packagePrefix}${mutationContext}: when cannot declare both "option" and "config".`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const sourceValue = optionName
|
|
241
|
+
? readObjectPath(options, optionName)
|
|
242
|
+
: resolveWhenConfigValue(configContext, configPath);
|
|
243
|
+
const optionValue = normalizeWhenSourceValue(sourceValue);
|
|
244
|
+
const equals = String(when.equals || "").trim();
|
|
245
|
+
const notEquals = String(when.notEquals || "").trim();
|
|
246
|
+
const includes = ensureArray(when.includes).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
247
|
+
const excludes = ensureArray(when.excludes).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
248
|
+
|
|
249
|
+
if (equals && optionValue !== equals) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (notEquals && optionValue === notEquals) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (includes.length > 0 && !includes.includes(optionValue)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (excludes.length > 0 && excludes.includes(optionValue)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildFileWriteGroups(fileMutations) {
|
|
266
|
+
const groups = [];
|
|
267
|
+
const groupsByKey = new Map();
|
|
268
|
+
|
|
269
|
+
for (const mutation of ensureArray(fileMutations)) {
|
|
270
|
+
const normalized = normalizeFileMutationRecord(mutation);
|
|
271
|
+
if (normalized.op === "install-migration") {
|
|
272
|
+
if (!normalized.from || !normalized.slug) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
} else if (!normalized.from || (!normalized.to && !normalized.toSurface)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const destinationLabel = normalized.to
|
|
280
|
+
? normalized.to
|
|
281
|
+
: normalized.toSurfaceRoot
|
|
282
|
+
? `surface:${normalized.toSurface}.root`
|
|
283
|
+
: `surface:${normalized.toSurface}/${normalized.toSurfacePath}`;
|
|
284
|
+
|
|
285
|
+
const key = normalized.id
|
|
286
|
+
? `id:${normalized.id}`
|
|
287
|
+
: normalized.category || normalized.reason
|
|
288
|
+
? `meta:${normalized.category}::${normalized.reason}`
|
|
289
|
+
: `path:${destinationLabel}`;
|
|
290
|
+
|
|
291
|
+
let group = groupsByKey.get(key);
|
|
292
|
+
if (!group) {
|
|
293
|
+
group = {
|
|
294
|
+
id: normalized.id,
|
|
295
|
+
category: normalized.category,
|
|
296
|
+
reason: normalized.reason,
|
|
297
|
+
files: []
|
|
298
|
+
};
|
|
299
|
+
groupsByKey.set(key, group);
|
|
300
|
+
groups.push(group);
|
|
301
|
+
} else {
|
|
302
|
+
if (!group.category && normalized.category) {
|
|
303
|
+
group.category = normalized.category;
|
|
304
|
+
}
|
|
305
|
+
if (!group.reason && normalized.reason) {
|
|
306
|
+
group.reason = normalized.reason;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (normalized.op === "install-migration") {
|
|
311
|
+
const toDir = normalized.toDir || "migrations";
|
|
312
|
+
const extension = normalized.extension || ".cjs";
|
|
313
|
+
group.files.push({
|
|
314
|
+
from: normalized.from,
|
|
315
|
+
to: `${toDir}/<timestamp>_${normalized.slug}${extension}`
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
group.files.push({
|
|
321
|
+
from: normalized.from,
|
|
322
|
+
to: destinationLabel
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return groups;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizeRelativePath(fromRoot, absolutePath) {
|
|
330
|
+
return path.relative(fromRoot, absolutePath).split(path.sep).join("/");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function hashBuffer(buffer) {
|
|
334
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatMigrationTimestamp(date = new Date()) {
|
|
338
|
+
const source = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
|
|
339
|
+
const year = source.getUTCFullYear();
|
|
340
|
+
const month = String(source.getUTCMonth() + 1).padStart(2, "0");
|
|
341
|
+
const day = String(source.getUTCDate()).padStart(2, "0");
|
|
342
|
+
const hours = String(source.getUTCHours()).padStart(2, "0");
|
|
343
|
+
const minutes = String(source.getUTCMinutes()).padStart(2, "0");
|
|
344
|
+
const seconds = String(source.getUTCSeconds()).padStart(2, "0");
|
|
345
|
+
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function normalizeMigrationSlug(value, packageId) {
|
|
349
|
+
const normalized = String(value || "")
|
|
350
|
+
.trim()
|
|
351
|
+
.toLowerCase()
|
|
352
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
353
|
+
.replace(/^_+|_+$/g, "");
|
|
354
|
+
if (!normalized) {
|
|
355
|
+
throw createCliError(`Invalid install-migration mutation in ${packageId}: \"slug\" is required.`);
|
|
356
|
+
}
|
|
357
|
+
return normalized;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeMigrationExtension(value = "", fallback = ".cjs") {
|
|
361
|
+
const normalizedFallback = String(fallback || ".cjs").trim() || ".cjs";
|
|
362
|
+
const raw = String(value || "").trim();
|
|
363
|
+
const candidate = raw ? (raw.startsWith(".") ? raw : `.${raw}`) : normalizedFallback;
|
|
364
|
+
if (!/^\.[a-z0-9]+$/i.test(candidate)) {
|
|
365
|
+
throw createCliError(`Invalid install-migration extension: ${candidate}`);
|
|
366
|
+
}
|
|
367
|
+
return candidate.toLowerCase();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const JSKIT_MIGRATION_ID_PATTERN = /JSKIT_MIGRATION_ID:\s*([A-Za-z0-9._-]+)/i;
|
|
371
|
+
|
|
372
|
+
function extractMigrationIdFromSource(source) {
|
|
373
|
+
const content = String(source || "");
|
|
374
|
+
const match = content.match(JSKIT_MIGRATION_ID_PATTERN);
|
|
375
|
+
if (!match) {
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
return String(match[1] || "").trim();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function findExistingMigrationById({ appRoot, migrationsDirectory, migrationId }) {
|
|
382
|
+
const normalizedMigrationId = String(migrationId || "").trim();
|
|
383
|
+
if (!normalizedMigrationId) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const absoluteDirectory = path.join(appRoot, migrationsDirectory);
|
|
388
|
+
if (!(await fileExists(absoluteDirectory))) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const entries = await readdir(absoluteDirectory, { withFileTypes: true }).catch(() => []);
|
|
393
|
+
for (const entry of entries) {
|
|
394
|
+
if (!entry.isFile()) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const absolutePath = path.join(absoluteDirectory, entry.name);
|
|
398
|
+
const fileContent = await readFile(absolutePath, "utf8").catch(() => "");
|
|
399
|
+
const fileMigrationId = extractMigrationIdFromSource(fileContent);
|
|
400
|
+
if (!fileMigrationId || fileMigrationId !== normalizedMigrationId) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
path: normalizeRelativePath(appRoot, absolutePath)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function toScopedPackageId(input) {
|
|
413
|
+
const raw = String(input || "").trim();
|
|
414
|
+
if (!raw) {
|
|
415
|
+
return "";
|
|
416
|
+
}
|
|
417
|
+
if (raw.startsWith("@")) {
|
|
418
|
+
return raw;
|
|
419
|
+
}
|
|
420
|
+
return `@jskit-ai/${raw}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function resolvePackageIdInput(input, packageRegistry) {
|
|
424
|
+
const raw = String(input || "").trim();
|
|
425
|
+
if (!raw) {
|
|
426
|
+
return "";
|
|
427
|
+
}
|
|
428
|
+
if (packageRegistry.has(raw)) {
|
|
429
|
+
return raw;
|
|
430
|
+
}
|
|
431
|
+
const scoped = toScopedPackageId(raw);
|
|
432
|
+
if (scoped && packageRegistry.has(scoped)) {
|
|
433
|
+
return scoped;
|
|
434
|
+
}
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function resolveInstalledPackageIdInput(input, installedPackages) {
|
|
439
|
+
const raw = String(input || "").trim();
|
|
440
|
+
if (!raw) {
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
if (Object.prototype.hasOwnProperty.call(installedPackages, raw)) {
|
|
444
|
+
return raw;
|
|
445
|
+
}
|
|
446
|
+
const scoped = toScopedPackageId(raw);
|
|
447
|
+
if (scoped && Object.prototype.hasOwnProperty.call(installedPackages, scoped)) {
|
|
448
|
+
return scoped;
|
|
449
|
+
}
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function fileExists(absolutePath) {
|
|
454
|
+
try {
|
|
455
|
+
await access(absolutePath, fsConstants.F_OK);
|
|
456
|
+
return true;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function readJsonFile(absolutePath) {
|
|
463
|
+
const source = await readFile(absolutePath, "utf8");
|
|
464
|
+
return JSON.parse(source);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function writeJsonFile(absolutePath, value) {
|
|
468
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
469
|
+
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function readFileBufferIfExists(absolutePath) {
|
|
473
|
+
if (!(await fileExists(absolutePath))) {
|
|
474
|
+
return {
|
|
475
|
+
exists: false,
|
|
476
|
+
buffer: Buffer.alloc(0)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
exists: true,
|
|
482
|
+
buffer: await readFile(absolutePath)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function loadAppConfigModuleConfig(appRoot, relativePath) {
|
|
487
|
+
const absolutePath = path.join(appRoot, relativePath);
|
|
488
|
+
if (!(await fileExists(absolutePath))) {
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let moduleNamespace = null;
|
|
493
|
+
try {
|
|
494
|
+
moduleNamespace = await import(`${pathToFileURL(absolutePath).href}?t=${Date.now()}_${Math.random()}`);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
throw createCliError(
|
|
497
|
+
`Unable to load ${relativePath}: ${String(error?.message || error || "unknown error")}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const defaultExport = ensureObject(moduleNamespace?.default);
|
|
502
|
+
const fromNamedExport = ensureObject(moduleNamespace?.config);
|
|
503
|
+
const fromDefaultConfig = ensureObject(defaultExport?.config);
|
|
504
|
+
|
|
505
|
+
if (Object.keys(fromNamedExport).length > 0) {
|
|
506
|
+
return fromNamedExport;
|
|
507
|
+
}
|
|
508
|
+
if (Object.keys(fromDefaultConfig).length > 0) {
|
|
509
|
+
return fromDefaultConfig;
|
|
510
|
+
}
|
|
511
|
+
return defaultExport;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function loadMutationWhenConfigContext(appRoot) {
|
|
515
|
+
const publicConfig = await loadAppConfigModuleConfig(appRoot, PUBLIC_APP_CONFIG_RELATIVE_PATH);
|
|
516
|
+
const serverConfig = await loadAppConfigModuleConfig(appRoot, SERVER_APP_CONFIG_RELATIVE_PATH);
|
|
517
|
+
return {
|
|
518
|
+
public: publicConfig,
|
|
519
|
+
server: serverConfig,
|
|
520
|
+
merged: {
|
|
521
|
+
...publicConfig,
|
|
522
|
+
...serverConfig
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function createEmptyViteDevProxyConfig() {
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
version: VITE_DEV_PROXY_CONFIG_VERSION,
|
|
530
|
+
entries: Object.freeze([])
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeViteDevProxyPath(value = "", { context = "vite proxy entry" } = {}) {
|
|
535
|
+
const normalizedPath = String(value || "").trim();
|
|
536
|
+
if (!normalizedPath || !normalizedPath.startsWith("/")) {
|
|
537
|
+
throw createCliError(`${context} requires "path" starting with "/".`);
|
|
538
|
+
}
|
|
539
|
+
return normalizedPath.replace(/\/{2,}/g, "/");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function normalizeViteDevProxyEntry(value = {}, { context = "vite proxy entry" } = {}) {
|
|
543
|
+
const source = ensureObject(value);
|
|
544
|
+
const packageId = String(source.packageId || "").trim();
|
|
545
|
+
const entryId = String(source.id || "").trim();
|
|
546
|
+
if (!packageId) {
|
|
547
|
+
throw createCliError(`${context} requires "packageId".`);
|
|
548
|
+
}
|
|
549
|
+
if (!entryId) {
|
|
550
|
+
throw createCliError(`${context} requires "id".`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const normalized = {
|
|
554
|
+
packageId,
|
|
555
|
+
id: entryId,
|
|
556
|
+
path: normalizeViteDevProxyPath(source.path, {
|
|
557
|
+
context: `${context} (${packageId}:${entryId})`
|
|
558
|
+
})
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const target = String(source.target || "").trim();
|
|
562
|
+
if (target) {
|
|
563
|
+
normalized.target = target;
|
|
564
|
+
}
|
|
565
|
+
if (Object.prototype.hasOwnProperty.call(source, "changeOrigin")) {
|
|
566
|
+
normalized.changeOrigin = source.changeOrigin === true;
|
|
567
|
+
}
|
|
568
|
+
if (Object.prototype.hasOwnProperty.call(source, "ws")) {
|
|
569
|
+
normalized.ws = source.ws === true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return Object.freeze(normalized);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeViteDevProxyConfig(value = {}, { context = "vite proxy config" } = {}) {
|
|
576
|
+
const source = ensureObject(value);
|
|
577
|
+
const normalizedEntries = [];
|
|
578
|
+
const seenEntryKeys = new Set();
|
|
579
|
+
const seenPaths = new Set();
|
|
580
|
+
|
|
581
|
+
for (const [index, entry] of ensureArray(source.entries).entries()) {
|
|
582
|
+
const normalizedEntry = normalizeViteDevProxyEntry(entry, {
|
|
583
|
+
context: `${context}.entries[${index}]`
|
|
584
|
+
});
|
|
585
|
+
const entryKey = `${normalizedEntry.packageId}::${normalizedEntry.id}`;
|
|
586
|
+
if (seenEntryKeys.has(entryKey)) {
|
|
587
|
+
throw createCliError(`${context} has duplicate entry "${entryKey}".`);
|
|
588
|
+
}
|
|
589
|
+
if (seenPaths.has(normalizedEntry.path)) {
|
|
590
|
+
throw createCliError(`${context} has duplicate path "${normalizedEntry.path}".`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
seenEntryKeys.add(entryKey);
|
|
594
|
+
seenPaths.add(normalizedEntry.path);
|
|
595
|
+
normalizedEntries.push(normalizedEntry);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
normalizedEntries.sort((left, right) => {
|
|
599
|
+
const pathDiff = left.path.localeCompare(right.path);
|
|
600
|
+
if (pathDiff !== 0) {
|
|
601
|
+
return pathDiff;
|
|
602
|
+
}
|
|
603
|
+
const packageDiff = left.packageId.localeCompare(right.packageId);
|
|
604
|
+
if (packageDiff !== 0) {
|
|
605
|
+
return packageDiff;
|
|
606
|
+
}
|
|
607
|
+
return left.id.localeCompare(right.id);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return Object.freeze({
|
|
611
|
+
version: VITE_DEV_PROXY_CONFIG_VERSION,
|
|
612
|
+
entries: Object.freeze(normalizedEntries)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function resolveViteDevProxyConfigAbsolutePath(appRoot) {
|
|
617
|
+
return path.join(appRoot, VITE_DEV_PROXY_CONFIG_RELATIVE_PATH);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function loadViteDevProxyConfig(appRoot, { context = "vite proxy config" } = {}) {
|
|
621
|
+
const absolutePath = resolveViteDevProxyConfigAbsolutePath(appRoot);
|
|
622
|
+
const existing = await readFileBufferIfExists(absolutePath);
|
|
623
|
+
if (!existing.exists) {
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
absolutePath,
|
|
626
|
+
exists: false,
|
|
627
|
+
config: createEmptyViteDevProxyConfig()
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const relativePath = normalizeRelativePath(appRoot, absolutePath);
|
|
632
|
+
let parsed = {};
|
|
633
|
+
try {
|
|
634
|
+
parsed = JSON.parse(existing.buffer.toString("utf8"));
|
|
635
|
+
} catch {
|
|
636
|
+
throw createCliError(`Invalid ${context} at ${relativePath}: expected valid JSON.`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return Object.freeze({
|
|
640
|
+
absolutePath,
|
|
641
|
+
exists: true,
|
|
642
|
+
config: normalizeViteDevProxyConfig(parsed, {
|
|
643
|
+
context: `${context} (${relativePath})`
|
|
644
|
+
})
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function writeViteDevProxyConfig(appRoot, config = {}, touchedFiles = null) {
|
|
649
|
+
const absolutePath = resolveViteDevProxyConfigAbsolutePath(appRoot);
|
|
650
|
+
const relativePath = normalizeRelativePath(appRoot, absolutePath);
|
|
651
|
+
const normalizedConfig = normalizeViteDevProxyConfig(config);
|
|
652
|
+
|
|
653
|
+
if (normalizedConfig.entries.length < 1) {
|
|
654
|
+
if (await fileExists(absolutePath)) {
|
|
655
|
+
await rm(absolutePath);
|
|
656
|
+
if (touchedFiles && typeof touchedFiles.add === "function") {
|
|
657
|
+
touchedFiles.add(relativePath);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
await writeJsonFile(absolutePath, normalizedConfig);
|
|
664
|
+
if (touchedFiles && typeof touchedFiles.add === "function") {
|
|
665
|
+
touchedFiles.add(relativePath);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function normalizeViteProxyMutationRecord(value = {}) {
|
|
670
|
+
const source = ensureObject(value);
|
|
671
|
+
const changeOrigin = Object.prototype.hasOwnProperty.call(source, "changeOrigin")
|
|
672
|
+
? source.changeOrigin === true
|
|
673
|
+
: undefined;
|
|
674
|
+
const ws = Object.prototype.hasOwnProperty.call(source, "ws") ? source.ws === true : undefined;
|
|
675
|
+
return Object.freeze({
|
|
676
|
+
id: String(source.id || "").trim(),
|
|
677
|
+
path: String(source.path || "").trim(),
|
|
678
|
+
target: String(source.target || "").trim(),
|
|
679
|
+
changeOrigin,
|
|
680
|
+
ws,
|
|
681
|
+
when: normalizeMutationWhen(source.when)
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function applyViteMutations(packageEntry, appRoot, viteMutations, options, managedVite, touchedFiles) {
|
|
686
|
+
const mutations = ensureArray(ensureObject(viteMutations).proxy).map((entry) => normalizeViteProxyMutationRecord(entry));
|
|
687
|
+
if (mutations.length < 1) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const { config: currentConfig } = await loadViteDevProxyConfig(appRoot, {
|
|
692
|
+
context: `vite proxy config for ${packageEntry.packageId}`
|
|
693
|
+
});
|
|
694
|
+
const nextEntries = [...currentConfig.entries];
|
|
695
|
+
let changed = false;
|
|
696
|
+
|
|
697
|
+
for (const mutation of mutations) {
|
|
698
|
+
const configContext = mutation.when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
699
|
+
if (
|
|
700
|
+
!shouldApplyMutationWhen(mutation.when, {
|
|
701
|
+
options,
|
|
702
|
+
configContext,
|
|
703
|
+
packageId: packageEntry.packageId,
|
|
704
|
+
mutationContext: "vite proxy mutation"
|
|
705
|
+
})
|
|
706
|
+
) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const normalizedId = interpolateOptionValue(
|
|
711
|
+
mutation.id,
|
|
712
|
+
options,
|
|
713
|
+
packageEntry.packageId,
|
|
714
|
+
"mutations.vite.proxy.id"
|
|
715
|
+
);
|
|
716
|
+
if (!normalizedId) {
|
|
717
|
+
throw createCliError(`Invalid vite proxy mutation in ${packageEntry.packageId}: "id" is required.`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const normalizedPath = normalizeViteDevProxyPath(
|
|
721
|
+
interpolateOptionValue(
|
|
722
|
+
mutation.path,
|
|
723
|
+
options,
|
|
724
|
+
packageEntry.packageId,
|
|
725
|
+
`mutations.vite.proxy.${normalizedId}.path`
|
|
726
|
+
),
|
|
727
|
+
{
|
|
728
|
+
context: `Invalid vite proxy mutation in ${packageEntry.packageId} (${normalizedId})`
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const normalizedTarget = mutation.target
|
|
733
|
+
? String(
|
|
734
|
+
interpolateOptionValue(
|
|
735
|
+
mutation.target,
|
|
736
|
+
options,
|
|
737
|
+
packageEntry.packageId,
|
|
738
|
+
`mutations.vite.proxy.${normalizedId}.target`
|
|
739
|
+
) || ""
|
|
740
|
+
).trim()
|
|
741
|
+
: "";
|
|
742
|
+
|
|
743
|
+
for (let index = nextEntries.length - 1; index >= 0; index -= 1) {
|
|
744
|
+
const entry = nextEntries[index];
|
|
745
|
+
if (entry.packageId === packageEntry.packageId && entry.id === normalizedId) {
|
|
746
|
+
nextEntries.splice(index, 1);
|
|
747
|
+
changed = true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const conflictingEntry = nextEntries.find((entry) => entry.path === normalizedPath);
|
|
752
|
+
if (conflictingEntry) {
|
|
753
|
+
throw createCliError(
|
|
754
|
+
`Invalid vite proxy mutation in ${packageEntry.packageId}: path "${normalizedPath}" conflicts with ${conflictingEntry.packageId} (${conflictingEntry.id}).`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
nextEntries.push(
|
|
759
|
+
Object.freeze({
|
|
760
|
+
packageId: packageEntry.packageId,
|
|
761
|
+
id: normalizedId,
|
|
762
|
+
path: normalizedPath,
|
|
763
|
+
...(normalizedTarget ? { target: normalizedTarget } : {}),
|
|
764
|
+
...(typeof mutation.changeOrigin === "boolean" ? { changeOrigin: mutation.changeOrigin } : {}),
|
|
765
|
+
...(typeof mutation.ws === "boolean" ? { ws: mutation.ws } : {})
|
|
766
|
+
})
|
|
767
|
+
);
|
|
768
|
+
changed = true;
|
|
769
|
+
|
|
770
|
+
const mutationKey = `${normalizedPath}::${normalizedId}`;
|
|
771
|
+
managedVite[mutationKey] = Object.freeze({
|
|
772
|
+
op: "upsert-vite-proxy",
|
|
773
|
+
id: normalizedId,
|
|
774
|
+
path: normalizedPath
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!changed) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const nextConfig = normalizeViteDevProxyConfig(
|
|
783
|
+
{
|
|
784
|
+
entries: nextEntries
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
context: `vite proxy config for ${packageEntry.packageId}`
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
if (JSON.stringify(currentConfig) === JSON.stringify(nextConfig)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function removeManagedViteProxyEntries({ appRoot, packageId, managedViteChanges = {}, touchedFiles = null } = {}) {
|
|
799
|
+
const managedChanges = Object.values(ensureObject(managedViteChanges))
|
|
800
|
+
.map((entry) => ensureObject(entry))
|
|
801
|
+
.filter((entry) => String(entry.op || "").trim() === "upsert-vite-proxy");
|
|
802
|
+
if (managedChanges.length < 1) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const { exists, config: currentConfig } = await loadViteDevProxyConfig(appRoot, {
|
|
807
|
+
context: `vite proxy config while removing ${packageId}`
|
|
808
|
+
});
|
|
809
|
+
if (!exists) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
let nextEntries = [...currentConfig.entries];
|
|
814
|
+
for (const change of managedChanges) {
|
|
815
|
+
const changeId = String(change.id || "").trim();
|
|
816
|
+
const changePath = String(change.path || "").trim();
|
|
817
|
+
if (!changeId) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
nextEntries = nextEntries.filter((entry) => {
|
|
821
|
+
if (entry.packageId !== packageId || entry.id !== changeId) {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
if (changePath && entry.path !== changePath) {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const nextConfig = normalizeViteDevProxyConfig(
|
|
832
|
+
{
|
|
833
|
+
entries: nextEntries
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
context: `vite proxy config while removing ${packageId}`
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
if (JSON.stringify(currentConfig) === JSON.stringify(nextConfig)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function resolveAppRootFromCwd(cwd) {
|
|
847
|
+
const startDirectory = path.resolve(String(cwd || process.cwd()));
|
|
848
|
+
let currentDirectory = startDirectory;
|
|
849
|
+
|
|
850
|
+
while (true) {
|
|
851
|
+
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
852
|
+
if (await fileExists(packageJsonPath)) {
|
|
853
|
+
return currentDirectory;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
857
|
+
if (parentDirectory === currentDirectory) {
|
|
858
|
+
throw createCliError(
|
|
859
|
+
`Could not locate package.json starting from ${startDirectory}. Run jskit from an app directory (or a child directory of one).`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
currentDirectory = parentDirectory;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function loadAppPackageJson(appRoot) {
|
|
867
|
+
const packageJsonPath = path.join(appRoot, "package.json");
|
|
868
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
869
|
+
return {
|
|
870
|
+
packageJsonPath,
|
|
871
|
+
packageJson
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function createDefaultLock() {
|
|
876
|
+
return {
|
|
877
|
+
lockVersion: LOCK_VERSION,
|
|
878
|
+
installedPackages: {}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function loadLockFile(appRoot) {
|
|
883
|
+
const lockPath = path.join(appRoot, LOCK_RELATIVE_PATH);
|
|
884
|
+
if (!(await fileExists(lockPath))) {
|
|
885
|
+
return {
|
|
886
|
+
lockPath,
|
|
887
|
+
lock: createDefaultLock()
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const lock = await readJsonFile(lockPath);
|
|
892
|
+
const installedPackages = ensureObject(lock?.installedPackages);
|
|
893
|
+
const lockVersion = Number(lock?.lockVersion);
|
|
894
|
+
return {
|
|
895
|
+
lockPath,
|
|
896
|
+
lock: {
|
|
897
|
+
lockVersion: Number.isFinite(lockVersion) && lockVersion > 0 ? lockVersion : LOCK_VERSION,
|
|
898
|
+
installedPackages
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function createManagedPackageJsonChange(hadPrevious, previousValue, value) {
|
|
904
|
+
return {
|
|
905
|
+
hadPrevious: Boolean(hadPrevious),
|
|
906
|
+
previousValue: hadPrevious ? String(previousValue) : "",
|
|
907
|
+
value: String(value)
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function ensurePackageJsonSection(packageJson, sectionName) {
|
|
912
|
+
const sectionValue = ensureObject(packageJson[sectionName]);
|
|
913
|
+
packageJson[sectionName] = sectionValue;
|
|
914
|
+
return sectionValue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function applyPackageJsonField(packageJson, sectionName, key, value) {
|
|
918
|
+
const section = ensurePackageJsonSection(packageJson, sectionName);
|
|
919
|
+
const nextValue = String(value);
|
|
920
|
+
const hadPrevious = Object.prototype.hasOwnProperty.call(section, key);
|
|
921
|
+
const previousValue = hadPrevious ? String(section[key]) : "";
|
|
922
|
+
const changed = !hadPrevious || previousValue !== nextValue;
|
|
923
|
+
section[key] = nextValue;
|
|
924
|
+
return {
|
|
925
|
+
changed,
|
|
926
|
+
managed: createManagedPackageJsonChange(hadPrevious, previousValue, nextValue)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function removePackageJsonField(packageJson, sectionName, key) {
|
|
931
|
+
const section = ensureObject(packageJson[sectionName]);
|
|
932
|
+
if (!Object.prototype.hasOwnProperty.call(section, key)) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
delete section[key];
|
|
936
|
+
if (Object.keys(section).length < 1) {
|
|
937
|
+
delete packageJson[sectionName];
|
|
938
|
+
}
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function restorePackageJsonField(packageJson, sectionName, key, managedChange) {
|
|
943
|
+
const section = ensurePackageJsonSection(packageJson, sectionName);
|
|
944
|
+
const currentValue = Object.prototype.hasOwnProperty.call(section, key) ? String(section[key]) : "";
|
|
945
|
+
if (currentValue !== String(managedChange?.value || "")) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (managedChange?.hadPrevious) {
|
|
950
|
+
section[key] = String(managedChange.previousValue || "");
|
|
951
|
+
} else {
|
|
952
|
+
delete section[key];
|
|
953
|
+
}
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function parseEnvLineValue(line, key) {
|
|
958
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
959
|
+
if (!pattern.test(line)) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
const index = line.indexOf("=");
|
|
963
|
+
if (index === -1) {
|
|
964
|
+
return "";
|
|
965
|
+
}
|
|
966
|
+
return line.slice(index + 1);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function upsertEnvValue(content, key, value) {
|
|
970
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
971
|
+
const lookupPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
972
|
+
let index = -1;
|
|
973
|
+
|
|
974
|
+
for (let cursor = 0; cursor < lines.length; cursor += 1) {
|
|
975
|
+
if (lookupPattern.test(lines[cursor])) {
|
|
976
|
+
index = cursor;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const hadPrevious = index >= 0;
|
|
982
|
+
const previousValue = hadPrevious ? String(parseEnvLineValue(lines[index], key) || "") : "";
|
|
983
|
+
const nextLine = `${key}=${value}`;
|
|
984
|
+
|
|
985
|
+
if (hadPrevious) {
|
|
986
|
+
lines[index] = nextLine;
|
|
987
|
+
} else {
|
|
988
|
+
if (lines.length === 1 && lines[0] === "") {
|
|
989
|
+
lines[0] = nextLine;
|
|
990
|
+
} else {
|
|
991
|
+
lines.push(nextLine);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const normalized = `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
996
|
+
return {
|
|
997
|
+
hadPrevious,
|
|
998
|
+
previousValue,
|
|
999
|
+
content: normalized
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function removeEnvValue(content, key, expectedValue, previous) {
|
|
1004
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
1005
|
+
const lookupPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
1006
|
+
let index = -1;
|
|
1007
|
+
|
|
1008
|
+
for (let cursor = 0; cursor < lines.length; cursor += 1) {
|
|
1009
|
+
if (lookupPattern.test(lines[cursor])) {
|
|
1010
|
+
index = cursor;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (index < 0) {
|
|
1016
|
+
return {
|
|
1017
|
+
changed: false,
|
|
1018
|
+
content: content
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const currentValue = String(parseEnvLineValue(lines[index], key) || "");
|
|
1023
|
+
if (currentValue !== String(expectedValue || "")) {
|
|
1024
|
+
return {
|
|
1025
|
+
changed: false,
|
|
1026
|
+
content: content
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (previous?.hadPrevious) {
|
|
1031
|
+
lines[index] = `${key}=${String(previous.previousValue || "")}`;
|
|
1032
|
+
} else {
|
|
1033
|
+
lines.splice(index, 1);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const normalized = `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
1037
|
+
return {
|
|
1038
|
+
changed: true,
|
|
1039
|
+
content: normalized
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function normalizePackageInstallationMode(rawValue, descriptorPath) {
|
|
1044
|
+
const normalized = String(rawValue || "")
|
|
1045
|
+
.trim()
|
|
1046
|
+
.toLowerCase();
|
|
1047
|
+
if (!normalized) {
|
|
1048
|
+
return PACKAGE_INSTALL_MODE_INSTALLABLE;
|
|
1049
|
+
}
|
|
1050
|
+
if (!PACKAGE_INSTALL_MODES.includes(normalized)) {
|
|
1051
|
+
throw createCliError(
|
|
1052
|
+
`Invalid package descriptor at ${descriptorPath}: installationMode must be one of: ${PACKAGE_INSTALL_MODES.join(", ")}.`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return normalized;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
1059
|
+
const normalized = ensureObject(descriptor);
|
|
1060
|
+
const packageId = String(normalized.packageId || "").trim();
|
|
1061
|
+
const version = String(normalized.version || "").trim();
|
|
1062
|
+
|
|
1063
|
+
if (!packageId.startsWith("@jskit-ai/")) {
|
|
1064
|
+
throw createCliError(`Invalid package descriptor at ${descriptorPath}: packageId must start with @jskit-ai/.`);
|
|
1065
|
+
}
|
|
1066
|
+
if (!version) {
|
|
1067
|
+
throw createCliError(`Invalid package descriptor at ${descriptorPath}: missing version.`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const runtime = ensureObject(normalized.runtime);
|
|
1071
|
+
const server = ensureObject(runtime.server);
|
|
1072
|
+
const client = ensureObject(runtime.client);
|
|
1073
|
+
const hasServerProviders = Array.isArray(server.providers);
|
|
1074
|
+
const hasClientProviders = Array.isArray(client.providers);
|
|
1075
|
+
if (!hasServerProviders && !hasClientProviders) {
|
|
1076
|
+
throw createCliError(
|
|
1077
|
+
`Invalid package descriptor at ${descriptorPath}: runtime.server.providers or runtime.client.providers must be declared.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return {
|
|
1082
|
+
...normalized,
|
|
1083
|
+
installationMode: normalizePackageInstallationMode(normalized.installationMode, descriptorPath)
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function isCloneOnlyPackageEntry(packageEntry) {
|
|
1088
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
1089
|
+
return String(descriptor.installationMode || "").trim().toLowerCase() === PACKAGE_INSTALL_MODE_CLONE_ONLY;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { expectedPackageId = "", fallbackVersion = "" } = {}) {
|
|
1093
|
+
const normalized = ensureObject(descriptor);
|
|
1094
|
+
const packageId = String(normalized.packageId || "").trim();
|
|
1095
|
+
const version = String(normalized.version || "").trim() || String(fallbackVersion || "").trim();
|
|
1096
|
+
|
|
1097
|
+
if (!packageId) {
|
|
1098
|
+
throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing packageId.`);
|
|
1099
|
+
}
|
|
1100
|
+
if (expectedPackageId && packageId !== expectedPackageId) {
|
|
1101
|
+
throw createCliError(
|
|
1102
|
+
`Descriptor/package mismatch at ${descriptorPath}: package.descriptor.mjs has ${packageId} but package.json has ${expectedPackageId}.`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
if (!version) {
|
|
1106
|
+
throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing version.`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
...normalized,
|
|
1111
|
+
packageId,
|
|
1112
|
+
version
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function createPackageEntry({
|
|
1117
|
+
packageId,
|
|
1118
|
+
version,
|
|
1119
|
+
descriptor,
|
|
1120
|
+
rootDir = "",
|
|
1121
|
+
relativeDir = "",
|
|
1122
|
+
descriptorRelativePath = "",
|
|
1123
|
+
packageJson = {},
|
|
1124
|
+
sourceType = "",
|
|
1125
|
+
source = {}
|
|
1126
|
+
}) {
|
|
1127
|
+
const normalizedSourceType = String(sourceType || "").trim() || "package";
|
|
1128
|
+
const normalizedDescriptorPath = String(descriptorRelativePath || "").trim();
|
|
1129
|
+
const normalizedSource = {
|
|
1130
|
+
type: normalizedSourceType,
|
|
1131
|
+
...ensureObject(source)
|
|
1132
|
+
};
|
|
1133
|
+
if (!normalizedSource.descriptorPath && normalizedDescriptorPath) {
|
|
1134
|
+
normalizedSource.descriptorPath = normalizedDescriptorPath;
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
packageId: String(packageId || "").trim(),
|
|
1138
|
+
version: String(version || "").trim(),
|
|
1139
|
+
descriptor: ensureObject(descriptor),
|
|
1140
|
+
rootDir: String(rootDir || "").trim(),
|
|
1141
|
+
relativeDir: String(relativeDir || "").trim(),
|
|
1142
|
+
descriptorRelativePath: normalizedDescriptorPath,
|
|
1143
|
+
packageJson: ensureObject(packageJson),
|
|
1144
|
+
sourceType: normalizedSourceType,
|
|
1145
|
+
source: normalizedSource
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function mergePackageRegistries(...registries) {
|
|
1150
|
+
const merged = new Map();
|
|
1151
|
+
for (const registry of registries) {
|
|
1152
|
+
if (!(registry instanceof Map)) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
for (const [packageId, packageEntry] of registry.entries()) {
|
|
1156
|
+
merged.set(packageId, packageEntry);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return merged;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function validateBundleDescriptorShape(descriptor, descriptorPath) {
|
|
1163
|
+
const normalized = ensureObject(descriptor);
|
|
1164
|
+
const bundleId = String(normalized.bundleId || "").trim();
|
|
1165
|
+
const version = String(normalized.version || "").trim();
|
|
1166
|
+
const packages = ensureArray(normalized.packages).map((value) => String(value));
|
|
1167
|
+
|
|
1168
|
+
if (!bundleId) {
|
|
1169
|
+
throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: missing bundleId.`);
|
|
1170
|
+
}
|
|
1171
|
+
if (!version) {
|
|
1172
|
+
throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: missing version.`);
|
|
1173
|
+
}
|
|
1174
|
+
if (packages.length < 2) {
|
|
1175
|
+
throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: bundles must contain at least two packages.`);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return normalized;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
async function loadWorkspacePackageRegistry() {
|
|
1182
|
+
if (!MODULES_ROOT || !(await fileExists(MODULES_ROOT))) {
|
|
1183
|
+
return new Map();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const directories = [];
|
|
1187
|
+
const levelOne = await readdir(MODULES_ROOT, { withFileTypes: true });
|
|
1188
|
+
|
|
1189
|
+
for (const entry of levelOne) {
|
|
1190
|
+
if (!entry.isDirectory()) {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (entry.name.startsWith(".") || entry.name.endsWith(".LEGACY")) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const absolute = path.join(MODULES_ROOT, entry.name);
|
|
1198
|
+
const descriptorPath = path.join(absolute, "package.descriptor.mjs");
|
|
1199
|
+
if (await fileExists(descriptorPath)) {
|
|
1200
|
+
directories.push(absolute);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const nested = await readdir(absolute, { withFileTypes: true }).catch(() => []);
|
|
1205
|
+
for (const child of nested) {
|
|
1206
|
+
if (!child.isDirectory() || child.name.startsWith(".")) {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
const nestedAbsolute = path.join(absolute, child.name);
|
|
1210
|
+
const nestedDescriptor = path.join(nestedAbsolute, "package.descriptor.mjs");
|
|
1211
|
+
if (await fileExists(nestedDescriptor)) {
|
|
1212
|
+
directories.push(nestedAbsolute);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const uniqueDirectories = sortStrings([...new Set(directories)]);
|
|
1218
|
+
const registry = new Map();
|
|
1219
|
+
|
|
1220
|
+
for (const packageRoot of uniqueDirectories) {
|
|
1221
|
+
const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
|
|
1222
|
+
const descriptorModule = await import(pathToFileURL(descriptorPath).href);
|
|
1223
|
+
const descriptor = validatePackageDescriptorShape(descriptorModule?.default, descriptorPath);
|
|
1224
|
+
|
|
1225
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
1226
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
1227
|
+
throw createCliError(`Missing package.json for ${descriptor.packageId} at ${packageRoot}.`);
|
|
1228
|
+
}
|
|
1229
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
1230
|
+
const packageName = String(packageJson?.name || "").trim();
|
|
1231
|
+
if (packageName !== descriptor.packageId) {
|
|
1232
|
+
throw createCliError(
|
|
1233
|
+
`Descriptor/package mismatch at ${packageRoot}: package.descriptor.mjs has ${descriptor.packageId} but package.json has ${packageName || "(empty)"}.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const relativeDir = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, packageRoot);
|
|
1238
|
+
const descriptorRelativePath = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, descriptorPath);
|
|
1239
|
+
registry.set(
|
|
1240
|
+
descriptor.packageId,
|
|
1241
|
+
createPackageEntry({
|
|
1242
|
+
packageId: descriptor.packageId,
|
|
1243
|
+
version: descriptor.version,
|
|
1244
|
+
descriptor,
|
|
1245
|
+
rootDir: packageRoot,
|
|
1246
|
+
relativeDir,
|
|
1247
|
+
descriptorRelativePath,
|
|
1248
|
+
packageJson,
|
|
1249
|
+
sourceType: "packages-directory",
|
|
1250
|
+
source: {
|
|
1251
|
+
descriptorPath: descriptorRelativePath
|
|
1252
|
+
}
|
|
1253
|
+
})
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return registry;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function loadAppLocalPackageRegistry(appRoot) {
|
|
1261
|
+
const localPackagesRoot = path.join(appRoot, "packages");
|
|
1262
|
+
if (!(await fileExists(localPackagesRoot))) {
|
|
1263
|
+
return new Map();
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const registry = new Map();
|
|
1267
|
+
const entries = await readdir(localPackagesRoot, { withFileTypes: true });
|
|
1268
|
+
for (const entry of entries) {
|
|
1269
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const packageRoot = path.join(localPackagesRoot, entry.name);
|
|
1274
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
1275
|
+
const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
|
|
1276
|
+
if (!(await fileExists(packageJsonPath)) || !(await fileExists(descriptorPath))) {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
1281
|
+
const packageId = String(packageJson?.name || "").trim();
|
|
1282
|
+
if (!packageId) {
|
|
1283
|
+
throw createCliError(`Invalid app-local package at ${normalizeRelativePath(appRoot, packageRoot)}: package.json missing name.`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
|
|
1287
|
+
const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
|
|
1288
|
+
expectedPackageId: packageId,
|
|
1289
|
+
fallbackVersion: String(packageJson?.version || "").trim()
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const relativeDir = normalizeRelativePath(appRoot, packageRoot);
|
|
1293
|
+
const descriptorRelativePath = normalizeRelativePath(appRoot, descriptorPath);
|
|
1294
|
+
registry.set(
|
|
1295
|
+
packageId,
|
|
1296
|
+
createPackageEntry({
|
|
1297
|
+
packageId: descriptor.packageId,
|
|
1298
|
+
version: descriptor.version,
|
|
1299
|
+
descriptor,
|
|
1300
|
+
rootDir: packageRoot,
|
|
1301
|
+
relativeDir,
|
|
1302
|
+
descriptorRelativePath,
|
|
1303
|
+
packageJson,
|
|
1304
|
+
sourceType: "app-local-package",
|
|
1305
|
+
source: {
|
|
1306
|
+
packagePath: normalizeRelativePosixPath(relativeDir),
|
|
1307
|
+
descriptorPath: descriptorRelativePath
|
|
1308
|
+
}
|
|
1309
|
+
})
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return registry;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function loadCatalogPackageRegistry() {
|
|
1317
|
+
if (!(await fileExists(CATALOG_PACKAGES_PATH))) {
|
|
1318
|
+
return new Map();
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const catalog = await readJsonFile(CATALOG_PACKAGES_PATH);
|
|
1322
|
+
const packageRecords = ensureArray(catalog?.packages);
|
|
1323
|
+
const registry = new Map();
|
|
1324
|
+
|
|
1325
|
+
for (const packageRecord of packageRecords) {
|
|
1326
|
+
const record = ensureObject(packageRecord);
|
|
1327
|
+
const packageId = String(record.packageId || "").trim();
|
|
1328
|
+
const descriptorPath = `${normalizeRelativePath(CLI_PACKAGE_ROOT, CATALOG_PACKAGES_PATH)}#${packageId || "unknown"}`;
|
|
1329
|
+
const descriptor = validatePackageDescriptorShape(record.descriptor, descriptorPath);
|
|
1330
|
+
if (!packageId) {
|
|
1331
|
+
throw createCliError(`Invalid catalog package entry at ${descriptorPath}: missing packageId.`);
|
|
1332
|
+
}
|
|
1333
|
+
if (descriptor.packageId !== packageId) {
|
|
1334
|
+
throw createCliError(
|
|
1335
|
+
`Invalid catalog package entry at ${descriptorPath}: descriptor packageId ${descriptor.packageId} does not match catalog packageId ${packageId}.`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const version = String(record.version || descriptor.version || "").trim();
|
|
1340
|
+
if (!version) {
|
|
1341
|
+
throw createCliError(`Invalid catalog package entry at ${descriptorPath}: missing version.`);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
registry.set(
|
|
1345
|
+
packageId,
|
|
1346
|
+
createPackageEntry({
|
|
1347
|
+
packageId,
|
|
1348
|
+
version,
|
|
1349
|
+
descriptor: {
|
|
1350
|
+
...descriptor,
|
|
1351
|
+
version
|
|
1352
|
+
},
|
|
1353
|
+
rootDir: "",
|
|
1354
|
+
relativeDir: "",
|
|
1355
|
+
descriptorRelativePath: descriptorPath,
|
|
1356
|
+
packageJson: {
|
|
1357
|
+
name: packageId,
|
|
1358
|
+
version
|
|
1359
|
+
},
|
|
1360
|
+
sourceType: "catalog",
|
|
1361
|
+
source: {
|
|
1362
|
+
descriptorPath
|
|
1363
|
+
}
|
|
1364
|
+
})
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return registry;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
async function loadPackageRegistry() {
|
|
1372
|
+
const workspaceRegistry = await loadWorkspacePackageRegistry();
|
|
1373
|
+
const catalogRegistry = await loadCatalogPackageRegistry();
|
|
1374
|
+
const merged = mergePackageRegistries(catalogRegistry, workspaceRegistry);
|
|
1375
|
+
|
|
1376
|
+
if (merged.size === 0) {
|
|
1377
|
+
throw createCliError(
|
|
1378
|
+
"Unable to load package registry. Provide JSKIT_REPO_ROOT for workspace mode or ensure @jskit-ai/jskit-catalog is installed (or set JSKIT_CATALOG_PACKAGES_PATH)."
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return merged;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
|
|
1386
|
+
const normalizedPackageId = String(packageId || "").trim();
|
|
1387
|
+
if (!normalizedPackageId) {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const packageRoot = path.resolve(appRoot, "node_modules", ...normalizedPackageId.split("/"));
|
|
1392
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
1393
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
1398
|
+
const resolvedPackageId = String(packageJson?.name || "").trim() || normalizedPackageId;
|
|
1399
|
+
const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
|
|
1400
|
+
if (!(await fileExists(descriptorPath))) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
|
|
1405
|
+
const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
|
|
1406
|
+
expectedPackageId: resolvedPackageId,
|
|
1407
|
+
fallbackVersion: String(packageJson?.version || "").trim()
|
|
1408
|
+
});
|
|
1409
|
+
const relativeDir = normalizeRelativePath(appRoot, packageRoot);
|
|
1410
|
+
const descriptorRelativePath = normalizeRelativePath(appRoot, descriptorPath);
|
|
1411
|
+
|
|
1412
|
+
return createPackageEntry({
|
|
1413
|
+
packageId: descriptor.packageId,
|
|
1414
|
+
version: descriptor.version,
|
|
1415
|
+
descriptor,
|
|
1416
|
+
rootDir: packageRoot,
|
|
1417
|
+
relativeDir,
|
|
1418
|
+
descriptorRelativePath,
|
|
1419
|
+
packageJson,
|
|
1420
|
+
sourceType: "npm-installed-package",
|
|
1421
|
+
source: {
|
|
1422
|
+
packagePath: normalizeRelativePosixPath(relativeDir),
|
|
1423
|
+
descriptorPath: descriptorRelativePath
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function resolveInstalledNodeModulePackageEntry({ appRoot, packageId }) {
|
|
1429
|
+
const raw = String(packageId || "").trim();
|
|
1430
|
+
if (!raw) {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const candidates = [];
|
|
1435
|
+
const seen = new Set();
|
|
1436
|
+
const appendCandidate = (value) => {
|
|
1437
|
+
const candidate = String(value || "").trim();
|
|
1438
|
+
if (!candidate || seen.has(candidate)) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
seen.add(candidate);
|
|
1442
|
+
candidates.push(candidate);
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
appendCandidate(raw);
|
|
1446
|
+
appendCandidate(toScopedPackageId(raw));
|
|
1447
|
+
|
|
1448
|
+
for (const candidateId of candidates) {
|
|
1449
|
+
const entry = await loadInstalledNodeModulePackageEntry({
|
|
1450
|
+
appRoot,
|
|
1451
|
+
packageId: candidateId
|
|
1452
|
+
});
|
|
1453
|
+
if (entry) {
|
|
1454
|
+
return entry;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async function hydratePackageRegistryFromInstalledNodeModules({
|
|
1462
|
+
appRoot,
|
|
1463
|
+
packageRegistry,
|
|
1464
|
+
seedPackageIds = []
|
|
1465
|
+
}) {
|
|
1466
|
+
const queue = ensureArray(seedPackageIds)
|
|
1467
|
+
.map((value) => String(value || "").trim())
|
|
1468
|
+
.filter(Boolean);
|
|
1469
|
+
const visited = new Set();
|
|
1470
|
+
|
|
1471
|
+
while (queue.length > 0) {
|
|
1472
|
+
const packageId = queue.shift();
|
|
1473
|
+
if (!packageId || visited.has(packageId)) {
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
visited.add(packageId);
|
|
1477
|
+
|
|
1478
|
+
let packageEntry = packageRegistry.get(packageId);
|
|
1479
|
+
if (!packageEntry) {
|
|
1480
|
+
const resolvedEntry = await resolveInstalledNodeModulePackageEntry({
|
|
1481
|
+
appRoot,
|
|
1482
|
+
packageId
|
|
1483
|
+
});
|
|
1484
|
+
if (!resolvedEntry) {
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
packageRegistry.set(resolvedEntry.packageId, resolvedEntry);
|
|
1489
|
+
packageEntry = resolvedEntry;
|
|
1490
|
+
if (resolvedEntry.packageId !== packageId && !visited.has(resolvedEntry.packageId)) {
|
|
1491
|
+
queue.push(resolvedEntry.packageId);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const dependsOn = ensureArray(packageEntry?.descriptor?.dependsOn).map((value) => String(value || "").trim()).filter(Boolean);
|
|
1496
|
+
for (const dependencyId of dependsOn) {
|
|
1497
|
+
if (!visited.has(dependencyId)) {
|
|
1498
|
+
queue.push(dependencyId);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function loadBundleRegistry() {
|
|
1505
|
+
if (!(await fileExists(BUNDLES_ROOT))) {
|
|
1506
|
+
return new Map();
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const bundles = new Map();
|
|
1510
|
+
const entries = await readdir(BUNDLES_ROOT, { withFileTypes: true });
|
|
1511
|
+
for (const entry of entries) {
|
|
1512
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const descriptorPath = path.join(BUNDLES_ROOT, entry.name, "bundle.descriptor.mjs");
|
|
1517
|
+
if (!(await fileExists(descriptorPath))) {
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const descriptorModule = await import(pathToFileURL(descriptorPath).href);
|
|
1522
|
+
const descriptor = validateBundleDescriptorShape(descriptorModule?.default, descriptorPath);
|
|
1523
|
+
bundles.set(descriptor.bundleId, descriptor);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return bundles;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function resolvePackageDependencySpecifier(packageEntry, { existingValue = "" } = {}) {
|
|
1530
|
+
const source = ensureObject(packageEntry?.source);
|
|
1531
|
+
const sourceType = String(source.type || packageEntry?.sourceType || "").trim();
|
|
1532
|
+
if (sourceType === "app-local-package" || sourceType === "local-package") {
|
|
1533
|
+
const packagePath = normalizeRelativePosixPath(String(source.packagePath || packageEntry?.relativeDir || "").trim());
|
|
1534
|
+
if (!packagePath) {
|
|
1535
|
+
throw createCliError(`Unable to resolve local package path for ${String(packageEntry?.packageId || "unknown package")}.`);
|
|
1536
|
+
}
|
|
1537
|
+
return toFileDependencySpecifier(packagePath);
|
|
1538
|
+
}
|
|
1539
|
+
if (sourceType === "npm-installed-package") {
|
|
1540
|
+
const normalizedExisting = String(existingValue || "").trim();
|
|
1541
|
+
if (normalizedExisting) {
|
|
1542
|
+
return normalizedExisting;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const descriptorVersion = String(packageEntry?.version || "").trim();
|
|
1547
|
+
if (descriptorVersion) {
|
|
1548
|
+
return descriptorVersion;
|
|
1549
|
+
}
|
|
1550
|
+
const packageJsonVersion = String(packageEntry?.packageJson?.version || "").trim();
|
|
1551
|
+
if (packageJsonVersion) {
|
|
1552
|
+
return packageJsonVersion;
|
|
1553
|
+
}
|
|
1554
|
+
throw createCliError(`Unable to resolve dependency specifier for ${String(packageEntry?.packageId || "unknown package")}.`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function normalizePackageNameSegment(rawValue, { label = "package name" } = {}) {
|
|
1558
|
+
const lowered = String(rawValue || "")
|
|
1559
|
+
.trim()
|
|
1560
|
+
.toLowerCase()
|
|
1561
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1562
|
+
.replace(/-+/g, "-")
|
|
1563
|
+
.replace(/^[._-]+|[._-]+$/g, "");
|
|
1564
|
+
if (!lowered) {
|
|
1565
|
+
throw createCliError(`Invalid ${label}. Use letters, numbers, dash, underscore, or dot.`);
|
|
1566
|
+
}
|
|
1567
|
+
return lowered;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function normalizeScopeName(rawScope) {
|
|
1571
|
+
const normalized = String(rawScope || "").trim().replace(/^@+/, "");
|
|
1572
|
+
return normalizePackageNameSegment(normalized, { label: "scope" });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function resolveDefaultLocalScopeFromAppName(appPackageName) {
|
|
1576
|
+
const appName = String(appPackageName || "").trim();
|
|
1577
|
+
if (!appName) {
|
|
1578
|
+
return "app";
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const unscoped = appName.startsWith("@")
|
|
1582
|
+
? appName.slice(appName.indexOf("/") + 1)
|
|
1583
|
+
: appName;
|
|
1584
|
+
return normalizeScopeName(unscoped || "app");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function normalizeRelativePosixPath(pathValue) {
|
|
1588
|
+
return String(pathValue || "")
|
|
1589
|
+
.trim()
|
|
1590
|
+
.replace(/\\/g, "/")
|
|
1591
|
+
.replace(/^\/+|\/+$/g, "")
|
|
1592
|
+
.replace(/\/{2,}/g, "/");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function toFileDependencySpecifier(relativePath) {
|
|
1596
|
+
const normalized = normalizeRelativePosixPath(relativePath);
|
|
1597
|
+
if (!normalized) {
|
|
1598
|
+
throw createCliError("Cannot create file: dependency specifier from empty relative path.");
|
|
1599
|
+
}
|
|
1600
|
+
return `file:${normalized}`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function resolveLocalPackageId({ rawName, appPackageName, inlineOptions }) {
|
|
1604
|
+
const explicitPackageId = String(inlineOptions["package-id"] || "").trim();
|
|
1605
|
+
if (explicitPackageId) {
|
|
1606
|
+
const scopedPattern = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
|
|
1607
|
+
if (!scopedPattern.test(explicitPackageId)) {
|
|
1608
|
+
throw createCliError(
|
|
1609
|
+
`Invalid --package-id ${explicitPackageId}. Expected format: @scope/name (lowercase alphanumeric, ., _, -).`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
const packageName = explicitPackageId.slice(explicitPackageId.indexOf("/") + 1);
|
|
1613
|
+
return {
|
|
1614
|
+
packageId: explicitPackageId,
|
|
1615
|
+
packageDirName: normalizePackageNameSegment(packageName)
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const packageDirName = normalizePackageNameSegment(rawName);
|
|
1620
|
+
const scopeName = String(inlineOptions.scope || "").trim()
|
|
1621
|
+
? normalizeScopeName(inlineOptions.scope)
|
|
1622
|
+
: resolveDefaultLocalScopeFromAppName(appPackageName);
|
|
1623
|
+
return {
|
|
1624
|
+
packageId: `@${scopeName}/${packageDirName}`,
|
|
1625
|
+
packageDirName
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function createLocalPackageDescriptorTemplate({ packageId, description }) {
|
|
1630
|
+
return `export default Object.freeze({
|
|
1631
|
+
packageVersion: 1,
|
|
1632
|
+
packageId: "${packageId}",
|
|
1633
|
+
version: "0.1.0",
|
|
1634
|
+
description: ${JSON.stringify(String(description || ""))},
|
|
1635
|
+
dependsOn: [
|
|
1636
|
+
// "@jskit-ai/kernel"
|
|
1637
|
+
],
|
|
1638
|
+
capabilities: {
|
|
1639
|
+
provides: [
|
|
1640
|
+
// "example.feature"
|
|
1641
|
+
],
|
|
1642
|
+
requires: [
|
|
1643
|
+
// "example.dependency"
|
|
1644
|
+
]
|
|
1645
|
+
},
|
|
1646
|
+
options: {
|
|
1647
|
+
// "example-option": {
|
|
1648
|
+
// required: true,
|
|
1649
|
+
// promptLabel: "Enter option value",
|
|
1650
|
+
// promptHint: "Used by mutations.text interpolation",
|
|
1651
|
+
// defaultValue: "example"
|
|
1652
|
+
// }
|
|
1653
|
+
},
|
|
1654
|
+
runtime: {
|
|
1655
|
+
server: {
|
|
1656
|
+
providers: [
|
|
1657
|
+
// {
|
|
1658
|
+
// entrypoint: "src/server/providers/ExampleServerProvider.js",
|
|
1659
|
+
// export: "ExampleServerProvider"
|
|
1660
|
+
// }
|
|
1661
|
+
]
|
|
1662
|
+
},
|
|
1663
|
+
client: {
|
|
1664
|
+
providers: [
|
|
1665
|
+
// {
|
|
1666
|
+
// entrypoint: "src/client/providers/ExampleClientProvider.js",
|
|
1667
|
+
// export: "ExampleClientProvider"
|
|
1668
|
+
// }
|
|
1669
|
+
]
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
metadata: {
|
|
1673
|
+
server: {
|
|
1674
|
+
routes: [
|
|
1675
|
+
// {
|
|
1676
|
+
// method: "GET",
|
|
1677
|
+
// path: "/api/example",
|
|
1678
|
+
// summary: "Describe server route validator"
|
|
1679
|
+
// }
|
|
1680
|
+
]
|
|
1681
|
+
},
|
|
1682
|
+
ui: {
|
|
1683
|
+
routes: [
|
|
1684
|
+
// {
|
|
1685
|
+
// id: "example.route",
|
|
1686
|
+
// path: "/example",
|
|
1687
|
+
// scope: "global",
|
|
1688
|
+
// name: "example-route",
|
|
1689
|
+
// componentKey: "example-route",
|
|
1690
|
+
// autoRegister: true,
|
|
1691
|
+
// guard: {
|
|
1692
|
+
// policy: "public"
|
|
1693
|
+
// },
|
|
1694
|
+
// purpose: "Describe what this route is for."
|
|
1695
|
+
// }
|
|
1696
|
+
],
|
|
1697
|
+
elements: [
|
|
1698
|
+
// {
|
|
1699
|
+
// key: "example-route",
|
|
1700
|
+
// export: "ExampleView",
|
|
1701
|
+
// entrypoint: "src/client/views/ExampleView.vue",
|
|
1702
|
+
// purpose: "UI element exposed by this package."
|
|
1703
|
+
// }
|
|
1704
|
+
],
|
|
1705
|
+
overrides: [
|
|
1706
|
+
// {
|
|
1707
|
+
// targetId: "some.existing.route",
|
|
1708
|
+
// mode: "replace",
|
|
1709
|
+
// reason: "Explain override intent."
|
|
1710
|
+
// }
|
|
1711
|
+
]
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
mutations: {
|
|
1715
|
+
dependencies: {
|
|
1716
|
+
runtime: {
|
|
1717
|
+
// "@example/runtime-dependency": "^1.0.0"
|
|
1718
|
+
},
|
|
1719
|
+
dev: {
|
|
1720
|
+
// "@example/dev-dependency": "^1.0.0"
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
packageJson: {
|
|
1724
|
+
scripts: {
|
|
1725
|
+
// "lint:example": "eslint src/example"
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
procfile: {
|
|
1729
|
+
// worker: "node ./bin/worker.js"
|
|
1730
|
+
},
|
|
1731
|
+
vite: {
|
|
1732
|
+
proxy: [
|
|
1733
|
+
// {
|
|
1734
|
+
// id: "example-socket-proxy",
|
|
1735
|
+
// path: "/socket.io",
|
|
1736
|
+
// changeOrigin: true,
|
|
1737
|
+
// ws: true,
|
|
1738
|
+
// target: "http://localhost:3000",
|
|
1739
|
+
// reason: "Explain why this proxy is needed."
|
|
1740
|
+
// }
|
|
1741
|
+
]
|
|
1742
|
+
},
|
|
1743
|
+
text: [
|
|
1744
|
+
// {
|
|
1745
|
+
// op: "upsert-env",
|
|
1746
|
+
// file: ".env",
|
|
1747
|
+
// key: "EXAMPLE_ENV",
|
|
1748
|
+
// value: "\${option:example-option}",
|
|
1749
|
+
// reason: "Explain why this env var is needed.",
|
|
1750
|
+
// category: "runtime-config",
|
|
1751
|
+
// id: "example-env"
|
|
1752
|
+
// }
|
|
1753
|
+
],
|
|
1754
|
+
files: [
|
|
1755
|
+
// {
|
|
1756
|
+
// from: "templates/src/pages/example/index.vue",
|
|
1757
|
+
// to: "src/pages/example/index.vue",
|
|
1758
|
+
// reason: "Explain what is scaffolded.",
|
|
1759
|
+
// category: "example",
|
|
1760
|
+
// id: "example-file"
|
|
1761
|
+
// }
|
|
1762
|
+
]
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
`;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function createLocalPackageScaffoldFiles({ packageId, packageDescription }) {
|
|
1769
|
+
return [
|
|
1770
|
+
{
|
|
1771
|
+
relativePath: "package.json",
|
|
1772
|
+
content: `${JSON.stringify(
|
|
1773
|
+
{
|
|
1774
|
+
name: packageId,
|
|
1775
|
+
version: "0.1.0",
|
|
1776
|
+
private: true,
|
|
1777
|
+
type: "module",
|
|
1778
|
+
exports: {
|
|
1779
|
+
".": "./src/index.js",
|
|
1780
|
+
"./client": "./src/client/index.js",
|
|
1781
|
+
"./server": "./src/server/index.js",
|
|
1782
|
+
"./shared": "./src/shared/index.js"
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
null,
|
|
1786
|
+
2
|
|
1787
|
+
)}\n`
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
relativePath: "package.descriptor.mjs",
|
|
1791
|
+
content: createLocalPackageDescriptorTemplate({
|
|
1792
|
+
packageId,
|
|
1793
|
+
description: packageDescription
|
|
1794
|
+
})
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
relativePath: "src/index.js",
|
|
1798
|
+
content: "export {};\n"
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
relativePath: "src/server/index.js",
|
|
1802
|
+
content: "export {};\n"
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
relativePath: "src/client/index.js",
|
|
1806
|
+
content: [
|
|
1807
|
+
"const routeComponents = Object.freeze({});",
|
|
1808
|
+
"",
|
|
1809
|
+
"async function bootClient({ logger } = {}) {",
|
|
1810
|
+
" if (logger && typeof logger.debug === \"function\") {",
|
|
1811
|
+
` logger.debug({ packageId: ${JSON.stringify(packageId)} }, "bootClient executed.");`,
|
|
1812
|
+
" }",
|
|
1813
|
+
"}",
|
|
1814
|
+
"",
|
|
1815
|
+
"export { routeComponents, bootClient };",
|
|
1816
|
+
""
|
|
1817
|
+
].join("\n")
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
relativePath: "src/shared/index.js",
|
|
1821
|
+
content: "export {};\n"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
relativePath: "README.md",
|
|
1825
|
+
content: [
|
|
1826
|
+
`# ${packageId}`,
|
|
1827
|
+
"",
|
|
1828
|
+
"App-local JSKIT module scaffold.",
|
|
1829
|
+
"",
|
|
1830
|
+
"## Next Steps",
|
|
1831
|
+
"",
|
|
1832
|
+
"- Define runtime providers in `package.descriptor.mjs`.",
|
|
1833
|
+
"- Add client/server exports under `src/`.",
|
|
1834
|
+
"- Keep package version in sync with descriptor version.",
|
|
1835
|
+
""
|
|
1836
|
+
].join("\n")
|
|
1837
|
+
}
|
|
1838
|
+
];
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function resolveLocalDependencyOrder(initialPackageIds, packageRegistry) {
|
|
1842
|
+
const ordered = [];
|
|
1843
|
+
const visited = new Set();
|
|
1844
|
+
const visiting = new Set();
|
|
1845
|
+
const externalDependencies = new Set();
|
|
1846
|
+
|
|
1847
|
+
function visit(packageId, lineage = []) {
|
|
1848
|
+
if (visited.has(packageId)) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (visiting.has(packageId)) {
|
|
1852
|
+
const cyclePath = [...lineage, packageId].join(" -> ");
|
|
1853
|
+
throw createCliError(`Dependency cycle detected: ${cyclePath}`);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const packageEntry = packageRegistry.get(packageId);
|
|
1857
|
+
if (!packageEntry) {
|
|
1858
|
+
throw createCliError(`Unknown package: ${packageId}`);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
visiting.add(packageId);
|
|
1862
|
+
for (const dependencyId of ensureArray(packageEntry.descriptor.dependsOn).map((value) => String(value))) {
|
|
1863
|
+
if (packageRegistry.has(dependencyId)) {
|
|
1864
|
+
visit(dependencyId, [...lineage, packageId]);
|
|
1865
|
+
} else {
|
|
1866
|
+
externalDependencies.add(dependencyId);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
visiting.delete(packageId);
|
|
1870
|
+
visited.add(packageId);
|
|
1871
|
+
ordered.push(packageId);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
for (const packageId of initialPackageIds) {
|
|
1875
|
+
visit(packageId);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
return {
|
|
1879
|
+
ordered,
|
|
1880
|
+
externalDependencies: sortStrings([...externalDependencies])
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function listDeclaredCapabilities(capabilitiesSection, fieldName) {
|
|
1885
|
+
const section = ensureObject(capabilitiesSection);
|
|
1886
|
+
const source = ensureArray(section[fieldName]);
|
|
1887
|
+
const normalized = [];
|
|
1888
|
+
const seen = new Set();
|
|
1889
|
+
for (const value of source) {
|
|
1890
|
+
const capabilityId = String(value || "").trim();
|
|
1891
|
+
if (!capabilityId || seen.has(capabilityId)) {
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
seen.add(capabilityId);
|
|
1895
|
+
normalized.push(capabilityId);
|
|
1896
|
+
}
|
|
1897
|
+
return normalized;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function buildCapabilityGraph(packageRegistry) {
|
|
1901
|
+
const graph = new Map();
|
|
1902
|
+
const ensureNode = (capabilityId) => {
|
|
1903
|
+
if (!graph.has(capabilityId)) {
|
|
1904
|
+
graph.set(capabilityId, {
|
|
1905
|
+
providers: new Set(),
|
|
1906
|
+
requirers: new Set()
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
return graph.get(capabilityId);
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1912
|
+
for (const [packageId, packageEntry] of packageRegistry.entries()) {
|
|
1913
|
+
const capabilities = ensureObject(packageEntry?.descriptor?.capabilities);
|
|
1914
|
+
for (const capabilityId of listDeclaredCapabilities(capabilities, "provides")) {
|
|
1915
|
+
ensureNode(capabilityId).providers.add(packageId);
|
|
1916
|
+
}
|
|
1917
|
+
for (const capabilityId of listDeclaredCapabilities(capabilities, "requires")) {
|
|
1918
|
+
ensureNode(capabilityId).requirers.add(packageId);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
for (const [capabilityId, providers] of Object.entries(BUILTIN_CAPABILITY_PROVIDERS)) {
|
|
1923
|
+
const node = ensureNode(capabilityId);
|
|
1924
|
+
for (const providerId of ensureArray(providers).map((value) => String(value || "").trim()).filter(Boolean)) {
|
|
1925
|
+
node.providers.add(providerId);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
const normalizedGraph = new Map();
|
|
1930
|
+
for (const [capabilityId, node] of graph.entries()) {
|
|
1931
|
+
normalizedGraph.set(capabilityId, {
|
|
1932
|
+
providers: sortStrings([...node.providers]),
|
|
1933
|
+
requirers: sortStrings([...node.requirers])
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
return normalizedGraph;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function createCapabilityPackageDetail(packageId, packageRegistry) {
|
|
1940
|
+
const packageEntry = packageRegistry.get(packageId);
|
|
1941
|
+
return {
|
|
1942
|
+
packageId,
|
|
1943
|
+
version: String(packageEntry?.version || packageEntry?.descriptor?.version || "").trim(),
|
|
1944
|
+
descriptorPath: String(packageEntry?.descriptorRelativePath || "").trim()
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function buildCapabilityDetailsForPackage({ packageRegistry, packageId, dependsOn = [], provides = [], requires = [] }) {
|
|
1949
|
+
const graph = buildCapabilityGraph(packageRegistry);
|
|
1950
|
+
const dependsOnSet = new Set(ensureArray(dependsOn).map((value) => String(value || "").trim()).filter(Boolean));
|
|
1951
|
+
|
|
1952
|
+
function buildCapabilityRecord(capabilityId) {
|
|
1953
|
+
const node = graph.get(capabilityId) || {
|
|
1954
|
+
providers: [],
|
|
1955
|
+
requirers: []
|
|
1956
|
+
};
|
|
1957
|
+
const providers = sortStrings(ensureArray(node.providers));
|
|
1958
|
+
const requirers = sortStrings(ensureArray(node.requirers));
|
|
1959
|
+
const providersInDependsOn = providers.filter((providerId) => dependsOnSet.has(providerId));
|
|
1960
|
+
return {
|
|
1961
|
+
capabilityId,
|
|
1962
|
+
providers,
|
|
1963
|
+
requirers,
|
|
1964
|
+
providersInDependsOn,
|
|
1965
|
+
providerDetails: providers.map((providerId) => createCapabilityPackageDetail(providerId, packageRegistry)),
|
|
1966
|
+
requirerDetails: requirers.map((requirerId) => createCapabilityPackageDetail(requirerId, packageRegistry)),
|
|
1967
|
+
isProvidedByCurrentPackage: providers.includes(packageId),
|
|
1968
|
+
isRequiredByCurrentPackage: requirers.includes(packageId)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
return {
|
|
1973
|
+
provides: ensureArray(provides).map((capabilityId) => buildCapabilityRecord(capabilityId)),
|
|
1974
|
+
requires: ensureArray(requires).map((capabilityId) => buildCapabilityRecord(capabilityId))
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function escapeRegexLiteral(value) {
|
|
1979
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function parseQuotedStringLiteral(value) {
|
|
1983
|
+
const source = String(value || "").trim();
|
|
1984
|
+
if (source.length < 2) {
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const quote = source[0];
|
|
1989
|
+
if ((quote !== "\"" && quote !== "'") || source[source.length - 1] !== quote) {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (quote === "\"") {
|
|
1994
|
+
try {
|
|
1995
|
+
return JSON.parse(source);
|
|
1996
|
+
} catch {
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return source
|
|
2002
|
+
.slice(1, -1)
|
|
2003
|
+
.replace(/\\\\/g, "\\")
|
|
2004
|
+
.replace(/\\'/g, "'")
|
|
2005
|
+
.replace(/\\"/g, "\"")
|
|
2006
|
+
.replace(/\\n/g, "\n")
|
|
2007
|
+
.replace(/\\r/g, "\r")
|
|
2008
|
+
.replace(/\\t/g, "\t");
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function resolveLineNumberAtIndex(source, index) {
|
|
2012
|
+
const text = String(source || "");
|
|
2013
|
+
const maxIndex = Math.max(0, Math.min(Number(index) || 0, text.length));
|
|
2014
|
+
let line = 1;
|
|
2015
|
+
for (let cursor = 0; cursor < maxIndex; cursor += 1) {
|
|
2016
|
+
if (text[cursor] === "\n") {
|
|
2017
|
+
line += 1;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return line;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function findMatchingBraceIndex(source, openBraceIndex) {
|
|
2024
|
+
const text = String(source || "");
|
|
2025
|
+
const startIndex = Number(openBraceIndex);
|
|
2026
|
+
if (!Number.isInteger(startIndex) || startIndex < 0 || startIndex >= text.length || text[startIndex] !== "{") {
|
|
2027
|
+
return -1;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
let depth = 0;
|
|
2031
|
+
let inSingleQuote = false;
|
|
2032
|
+
let inDoubleQuote = false;
|
|
2033
|
+
let inTemplateQuote = false;
|
|
2034
|
+
let inLineComment = false;
|
|
2035
|
+
let inBlockComment = false;
|
|
2036
|
+
|
|
2037
|
+
for (let cursor = startIndex; cursor < text.length; cursor += 1) {
|
|
2038
|
+
const current = text[cursor];
|
|
2039
|
+
const next = text[cursor + 1] || "";
|
|
2040
|
+
|
|
2041
|
+
if (inLineComment) {
|
|
2042
|
+
if (current === "\n") {
|
|
2043
|
+
inLineComment = false;
|
|
2044
|
+
}
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (inBlockComment) {
|
|
2049
|
+
if (current === "*" && next === "/") {
|
|
2050
|
+
inBlockComment = false;
|
|
2051
|
+
cursor += 1;
|
|
2052
|
+
}
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
if (inSingleQuote) {
|
|
2057
|
+
if (current === "\\") {
|
|
2058
|
+
cursor += 1;
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (current === "'") {
|
|
2062
|
+
inSingleQuote = false;
|
|
2063
|
+
}
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (inDoubleQuote) {
|
|
2068
|
+
if (current === "\\") {
|
|
2069
|
+
cursor += 1;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (current === "\"") {
|
|
2073
|
+
inDoubleQuote = false;
|
|
2074
|
+
}
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
if (inTemplateQuote) {
|
|
2079
|
+
if (current === "\\") {
|
|
2080
|
+
cursor += 1;
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
if (current === "`") {
|
|
2084
|
+
inTemplateQuote = false;
|
|
2085
|
+
}
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if (current === "/" && next === "/") {
|
|
2090
|
+
inLineComment = true;
|
|
2091
|
+
cursor += 1;
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (current === "/" && next === "*") {
|
|
2095
|
+
inBlockComment = true;
|
|
2096
|
+
cursor += 1;
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (current === "'") {
|
|
2100
|
+
inSingleQuote = true;
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (current === "\"") {
|
|
2104
|
+
inDoubleQuote = true;
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (current === "`") {
|
|
2108
|
+
inTemplateQuote = true;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
if (current === "{") {
|
|
2113
|
+
depth += 1;
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
if (current === "}") {
|
|
2117
|
+
depth -= 1;
|
|
2118
|
+
if (depth === 0) {
|
|
2119
|
+
return cursor;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
return -1;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function extractProviderLifecycleMethodRanges(source, providerExportName) {
|
|
2128
|
+
const text = String(source || "");
|
|
2129
|
+
const providerName = String(providerExportName || "").trim();
|
|
2130
|
+
if (!text) {
|
|
2131
|
+
return [];
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const fallback = [
|
|
2135
|
+
{
|
|
2136
|
+
lifecycle: "unknown",
|
|
2137
|
+
start: 0,
|
|
2138
|
+
end: text.length
|
|
2139
|
+
}
|
|
2140
|
+
];
|
|
2141
|
+
if (!providerName) {
|
|
2142
|
+
return fallback;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const classPattern = new RegExp(`\\bclass\\s+${escapeRegexLiteral(providerName)}\\b`);
|
|
2146
|
+
const classMatch = classPattern.exec(text);
|
|
2147
|
+
if (!classMatch) {
|
|
2148
|
+
return fallback;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const classOpenBraceIndex = text.indexOf("{", classMatch.index + classMatch[0].length);
|
|
2152
|
+
if (classOpenBraceIndex < 0) {
|
|
2153
|
+
return fallback;
|
|
2154
|
+
}
|
|
2155
|
+
const classCloseBraceIndex = findMatchingBraceIndex(text, classOpenBraceIndex);
|
|
2156
|
+
if (classCloseBraceIndex < 0) {
|
|
2157
|
+
return fallback;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
const classBody = text.slice(classOpenBraceIndex + 1, classCloseBraceIndex);
|
|
2161
|
+
const methodPattern = /\b(?:async\s+)?(register|boot)\s*\([^)]*\)\s*\{/g;
|
|
2162
|
+
const ranges = [];
|
|
2163
|
+
let methodMatch = methodPattern.exec(classBody);
|
|
2164
|
+
while (methodMatch) {
|
|
2165
|
+
const lifecycle = String(methodMatch[1] || "").trim() || "unknown";
|
|
2166
|
+
const methodOpenOffset = methodMatch[0].lastIndexOf("{");
|
|
2167
|
+
if (methodOpenOffset < 0) {
|
|
2168
|
+
methodMatch = methodPattern.exec(classBody);
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const methodOpenIndex = classOpenBraceIndex + 1 + methodMatch.index + methodOpenOffset;
|
|
2172
|
+
const methodCloseIndex = findMatchingBraceIndex(text, methodOpenIndex);
|
|
2173
|
+
if (methodCloseIndex < 0) {
|
|
2174
|
+
methodMatch = methodPattern.exec(classBody);
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
ranges.push({
|
|
2178
|
+
lifecycle,
|
|
2179
|
+
start: methodOpenIndex + 1,
|
|
2180
|
+
end: methodCloseIndex
|
|
2181
|
+
});
|
|
2182
|
+
methodMatch = methodPattern.exec(classBody);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
if (ranges.length > 0) {
|
|
2186
|
+
return ranges;
|
|
2187
|
+
}
|
|
2188
|
+
return [
|
|
2189
|
+
{
|
|
2190
|
+
lifecycle: "class",
|
|
2191
|
+
start: classOpenBraceIndex + 1,
|
|
2192
|
+
end: classCloseBraceIndex
|
|
2193
|
+
}
|
|
2194
|
+
];
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function collectConstTokenAssignments(source) {
|
|
2198
|
+
const text = String(source || "");
|
|
2199
|
+
const assignments = new Map();
|
|
2200
|
+
const pattern = /^\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*([^;]+);\s*$/gm;
|
|
2201
|
+
let match = pattern.exec(text);
|
|
2202
|
+
while (match) {
|
|
2203
|
+
const identifier = String(match[1] || "").trim();
|
|
2204
|
+
const expression = String(match[2] || "").trim();
|
|
2205
|
+
if (identifier && expression) {
|
|
2206
|
+
assignments.set(identifier, expression);
|
|
2207
|
+
}
|
|
2208
|
+
match = pattern.exec(text);
|
|
2209
|
+
}
|
|
2210
|
+
return assignments;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
function resolveTokenFromExpression(expression, constAssignments, visited = new Set()) {
|
|
2214
|
+
let normalized = String(expression || "").trim();
|
|
2215
|
+
if (!normalized) {
|
|
2216
|
+
return {
|
|
2217
|
+
token: "",
|
|
2218
|
+
resolved: false,
|
|
2219
|
+
kind: "empty"
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
while (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
2224
|
+
normalized = normalized.slice(1, -1).trim();
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const quoted = parseQuotedStringLiteral(normalized);
|
|
2228
|
+
if (quoted !== null) {
|
|
2229
|
+
return {
|
|
2230
|
+
token: quoted,
|
|
2231
|
+
resolved: true,
|
|
2232
|
+
kind: "string"
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const symbolMatch = /^Symbol\.for\(\s*(['"])(.*?)\1\s*\)$/.exec(normalized);
|
|
2237
|
+
if (symbolMatch) {
|
|
2238
|
+
return {
|
|
2239
|
+
token: `Symbol.for(${symbolMatch[2]})`,
|
|
2240
|
+
resolved: true,
|
|
2241
|
+
kind: "symbol"
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/.test(normalized)) {
|
|
2246
|
+
return {
|
|
2247
|
+
token: normalized,
|
|
2248
|
+
resolved: true,
|
|
2249
|
+
kind: "member"
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalized)) {
|
|
2254
|
+
const identifier = normalized;
|
|
2255
|
+
if (visited.has(identifier)) {
|
|
2256
|
+
return {
|
|
2257
|
+
token: identifier,
|
|
2258
|
+
resolved: false,
|
|
2259
|
+
kind: "cyclic-identifier"
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
const nextExpression = constAssignments.get(identifier);
|
|
2263
|
+
if (nextExpression) {
|
|
2264
|
+
return resolveTokenFromExpression(nextExpression, constAssignments, new Set([...visited, identifier]));
|
|
2265
|
+
}
|
|
2266
|
+
return {
|
|
2267
|
+
token: identifier,
|
|
2268
|
+
resolved: false,
|
|
2269
|
+
kind: "identifier"
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
return {
|
|
2274
|
+
token: normalized,
|
|
2275
|
+
resolved: false,
|
|
2276
|
+
kind: "expression"
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function collectContainerBindingsFromProviderSource({ source, providerLabel, entrypoint, providerExportName }) {
|
|
2281
|
+
const text = String(source || "");
|
|
2282
|
+
if (!text) {
|
|
2283
|
+
return [];
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
const constAssignments = collectConstTokenAssignments(text);
|
|
2287
|
+
const methodRanges = extractProviderLifecycleMethodRanges(text, providerExportName);
|
|
2288
|
+
const records = [];
|
|
2289
|
+
|
|
2290
|
+
for (const range of methodRanges) {
|
|
2291
|
+
const lifecycle = String(range?.lifecycle || "unknown").trim() || "unknown";
|
|
2292
|
+
const start = Number(range?.start) || 0;
|
|
2293
|
+
const end = Number(range?.end) || text.length;
|
|
2294
|
+
const slice = text.slice(start, end);
|
|
2295
|
+
const bindingPattern = /\bapp\.(singleton|bind|scoped|instance)\s*\(\s*([\s\S]*?)\s*,/g;
|
|
2296
|
+
let match = bindingPattern.exec(slice);
|
|
2297
|
+
while (match) {
|
|
2298
|
+
const binding = String(match[1] || "").trim();
|
|
2299
|
+
const tokenExpression = String(match[2] || "")
|
|
2300
|
+
.replace(/\s+/g, " ")
|
|
2301
|
+
.trim();
|
|
2302
|
+
if (!tokenExpression) {
|
|
2303
|
+
match = bindingPattern.exec(slice);
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
const tokenResolution = resolveTokenFromExpression(tokenExpression, constAssignments);
|
|
2307
|
+
const line = resolveLineNumberAtIndex(text, start + match.index);
|
|
2308
|
+
records.push({
|
|
2309
|
+
provider: providerLabel,
|
|
2310
|
+
entrypoint: String(entrypoint || "").trim(),
|
|
2311
|
+
exportName: String(providerExportName || "").trim(),
|
|
2312
|
+
lifecycle,
|
|
2313
|
+
binding,
|
|
2314
|
+
token: String(tokenResolution.token || "").trim(),
|
|
2315
|
+
tokenExpression,
|
|
2316
|
+
tokenResolved: Boolean(tokenResolution.resolved),
|
|
2317
|
+
tokenKind: String(tokenResolution.kind || "").trim(),
|
|
2318
|
+
location: `${String(entrypoint || "").trim()}:${line}`,
|
|
2319
|
+
line
|
|
2320
|
+
});
|
|
2321
|
+
match = bindingPattern.exec(slice);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
return records;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function collectPackageExportEntries(exportsField) {
|
|
2329
|
+
const entries = [];
|
|
2330
|
+
const normalizeExportSubpath = (subpath) => {
|
|
2331
|
+
const normalized = String(subpath || ".").trim() || ".";
|
|
2332
|
+
if (normalized === "." || normalized === "./") {
|
|
2333
|
+
return {
|
|
2334
|
+
normalized: ".",
|
|
2335
|
+
segments: []
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const withoutPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
2340
|
+
const segments = withoutPrefix.split("/").map((value) => String(value || "").trim()).filter(Boolean);
|
|
2341
|
+
return {
|
|
2342
|
+
normalized: normalized.startsWith("./") ? normalized : `./${withoutPrefix}`,
|
|
2343
|
+
segments
|
|
2344
|
+
};
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
const resolveSubpathSortPriority = (subpath) => {
|
|
2348
|
+
const normalized = normalizeExportSubpath(subpath);
|
|
2349
|
+
const firstSegment = String(normalized.segments[0] || "").trim();
|
|
2350
|
+
if (firstSegment === "client") {
|
|
2351
|
+
return 0;
|
|
2352
|
+
}
|
|
2353
|
+
if (firstSegment === "server") {
|
|
2354
|
+
return 1;
|
|
2355
|
+
}
|
|
2356
|
+
if (firstSegment === "shared") {
|
|
2357
|
+
return 2;
|
|
2358
|
+
}
|
|
2359
|
+
if (normalized.normalized === ".") {
|
|
2360
|
+
return 3;
|
|
2361
|
+
}
|
|
2362
|
+
return 10;
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
const appendEntry = (subpath, conditions, target) => {
|
|
2366
|
+
const normalizedSubpath = String(subpath || ".").trim() || ".";
|
|
2367
|
+
const normalizedTarget = String(target || "").trim();
|
|
2368
|
+
if (!normalizedTarget) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
const normalizedConditions = ensureArray(conditions).map((value) => String(value || "").trim()).filter(Boolean);
|
|
2372
|
+
entries.push({
|
|
2373
|
+
subpath: normalizedSubpath,
|
|
2374
|
+
condition: normalizedConditions.length > 0 ? normalizedConditions.join(".") : "default",
|
|
2375
|
+
target: normalizedTarget
|
|
2376
|
+
});
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
const visit = (subpath, value, conditionStack = []) => {
|
|
2380
|
+
if (typeof value === "string") {
|
|
2381
|
+
appendEntry(subpath, conditionStack, value);
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
if (Array.isArray(value)) {
|
|
2385
|
+
for (const item of value) {
|
|
2386
|
+
visit(subpath, item, conditionStack);
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (!value || typeof value !== "object") {
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
for (const [conditionName, nested] of Object.entries(value)) {
|
|
2394
|
+
visit(subpath, nested, [...conditionStack, conditionName]);
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
if (typeof exportsField === "string" || Array.isArray(exportsField)) {
|
|
2399
|
+
visit(".", exportsField, []);
|
|
2400
|
+
} else if (exportsField && typeof exportsField === "object") {
|
|
2401
|
+
const root = ensureObject(exportsField);
|
|
2402
|
+
const rootKeys = Object.keys(root);
|
|
2403
|
+
const hasSubpathKeys = rootKeys.some((key) => key.startsWith("."));
|
|
2404
|
+
if (hasSubpathKeys) {
|
|
2405
|
+
for (const [subpath, value] of Object.entries(root)) {
|
|
2406
|
+
visit(subpath, value, []);
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
visit(".", root, []);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
const deduplicated = [];
|
|
2414
|
+
const seen = new Set();
|
|
2415
|
+
for (const entry of entries) {
|
|
2416
|
+
const key = `${entry.subpath}::${entry.condition}::${entry.target}`;
|
|
2417
|
+
if (seen.has(key)) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
seen.add(key);
|
|
2421
|
+
deduplicated.push(entry);
|
|
2422
|
+
}
|
|
2423
|
+
return deduplicated.sort((left, right) => {
|
|
2424
|
+
const leftPriority = resolveSubpathSortPriority(left.subpath);
|
|
2425
|
+
const rightPriority = resolveSubpathSortPriority(right.subpath);
|
|
2426
|
+
if (leftPriority !== rightPriority) {
|
|
2427
|
+
return leftPriority - rightPriority;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const leftParts = normalizeExportSubpath(left.subpath);
|
|
2431
|
+
const rightParts = normalizeExportSubpath(right.subpath);
|
|
2432
|
+
const leftRoot = String(leftParts.segments[0] || "");
|
|
2433
|
+
const rightRoot = String(rightParts.segments[0] || "");
|
|
2434
|
+
const rootComparison = leftRoot.localeCompare(rightRoot);
|
|
2435
|
+
if (rootComparison !== 0) {
|
|
2436
|
+
return rootComparison;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const depthComparison = leftParts.segments.length - rightParts.segments.length;
|
|
2440
|
+
if (depthComparison !== 0) {
|
|
2441
|
+
return depthComparison;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const subpathComparison = left.subpath.localeCompare(right.subpath);
|
|
2445
|
+
if (subpathComparison !== 0) {
|
|
2446
|
+
return subpathComparison;
|
|
2447
|
+
}
|
|
2448
|
+
const conditionComparison = left.condition.localeCompare(right.condition);
|
|
2449
|
+
if (conditionComparison !== 0) {
|
|
2450
|
+
return conditionComparison;
|
|
2451
|
+
}
|
|
2452
|
+
return left.target.localeCompare(right.target);
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
async function describePackageExports({ packageRoot, packageJson }) {
|
|
2457
|
+
const rootDir = String(packageRoot || "").trim();
|
|
2458
|
+
if (!rootDir) {
|
|
2459
|
+
return [];
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
const exportsField = ensureObject(packageJson).exports;
|
|
2463
|
+
const entries = collectPackageExportEntries(exportsField);
|
|
2464
|
+
const records = [];
|
|
2465
|
+
|
|
2466
|
+
for (const entry of entries) {
|
|
2467
|
+
const subpath = String(entry.subpath || ".").trim() || ".";
|
|
2468
|
+
const condition = String(entry.condition || "default").trim() || "default";
|
|
2469
|
+
const target = String(entry.target || "").trim();
|
|
2470
|
+
const isPattern = subpath.includes("*") || target.includes("*");
|
|
2471
|
+
const isRelativeTarget = target.startsWith("./");
|
|
2472
|
+
let targetExists = null;
|
|
2473
|
+
if (isRelativeTarget && !isPattern) {
|
|
2474
|
+
const absoluteTargetPath = path.resolve(rootDir, target);
|
|
2475
|
+
targetExists = await fileExists(absoluteTargetPath);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
let targetType = "external";
|
|
2479
|
+
if (isPattern) {
|
|
2480
|
+
targetType = "pattern";
|
|
2481
|
+
} else if (isRelativeTarget) {
|
|
2482
|
+
targetType = "file";
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
records.push({
|
|
2486
|
+
subpath,
|
|
2487
|
+
condition,
|
|
2488
|
+
target,
|
|
2489
|
+
targetType,
|
|
2490
|
+
targetExists
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
return records;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
function parseNamedExportSpecifiers(specifierSource) {
|
|
2498
|
+
const source = String(specifierSource || "");
|
|
2499
|
+
return source
|
|
2500
|
+
.split(",")
|
|
2501
|
+
.map((entry) => entry.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/g, "").trim())
|
|
2502
|
+
.filter(Boolean)
|
|
2503
|
+
.map((entry) => entry.replace(/\s+/g, " "))
|
|
2504
|
+
.map((entry) => {
|
|
2505
|
+
const aliasMatch = /^(.+?)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/.exec(entry);
|
|
2506
|
+
if (aliasMatch) {
|
|
2507
|
+
return aliasMatch[2];
|
|
2508
|
+
}
|
|
2509
|
+
return entry;
|
|
2510
|
+
})
|
|
2511
|
+
.filter((entry) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(entry));
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
function parseExportedSymbolsFromSource(source) {
|
|
2515
|
+
const text = String(source || "");
|
|
2516
|
+
const symbols = new Set();
|
|
2517
|
+
const starReExports = new Set();
|
|
2518
|
+
const namedReExports = new Set();
|
|
2519
|
+
|
|
2520
|
+
const namespaceStarPattern = /export\s+\*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+["']([^"']+)["']\s*;?/g;
|
|
2521
|
+
let match = namespaceStarPattern.exec(text);
|
|
2522
|
+
while (match) {
|
|
2523
|
+
symbols.add(String(match[1] || "").trim());
|
|
2524
|
+
starReExports.add(String(match[2] || "").trim());
|
|
2525
|
+
match = namespaceStarPattern.exec(text);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
const starPattern = /export\s+\*\s+from\s+["']([^"']+)["']\s*;?/g;
|
|
2529
|
+
match = starPattern.exec(text);
|
|
2530
|
+
while (match) {
|
|
2531
|
+
starReExports.add(String(match[1] || "").trim());
|
|
2532
|
+
match = starPattern.exec(text);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
const namedPattern = /export\s*\{([\s\S]*?)\}\s*(?:from\s*["']([^"']+)["'])?\s*;?/g;
|
|
2536
|
+
match = namedPattern.exec(text);
|
|
2537
|
+
while (match) {
|
|
2538
|
+
const listSource = String(match[1] || "");
|
|
2539
|
+
for (const symbol of parseNamedExportSpecifiers(listSource)) {
|
|
2540
|
+
symbols.add(symbol);
|
|
2541
|
+
}
|
|
2542
|
+
if (match[2]) {
|
|
2543
|
+
namedReExports.add(String(match[2] || "").trim());
|
|
2544
|
+
}
|
|
2545
|
+
match = namedPattern.exec(text);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const functionPattern = /export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
|
|
2549
|
+
match = functionPattern.exec(text);
|
|
2550
|
+
while (match) {
|
|
2551
|
+
symbols.add(String(match[1] || "").trim());
|
|
2552
|
+
match = functionPattern.exec(text);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const classPattern = /export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
|
|
2556
|
+
match = classPattern.exec(text);
|
|
2557
|
+
while (match) {
|
|
2558
|
+
symbols.add(String(match[1] || "").trim());
|
|
2559
|
+
match = classPattern.exec(text);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
const variablePattern = /export\s+(?:const|let|var)\s+([\s\S]*?);/g;
|
|
2563
|
+
match = variablePattern.exec(text);
|
|
2564
|
+
while (match) {
|
|
2565
|
+
const declaration = String(match[1] || "");
|
|
2566
|
+
const names = declaration.split(",").map((entry) => String(entry || "").trim());
|
|
2567
|
+
for (const name of names) {
|
|
2568
|
+
const declarationMatch = /^([A-Za-z_$][A-Za-z0-9_$]*)\b/.exec(name);
|
|
2569
|
+
if (declarationMatch) {
|
|
2570
|
+
symbols.add(String(declarationMatch[1] || "").trim());
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
match = variablePattern.exec(text);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
const hasDefaultExport = /\bexport\s+default\b/.test(text);
|
|
2577
|
+
return {
|
|
2578
|
+
symbols: sortStrings([...symbols]),
|
|
2579
|
+
starReExports: sortStrings([...starReExports]),
|
|
2580
|
+
namedReExports: sortStrings([...namedReExports]),
|
|
2581
|
+
hasDefaultExport
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
function classifyExportedSymbols(symbols = []) {
|
|
2586
|
+
const source = ensureArray(symbols).map((value) => String(value || "").trim()).filter(Boolean);
|
|
2587
|
+
const providers = [];
|
|
2588
|
+
const constants = [];
|
|
2589
|
+
const functions = [];
|
|
2590
|
+
const classesOrTypes = [];
|
|
2591
|
+
const internals = [];
|
|
2592
|
+
const others = [];
|
|
2593
|
+
|
|
2594
|
+
for (const symbol of source) {
|
|
2595
|
+
if (/Provider$/.test(symbol)) {
|
|
2596
|
+
providers.push(symbol);
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
if (/^__/.test(symbol)) {
|
|
2600
|
+
internals.push(symbol);
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (/^[A-Z0-9_]+$/.test(symbol)) {
|
|
2604
|
+
constants.push(symbol);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
if (/^[a-z]/.test(symbol)) {
|
|
2608
|
+
functions.push(symbol);
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (/^[A-Z]/.test(symbol)) {
|
|
2612
|
+
classesOrTypes.push(symbol);
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
others.push(symbol);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
return {
|
|
2619
|
+
providers: sortStrings(providers),
|
|
2620
|
+
constants: sortStrings(constants),
|
|
2621
|
+
functions: sortStrings(functions),
|
|
2622
|
+
classesOrTypes: sortStrings(classesOrTypes),
|
|
2623
|
+
internals: sortStrings(internals),
|
|
2624
|
+
others: sortStrings(others)
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function formatPackageSubpathImport(packageId, subpath) {
|
|
2629
|
+
const normalizedPackageId = String(packageId || "").trim();
|
|
2630
|
+
const normalizedSubpath = String(subpath || "").trim();
|
|
2631
|
+
if (!normalizedPackageId) {
|
|
2632
|
+
return normalizedSubpath;
|
|
2633
|
+
}
|
|
2634
|
+
if (!normalizedSubpath || normalizedSubpath === ".") {
|
|
2635
|
+
return normalizedPackageId;
|
|
2636
|
+
}
|
|
2637
|
+
if (normalizedSubpath.startsWith("./")) {
|
|
2638
|
+
return `${normalizedPackageId}/${normalizedSubpath.slice(2)}`;
|
|
2639
|
+
}
|
|
2640
|
+
if (normalizedSubpath.startsWith("/")) {
|
|
2641
|
+
return `${normalizedPackageId}${normalizedSubpath}`;
|
|
2642
|
+
}
|
|
2643
|
+
return `${normalizedPackageId}/${normalizedSubpath}`;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
function normalizePlacementOutlets(value) {
|
|
2647
|
+
const outlets = [];
|
|
2648
|
+
const source = ensureArray(value);
|
|
2649
|
+
for (const entry of source) {
|
|
2650
|
+
const record = ensureObject(entry);
|
|
2651
|
+
const host = String(record.host || "").trim();
|
|
2652
|
+
const position = String(record.position || "").trim();
|
|
2653
|
+
if (!host || !position) {
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
|
|
2658
|
+
const description = String(record.description || "").trim();
|
|
2659
|
+
const sourceLabel = String(record.source || "").trim();
|
|
2660
|
+
outlets.push(
|
|
2661
|
+
Object.freeze({
|
|
2662
|
+
host,
|
|
2663
|
+
position,
|
|
2664
|
+
surfaces: Object.freeze(surfaces),
|
|
2665
|
+
description,
|
|
2666
|
+
source: sourceLabel
|
|
2667
|
+
})
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
return Object.freeze(
|
|
2672
|
+
[...outlets].sort((left, right) => {
|
|
2673
|
+
const hostCompare = left.host.localeCompare(right.host);
|
|
2674
|
+
if (hostCompare !== 0) {
|
|
2675
|
+
return hostCompare;
|
|
2676
|
+
}
|
|
2677
|
+
return left.position.localeCompare(right.position);
|
|
2678
|
+
})
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
function normalizePlacementContributions(value) {
|
|
2683
|
+
const contributions = [];
|
|
2684
|
+
for (const entry of ensureArray(value)) {
|
|
2685
|
+
const record = ensureObject(entry);
|
|
2686
|
+
const id = String(record.id || "").trim();
|
|
2687
|
+
const host = String(record.host || "").trim();
|
|
2688
|
+
const position = String(record.position || "").trim();
|
|
2689
|
+
if (!id || !host || !position) {
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
|
|
2694
|
+
const componentToken = String(record.componentToken || "").trim();
|
|
2695
|
+
const when = String(record.when || "").trim();
|
|
2696
|
+
const description = String(record.description || "").trim();
|
|
2697
|
+
const source = String(record.source || "").trim();
|
|
2698
|
+
const parsedOrder = Number(record.order);
|
|
2699
|
+
const order = Number.isFinite(parsedOrder) ? Math.trunc(parsedOrder) : null;
|
|
2700
|
+
contributions.push(
|
|
2701
|
+
Object.freeze({
|
|
2702
|
+
id,
|
|
2703
|
+
host,
|
|
2704
|
+
position,
|
|
2705
|
+
surfaces: Object.freeze(surfaces),
|
|
2706
|
+
order,
|
|
2707
|
+
componentToken,
|
|
2708
|
+
when,
|
|
2709
|
+
description,
|
|
2710
|
+
source
|
|
2711
|
+
})
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
return Object.freeze(
|
|
2716
|
+
[...contributions].sort((left, right) => {
|
|
2717
|
+
const hostCompare = left.host.localeCompare(right.host);
|
|
2718
|
+
if (hostCompare !== 0) {
|
|
2719
|
+
return hostCompare;
|
|
2720
|
+
}
|
|
2721
|
+
const positionCompare = left.position.localeCompare(right.position);
|
|
2722
|
+
if (positionCompare !== 0) {
|
|
2723
|
+
return positionCompare;
|
|
2724
|
+
}
|
|
2725
|
+
const leftOrder = Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
|
|
2726
|
+
const rightOrder = Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
|
|
2727
|
+
if (leftOrder !== rightOrder) {
|
|
2728
|
+
return leftOrder - rightOrder;
|
|
2729
|
+
}
|
|
2730
|
+
return left.id.localeCompare(right.id);
|
|
2731
|
+
})
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function deriveCanonicalExportTargetForSubpath(subpath) {
|
|
2736
|
+
const normalizedSubpath = String(subpath || "").trim();
|
|
2737
|
+
if (!normalizedSubpath) {
|
|
2738
|
+
return "";
|
|
2739
|
+
}
|
|
2740
|
+
if (normalizedSubpath === ".") {
|
|
2741
|
+
return "./src/index.js";
|
|
2742
|
+
}
|
|
2743
|
+
if (!normalizedSubpath.startsWith("./")) {
|
|
2744
|
+
return "";
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
const bareSubpath = normalizedSubpath.slice(2);
|
|
2748
|
+
if (!bareSubpath) {
|
|
2749
|
+
return "";
|
|
2750
|
+
}
|
|
2751
|
+
if (bareSubpath === "client" || bareSubpath === "server" || bareSubpath === "shared") {
|
|
2752
|
+
return `./src/${bareSubpath}/index.js`;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
const roots = ["client", "server", "shared"];
|
|
2756
|
+
for (const root of roots) {
|
|
2757
|
+
if (!bareSubpath.startsWith(`${root}/`)) {
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
const suffix = bareSubpath.slice(root.length + 1);
|
|
2761
|
+
if (!suffix) {
|
|
2762
|
+
return "";
|
|
2763
|
+
}
|
|
2764
|
+
const hasJsExtension = /\.(?:c|m)?js$/.test(suffix);
|
|
2765
|
+
const normalizedSuffix = hasJsExtension ? suffix : `${suffix}.js`;
|
|
2766
|
+
return `./src/${root}/${normalizedSuffix}`;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
return "";
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function shouldShowPackageExportTarget({ subpath, target, targetType }) {
|
|
2773
|
+
if (String(targetType || "").trim() !== "file") {
|
|
2774
|
+
return true;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
const canonicalTarget = deriveCanonicalExportTargetForSubpath(subpath);
|
|
2778
|
+
if (!canonicalTarget) {
|
|
2779
|
+
return true;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
const normalizeTarget = (value) => {
|
|
2783
|
+
const raw = String(value || "").trim();
|
|
2784
|
+
if (!raw) {
|
|
2785
|
+
return "";
|
|
2786
|
+
}
|
|
2787
|
+
const withoutPrefix = raw.startsWith("./") ? raw.slice(2) : raw;
|
|
2788
|
+
return `./${normalizeRelativePosixPath(withoutPrefix)}`;
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
return normalizeTarget(target) !== normalizeTarget(canonicalTarget);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function deriveProviderDisplayName(bindingRecord) {
|
|
2795
|
+
const binding = ensureObject(bindingRecord);
|
|
2796
|
+
const providerLabel = String(binding.provider || "").trim();
|
|
2797
|
+
if (!providerLabel) {
|
|
2798
|
+
return "";
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
const hashIndex = providerLabel.lastIndexOf("#");
|
|
2802
|
+
if (hashIndex > -1 && hashIndex < providerLabel.length - 1) {
|
|
2803
|
+
return providerLabel.slice(hashIndex + 1);
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const entrypoint = String(binding.entrypoint || "").trim();
|
|
2807
|
+
if (!entrypoint) {
|
|
2808
|
+
return providerLabel;
|
|
2809
|
+
}
|
|
2810
|
+
const basename = path.posix.basename(entrypoint);
|
|
2811
|
+
return basename.replace(/\.(?:c|m)?js$/i, "") || providerLabel;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
async function collectExportFileSymbolSummaries({ packageRoot, packageExports, notes }) {
|
|
2815
|
+
const rootDir = String(packageRoot || "").trim();
|
|
2816
|
+
if (!rootDir) {
|
|
2817
|
+
return [];
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
const exportTargets = new Map();
|
|
2821
|
+
for (const entry of ensureArray(packageExports)) {
|
|
2822
|
+
const record = ensureObject(entry);
|
|
2823
|
+
if (record.targetType !== "file" || record.targetExists !== true) {
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
const target = String(record.target || "").trim();
|
|
2828
|
+
if (!target.startsWith("./")) {
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
const normalizedTarget = normalizeRelativePosixPath(target.replace(/^\.\//, ""));
|
|
2832
|
+
const basename = path.posix.basename(normalizedTarget);
|
|
2833
|
+
if (!/\.(?:js|mjs|cjs)$/i.test(basename)) {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (!exportTargets.has(normalizedTarget)) {
|
|
2838
|
+
exportTargets.set(normalizedTarget, {
|
|
2839
|
+
file: normalizedTarget,
|
|
2840
|
+
subpaths: new Set(),
|
|
2841
|
+
conditions: new Set()
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
const bucket = exportTargets.get(normalizedTarget);
|
|
2845
|
+
bucket.subpaths.add(String(record.subpath || ".").trim() || ".");
|
|
2846
|
+
const condition = String(record.condition || "default").trim() || "default";
|
|
2847
|
+
if (condition !== "default") {
|
|
2848
|
+
bucket.conditions.add(condition);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const summaries = [];
|
|
2853
|
+
for (const [relativeTargetPath, bucket] of exportTargets.entries()) {
|
|
2854
|
+
const absoluteTargetPath = path.resolve(rootDir, relativeTargetPath);
|
|
2855
|
+
if (!(await fileExists(absoluteTargetPath))) {
|
|
2856
|
+
ensureArray(notes).push(`Export file missing: ${relativeTargetPath}`);
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
let source = "";
|
|
2861
|
+
try {
|
|
2862
|
+
source = await readFile(absoluteTargetPath, "utf8");
|
|
2863
|
+
} catch (error) {
|
|
2864
|
+
ensureArray(notes).push(
|
|
2865
|
+
`Failed to read export file ${relativeTargetPath}: ${String(error?.message || error || "unknown error")}`
|
|
2866
|
+
);
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
const summary = parseExportedSymbolsFromSource(source);
|
|
2871
|
+
summaries.push({
|
|
2872
|
+
file: normalizeRelativePosixPath(relativeTargetPath),
|
|
2873
|
+
subpaths: sortStrings([...bucket.subpaths]),
|
|
2874
|
+
conditions: sortStrings([...bucket.conditions]),
|
|
2875
|
+
symbols: ensureArray(summary.symbols),
|
|
2876
|
+
hasDefaultExport: Boolean(summary.hasDefaultExport),
|
|
2877
|
+
starReExports: ensureArray(summary.starReExports),
|
|
2878
|
+
namedReExports: ensureArray(summary.namedReExports)
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
return summaries.sort((left, right) => String(left.file || "").localeCompare(String(right.file || "")));
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
async function inspectPackageOfferings({ packageEntry }) {
|
|
2886
|
+
const rootDir = String(packageEntry?.rootDir || "").trim();
|
|
2887
|
+
const notes = [];
|
|
2888
|
+
const details = {
|
|
2889
|
+
available: Boolean(rootDir),
|
|
2890
|
+
notes,
|
|
2891
|
+
packageExports: [],
|
|
2892
|
+
containerBindings: {
|
|
2893
|
+
server: [],
|
|
2894
|
+
client: []
|
|
2895
|
+
},
|
|
2896
|
+
exportedSymbols: []
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
if (!rootDir) {
|
|
2900
|
+
notes.push("Source files are unavailable for static introspection (catalog metadata only).");
|
|
2901
|
+
return details;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
const packageJson = ensureObject(packageEntry?.packageJson);
|
|
2905
|
+
details.packageExports = await describePackageExports({
|
|
2906
|
+
packageRoot: rootDir,
|
|
2907
|
+
packageJson
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
const runtime = ensureObject(packageEntry?.descriptor?.runtime);
|
|
2911
|
+
const runtimeSides = [
|
|
2912
|
+
{
|
|
2913
|
+
side: "server",
|
|
2914
|
+
providers: ensureArray(ensureObject(runtime.server).providers)
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
side: "client",
|
|
2918
|
+
providers: ensureArray(ensureObject(runtime.client).providers)
|
|
2919
|
+
}
|
|
2920
|
+
];
|
|
2921
|
+
|
|
2922
|
+
for (const runtimeSide of runtimeSides) {
|
|
2923
|
+
const side = String(runtimeSide.side || "").trim();
|
|
2924
|
+
if (!side) {
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
const bindings = [];
|
|
2928
|
+
for (const provider of runtimeSide.providers) {
|
|
2929
|
+
const record = ensureObject(provider);
|
|
2930
|
+
const entrypoint = String(record.entrypoint || "").trim();
|
|
2931
|
+
const exportName = String(record.export || "").trim();
|
|
2932
|
+
if (!entrypoint) {
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
const providerLabel = exportName ? `${entrypoint}#${exportName}` : entrypoint;
|
|
2937
|
+
if (entrypoint.includes("*")) {
|
|
2938
|
+
notes.push(`Skipped wildcard provider entrypoint during introspection: ${providerLabel}`);
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const providerPath = path.resolve(rootDir, entrypoint);
|
|
2943
|
+
if (!(await fileExists(providerPath))) {
|
|
2944
|
+
notes.push(`Provider file missing during introspection: ${providerLabel}`);
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
let source = "";
|
|
2949
|
+
try {
|
|
2950
|
+
source = await readFile(providerPath, "utf8");
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
notes.push(`Failed reading provider ${providerLabel}: ${String(error?.message || error || "unknown error")}`);
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
bindings.push(
|
|
2957
|
+
...collectContainerBindingsFromProviderSource({
|
|
2958
|
+
source,
|
|
2959
|
+
providerLabel,
|
|
2960
|
+
entrypoint,
|
|
2961
|
+
providerExportName: exportName
|
|
2962
|
+
})
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
details.containerBindings[side] = bindings.sort((left, right) => {
|
|
2967
|
+
const tokenComparison = String(left?.token || "").localeCompare(String(right?.token || ""));
|
|
2968
|
+
if (tokenComparison !== 0) {
|
|
2969
|
+
return tokenComparison;
|
|
2970
|
+
}
|
|
2971
|
+
const providerComparison = String(left?.provider || "").localeCompare(String(right?.provider || ""));
|
|
2972
|
+
if (providerComparison !== 0) {
|
|
2973
|
+
return providerComparison;
|
|
2974
|
+
}
|
|
2975
|
+
return Number(left?.line || 0) - Number(right?.line || 0);
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
details.exportedSymbols = await collectExportFileSymbolSummaries({
|
|
2980
|
+
packageRoot: rootDir,
|
|
2981
|
+
packageExports: details.packageExports,
|
|
2982
|
+
notes
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
return details;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function collectPlannedCapabilityIssues(plannedPackageIds, packageRegistry) {
|
|
2989
|
+
const selectedPackageIds = sortStrings(
|
|
2990
|
+
[...new Set(ensureArray(plannedPackageIds).map((value) => String(value || "").trim()).filter(Boolean))]
|
|
2991
|
+
);
|
|
2992
|
+
const selectedPackageSet = new Set(selectedPackageIds);
|
|
2993
|
+
const providersByCapability = new Map();
|
|
2994
|
+
|
|
2995
|
+
for (const [capabilityId, providers] of Object.entries(BUILTIN_CAPABILITY_PROVIDERS)) {
|
|
2996
|
+
if (!providersByCapability.has(capabilityId)) {
|
|
2997
|
+
providersByCapability.set(capabilityId, new Set());
|
|
2998
|
+
}
|
|
2999
|
+
for (const providerId of ensureArray(providers).map((value) => String(value || "").trim()).filter(Boolean)) {
|
|
3000
|
+
providersByCapability.get(capabilityId).add(providerId);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
for (const packageId of selectedPackageIds) {
|
|
3005
|
+
const packageEntry = packageRegistry.get(packageId);
|
|
3006
|
+
if (!packageEntry) {
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
const provides = listDeclaredCapabilities(packageEntry.descriptor.capabilities, "provides");
|
|
3010
|
+
for (const capabilityId of provides) {
|
|
3011
|
+
if (!providersByCapability.has(capabilityId)) {
|
|
3012
|
+
providersByCapability.set(capabilityId, new Set());
|
|
3013
|
+
}
|
|
3014
|
+
providersByCapability.get(capabilityId).add(packageId);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
const issues = [];
|
|
3019
|
+
for (const packageId of selectedPackageIds) {
|
|
3020
|
+
const packageEntry = packageRegistry.get(packageId);
|
|
3021
|
+
if (!packageEntry) {
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
const requires = listDeclaredCapabilities(packageEntry.descriptor.capabilities, "requires");
|
|
3025
|
+
for (const capabilityId of requires) {
|
|
3026
|
+
const selectedProviders = providersByCapability.get(capabilityId);
|
|
3027
|
+
if (selectedProviders && selectedProviders.size > 0) {
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
const availableProviders = [];
|
|
3032
|
+
for (const [candidatePackageId, candidatePackageEntry] of packageRegistry.entries()) {
|
|
3033
|
+
if (selectedPackageSet.has(candidatePackageId)) {
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
const candidateProvides = listDeclaredCapabilities(candidatePackageEntry.descriptor.capabilities, "provides");
|
|
3037
|
+
if (candidateProvides.includes(capabilityId)) {
|
|
3038
|
+
availableProviders.push(candidatePackageId);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
issues.push({
|
|
3043
|
+
packageId,
|
|
3044
|
+
capabilityId,
|
|
3045
|
+
availableProviders: sortStrings(availableProviders)
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
return issues;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
function validatePlannedCapabilityClosure(plannedPackageIds, packageRegistry, actionLabel) {
|
|
3054
|
+
const issues = collectPlannedCapabilityIssues(plannedPackageIds, packageRegistry);
|
|
3055
|
+
if (issues.length === 0) {
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
const lines = [`Cannot ${actionLabel}: capability requirements are not satisfied.`];
|
|
3060
|
+
for (const issue of issues) {
|
|
3061
|
+
const providersHint = issue.availableProviders.length > 0
|
|
3062
|
+
? ` Available providers: ${issue.availableProviders.join(", ")}.`
|
|
3063
|
+
: "";
|
|
3064
|
+
lines.push(
|
|
3065
|
+
`- ${issue.packageId} requires capability ${issue.capabilityId}, but no selected package provides it.${providersHint}`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
throw createCliError(lines.join("\n"));
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function resolveSurfaceVisibilityOptionPolicy(packageEntry = {}) {
|
|
3073
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
3074
|
+
const optionPolicies = ensureObject(descriptor.optionPolicies);
|
|
3075
|
+
const surfaceVisibilityPolicy = optionPolicies.surfaceVisibility;
|
|
3076
|
+
if (!surfaceVisibilityPolicy || surfaceVisibilityPolicy === false) {
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
let policy = {};
|
|
3081
|
+
if (surfaceVisibilityPolicy === true) {
|
|
3082
|
+
policy = {};
|
|
3083
|
+
} else if (typeof surfaceVisibilityPolicy === "object" && !Array.isArray(surfaceVisibilityPolicy)) {
|
|
3084
|
+
policy = ensureObject(surfaceVisibilityPolicy);
|
|
3085
|
+
} else {
|
|
3086
|
+
throw createCliError(
|
|
3087
|
+
`Invalid option policy in package ${packageEntry.packageId}: surfaceVisibility must be true or an object.`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const surfaceOption = String(policy.surfaceOption || "surface").trim();
|
|
3092
|
+
const visibilityOption = String(policy.visibilityOption || "visibility").trim();
|
|
3093
|
+
if (!surfaceOption || !visibilityOption) {
|
|
3094
|
+
throw createCliError(
|
|
3095
|
+
`Invalid option policy in package ${packageEntry.packageId}: surfaceVisibility requires non-empty surfaceOption and visibilityOption.`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
return Object.freeze({
|
|
3100
|
+
surfaceOption,
|
|
3101
|
+
visibilityOption,
|
|
3102
|
+
allowAuto: policy.allowAuto !== false
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function resolveSurfaceDefinitionsForOptionPolicy(configContext = {}) {
|
|
3107
|
+
const publicConfig = ensureObject(configContext.public);
|
|
3108
|
+
const mergedConfig = ensureObject(configContext.merged);
|
|
3109
|
+
const sourceDefinitions = ensureObject(publicConfig.surfaceDefinitions);
|
|
3110
|
+
const fallbackDefinitions = ensureObject(mergedConfig.surfaceDefinitions);
|
|
3111
|
+
const surfaceDefinitions =
|
|
3112
|
+
Object.keys(sourceDefinitions).length > 0 ? sourceDefinitions : fallbackDefinitions;
|
|
3113
|
+
|
|
3114
|
+
const normalizedDefinitions = {};
|
|
3115
|
+
for (const [key, value] of Object.entries(surfaceDefinitions)) {
|
|
3116
|
+
const definition = ensureObject(value);
|
|
3117
|
+
const definitionId = normalizeSurfaceIdForMutation(definition.id || key);
|
|
3118
|
+
if (!definitionId) {
|
|
3119
|
+
continue;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
normalizedDefinitions[definitionId] = Object.freeze({
|
|
3123
|
+
id: definitionId,
|
|
3124
|
+
enabled: definition.enabled !== false,
|
|
3125
|
+
requiresWorkspace: definition.requiresWorkspace === true
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
return Object.freeze(normalizedDefinitions);
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function normalizeResolvedOptionValue(value = "") {
|
|
3133
|
+
return String(value || "").trim().toLowerCase();
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function validateSurfaceVisibilityOptionPolicy({
|
|
3137
|
+
packageEntry,
|
|
3138
|
+
resolvedOptions = {},
|
|
3139
|
+
policy,
|
|
3140
|
+
configContext = {}
|
|
3141
|
+
} = {}) {
|
|
3142
|
+
const packageId = String(packageEntry?.packageId || "").trim() || "unknown-package";
|
|
3143
|
+
const surfaceIds = parseSurfaceIdListForMutation(resolvedOptions?.[policy.surfaceOption]);
|
|
3144
|
+
const visibility = normalizeResolvedOptionValue(resolvedOptions?.[policy.visibilityOption]);
|
|
3145
|
+
if (surfaceIds.length < 1 || !visibility) {
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
const skipWorkspaceRequirement = policy.allowAuto && visibility === "auto";
|
|
3149
|
+
|
|
3150
|
+
const surfaceDefinitions = resolveSurfaceDefinitionsForOptionPolicy(configContext);
|
|
3151
|
+
for (const surfaceId of surfaceIds) {
|
|
3152
|
+
const surfaceDefinition = surfaceDefinitions[surfaceId];
|
|
3153
|
+
if (!surfaceDefinition) {
|
|
3154
|
+
throw createCliError(
|
|
3155
|
+
`Invalid option combination for package ${packageId}: --${policy.surfaceOption} includes unknown surface "${surfaceId}" in config/public.js.`
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
if (surfaceDefinition.enabled !== true) {
|
|
3159
|
+
throw createCliError(
|
|
3160
|
+
`Invalid option combination for package ${packageId}: surface "${surfaceId}" is disabled in config/public.js.`
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
if (!skipWorkspaceRequirement && WORKSPACE_VISIBILITY_SET.has(visibility) && surfaceDefinition.requiresWorkspace !== true) {
|
|
3165
|
+
throw createCliError(
|
|
3166
|
+
`Invalid option combination for package ${packageId}: --${policy.visibilityOption} "${visibility}" requires surfaces with requiresWorkspace=true, but "${surfaceId}" has requiresWorkspace=false.`
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
async function validateResolvedOptionPolicies({
|
|
3173
|
+
packageEntry,
|
|
3174
|
+
resolvedOptions = {},
|
|
3175
|
+
appRoot = "",
|
|
3176
|
+
resolveConfigContext
|
|
3177
|
+
} = {}) {
|
|
3178
|
+
const policy = resolveSurfaceVisibilityOptionPolicy(packageEntry);
|
|
3179
|
+
if (!policy) {
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
if (!appRoot) {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const configContext = await resolveConfigContext();
|
|
3187
|
+
validateSurfaceVisibilityOptionPolicy({
|
|
3188
|
+
packageEntry,
|
|
3189
|
+
resolvedOptions,
|
|
3190
|
+
policy,
|
|
3191
|
+
configContext
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
async function resolvePackageOptions(packageEntry, inlineOptions, io, { appRoot = "" } = {}) {
|
|
3196
|
+
const optionSchemas = ensureObject(packageEntry.descriptor.options);
|
|
3197
|
+
const optionNames = Object.keys(optionSchemas);
|
|
3198
|
+
const resolved = {};
|
|
3199
|
+
const inlineOptionValues = ensureObject(inlineOptions);
|
|
3200
|
+
const hasInlineOption = (name) => Object.prototype.hasOwnProperty.call(inlineOptionValues, name);
|
|
3201
|
+
let configContext = null;
|
|
3202
|
+
|
|
3203
|
+
async function loadConfigContext() {
|
|
3204
|
+
if (!configContext) {
|
|
3205
|
+
configContext = await loadMutationWhenConfigContext(appRoot);
|
|
3206
|
+
}
|
|
3207
|
+
return configContext;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
async function resolveOptionDefaultFromConfig(configPath = "") {
|
|
3211
|
+
const normalizedConfigPath = String(configPath || "").trim();
|
|
3212
|
+
if (!normalizedConfigPath || !appRoot) {
|
|
3213
|
+
return "";
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
await loadConfigContext();
|
|
3217
|
+
return normalizeWhenSourceValue(resolveWhenConfigValue(configContext, normalizedConfigPath));
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
for (const optionName of optionNames) {
|
|
3221
|
+
const schema = ensureObject(optionSchemas[optionName]);
|
|
3222
|
+
const allowEmpty = schema.allowEmpty === true;
|
|
3223
|
+
if (hasInlineOption(optionName)) {
|
|
3224
|
+
const inlineValue = String(inlineOptionValues[optionName] || "").trim();
|
|
3225
|
+
if (inlineValue || allowEmpty) {
|
|
3226
|
+
resolved[optionName] = inlineValue;
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
if (schema.required) {
|
|
3230
|
+
throw createCliError(`Package ${packageEntry.packageId} option ${optionName} requires a non-empty value.`);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
const defaultFromConfigPath = String(schema.defaultFromConfig || "").trim();
|
|
3235
|
+
if (defaultFromConfigPath) {
|
|
3236
|
+
const defaultFromConfigValue = await resolveOptionDefaultFromConfig(defaultFromConfigPath);
|
|
3237
|
+
if (defaultFromConfigValue || allowEmpty) {
|
|
3238
|
+
resolved[optionName] = defaultFromConfigValue;
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
if (typeof schema.defaultValue === "string" && schema.defaultValue.trim()) {
|
|
3244
|
+
resolved[optionName] = schema.defaultValue.trim();
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
if (schema.required) {
|
|
3249
|
+
resolved[optionName] = await promptForRequiredOption({
|
|
3250
|
+
ownerType: "package",
|
|
3251
|
+
ownerId: packageEntry.packageId,
|
|
3252
|
+
optionName,
|
|
3253
|
+
optionSchema: schema,
|
|
3254
|
+
stdin: io.stdin,
|
|
3255
|
+
stdout: io.stdout
|
|
3256
|
+
});
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
resolved[optionName] = "";
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
await validateResolvedOptionPolicies({
|
|
3264
|
+
packageEntry,
|
|
3265
|
+
resolvedOptions: resolved,
|
|
3266
|
+
appRoot,
|
|
3267
|
+
resolveConfigContext: loadConfigContext
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
return resolved;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function validateInlineOptionsForPackage(packageEntry, inlineOptions) {
|
|
3274
|
+
const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
|
|
3275
|
+
const allowedOptionNames = Object.keys(optionSchemas);
|
|
3276
|
+
const allowed = new Set(allowedOptionNames);
|
|
3277
|
+
const providedOptionNames = Object.keys(ensureObject(inlineOptions));
|
|
3278
|
+
const unknownOptionNames = providedOptionNames.filter((optionName) => !allowed.has(optionName));
|
|
3279
|
+
|
|
3280
|
+
if (unknownOptionNames.length < 1) {
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
const sortedUnknown = sortStrings(unknownOptionNames);
|
|
3285
|
+
const suffix = allowedOptionNames.length > 0
|
|
3286
|
+
? ` Allowed options: ${sortStrings(allowedOptionNames).join(", ")}.`
|
|
3287
|
+
: " This package does not accept inline options.";
|
|
3288
|
+
|
|
3289
|
+
throw createCliError(
|
|
3290
|
+
`Unknown option(s) for package ${packageEntry.packageId}: ${sortedUnknown.join(", ")}.${suffix}`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function createManagedRecordBase(packageEntry, options) {
|
|
3295
|
+
const sourceRecord = {
|
|
3296
|
+
type: String(packageEntry?.sourceType || "packages-directory"),
|
|
3297
|
+
...ensureObject(packageEntry?.source)
|
|
3298
|
+
};
|
|
3299
|
+
if (!sourceRecord.descriptorPath && String(packageEntry?.descriptorRelativePath || "").trim()) {
|
|
3300
|
+
sourceRecord.descriptorPath = String(packageEntry.descriptorRelativePath).trim();
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
return {
|
|
3304
|
+
packageId: packageEntry.packageId,
|
|
3305
|
+
version: packageEntry.version,
|
|
3306
|
+
source: sourceRecord,
|
|
3307
|
+
managed: {
|
|
3308
|
+
packageJson: {
|
|
3309
|
+
dependencies: {},
|
|
3310
|
+
devDependencies: {},
|
|
3311
|
+
scripts: {}
|
|
3312
|
+
},
|
|
3313
|
+
text: {},
|
|
3314
|
+
vite: {},
|
|
3315
|
+
files: [],
|
|
3316
|
+
migrations: []
|
|
3317
|
+
},
|
|
3318
|
+
options,
|
|
3319
|
+
installedAt: new Date().toISOString()
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
async function runCommandCapture(command, args, { cwd } = {}) {
|
|
3324
|
+
return await new Promise((resolve, reject) => {
|
|
3325
|
+
const child = spawn(command, args, {
|
|
3326
|
+
cwd,
|
|
3327
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
let stdout = "";
|
|
3331
|
+
let stderr = "";
|
|
3332
|
+
|
|
3333
|
+
child.stdout.on("data", (chunk) => {
|
|
3334
|
+
stdout += chunk.toString("utf8");
|
|
3335
|
+
});
|
|
3336
|
+
child.stderr.on("data", (chunk) => {
|
|
3337
|
+
stderr += chunk.toString("utf8");
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
child.on("error", reject);
|
|
3341
|
+
child.on("exit", (code) => {
|
|
3342
|
+
if (code === 0) {
|
|
3343
|
+
resolve({
|
|
3344
|
+
stdout,
|
|
3345
|
+
stderr
|
|
3346
|
+
});
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
const details = String(stderr || stdout || "").trim();
|
|
3350
|
+
reject(createCliError(`${command} ${args.join(" ")} failed with exit code ${code}.${details ? ` ${details}` : ""}`));
|
|
3351
|
+
});
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function extractPackTarballName(packStdout) {
|
|
3356
|
+
const lines = String(packStdout || "")
|
|
3357
|
+
.split(/\r?\n/)
|
|
3358
|
+
.map((value) => value.trim())
|
|
3359
|
+
.filter(Boolean);
|
|
3360
|
+
return lines.length > 0 ? lines[lines.length - 1] : "";
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
async function materializePackageRootFromRegistry({ packageEntry, appRoot }) {
|
|
3364
|
+
const cacheKey = `${packageEntry.packageId}@${packageEntry.version}`;
|
|
3365
|
+
if (MATERIALIZED_PACKAGE_ROOTS.has(cacheKey)) {
|
|
3366
|
+
return MATERIALIZED_PACKAGE_ROOTS.get(cacheKey);
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "jskit-cli-pack-"));
|
|
3370
|
+
MATERIALIZED_PACKAGE_TEMP_DIRECTORIES.add(tempRoot);
|
|
3371
|
+
const packageSpec = `${packageEntry.packageId}@${packageEntry.version}`;
|
|
3372
|
+
const packResult = await runCommandCapture(
|
|
3373
|
+
"npm",
|
|
3374
|
+
["pack", packageSpec, "--silent", "--pack-destination", tempRoot],
|
|
3375
|
+
{ cwd: appRoot }
|
|
3376
|
+
);
|
|
3377
|
+
const tarballName = extractPackTarballName(packResult.stdout);
|
|
3378
|
+
if (!tarballName) {
|
|
3379
|
+
throw createCliError(`Unable to materialize ${packageSpec}: npm pack produced no tarball name.`);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
const tarballPath = path.join(tempRoot, tarballName);
|
|
3383
|
+
if (!(await fileExists(tarballPath))) {
|
|
3384
|
+
throw createCliError(`Unable to materialize ${packageSpec}: tarball missing at ${tarballPath}.`);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
const extractedRoot = path.join(tempRoot, "extracted");
|
|
3388
|
+
await mkdir(extractedRoot, { recursive: true });
|
|
3389
|
+
await runCommandCapture("tar", ["-xzf", tarballPath, "-C", extractedRoot], { cwd: appRoot });
|
|
3390
|
+
const packageRoot = path.join(extractedRoot, "package");
|
|
3391
|
+
const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
|
|
3392
|
+
if (!(await fileExists(descriptorPath))) {
|
|
3393
|
+
throw createCliError(`Materialized package ${packageSpec} does not contain package.descriptor.mjs.`);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
MATERIALIZED_PACKAGE_ROOTS.set(cacheKey, packageRoot);
|
|
3397
|
+
return packageRoot;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
async function resolvePackageTemplateRoot({ packageEntry, appRoot }) {
|
|
3401
|
+
const packageRoot = String(packageEntry?.rootDir || "").trim();
|
|
3402
|
+
if (packageRoot) {
|
|
3403
|
+
return packageRoot;
|
|
3404
|
+
}
|
|
3405
|
+
return await materializePackageRootFromRegistry({ packageEntry, appRoot });
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
async function cleanupMaterializedPackageRoots() {
|
|
3409
|
+
for (const tempDirectory of MATERIALIZED_PACKAGE_TEMP_DIRECTORIES) {
|
|
3410
|
+
await rm(tempDirectory, { recursive: true, force: true }).catch(() => {});
|
|
3411
|
+
}
|
|
3412
|
+
MATERIALIZED_PACKAGE_TEMP_DIRECTORIES.clear();
|
|
3413
|
+
MATERIALIZED_PACKAGE_ROOTS.clear();
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function interpolateFileMutationRecord(mutation, options, packageId) {
|
|
3417
|
+
const mutationKey = String(
|
|
3418
|
+
mutation?.id || mutation?.slug || mutation?.to || mutation?.toSurface || mutation?.from || "files"
|
|
3419
|
+
).trim();
|
|
3420
|
+
const interpolate = (value, field) =>
|
|
3421
|
+
interpolateOptionValue(String(value || ""), options, packageId, `${mutationKey}.${field}`);
|
|
3422
|
+
|
|
3423
|
+
return {
|
|
3424
|
+
...mutation,
|
|
3425
|
+
from: interpolate(mutation.from, "from"),
|
|
3426
|
+
to: interpolate(mutation.to, "to"),
|
|
3427
|
+
toSurface: interpolate(mutation.toSurface, "toSurface"),
|
|
3428
|
+
toSurfacePath: interpolate(mutation.toSurfacePath, "toSurfacePath"),
|
|
3429
|
+
toDir: interpolate(mutation.toDir, "toDir"),
|
|
3430
|
+
slug: interpolate(mutation.slug, "slug"),
|
|
3431
|
+
extension: interpolate(mutation.extension, "extension"),
|
|
3432
|
+
id: interpolate(mutation.id, "id"),
|
|
3433
|
+
category: interpolate(mutation.category, "category"),
|
|
3434
|
+
reason: interpolate(mutation.reason, "reason")
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
async function copyTemplateFile(sourcePath, targetPath, options, packageId, interpolationKey) {
|
|
3439
|
+
const sourceContent = await readFile(sourcePath, "utf8");
|
|
3440
|
+
const renderedContent = sourceContent.includes("${")
|
|
3441
|
+
? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
|
|
3442
|
+
: sourceContent;
|
|
3443
|
+
|
|
3444
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
3445
|
+
await writeFile(targetPath, renderedContent, "utf8");
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function normalizeSurfaceIdForMutation(value = "") {
|
|
3449
|
+
return String(value || "")
|
|
3450
|
+
.trim()
|
|
3451
|
+
.toLowerCase();
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
function parseSurfaceIdListForMutation(value = "") {
|
|
3455
|
+
const sourceValues = Array.isArray(value) ? value : [value];
|
|
3456
|
+
const surfaceIds = [];
|
|
3457
|
+
const seen = new Set();
|
|
3458
|
+
|
|
3459
|
+
for (const sourceValue of sourceValues) {
|
|
3460
|
+
const parsed = String(sourceValue || "")
|
|
3461
|
+
.split(",")
|
|
3462
|
+
.map((entry) => normalizeSurfaceIdForMutation(entry))
|
|
3463
|
+
.filter(Boolean);
|
|
3464
|
+
for (const surfaceId of parsed) {
|
|
3465
|
+
if (seen.has(surfaceId)) {
|
|
3466
|
+
continue;
|
|
3467
|
+
}
|
|
3468
|
+
seen.add(surfaceId);
|
|
3469
|
+
surfaceIds.push(surfaceId);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
return Object.freeze(surfaceIds);
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
function normalizeSurfacePagesRootForMutation(value = "") {
|
|
3477
|
+
const rawValue = String(value || "").trim();
|
|
3478
|
+
if (!rawValue || rawValue === "/") {
|
|
3479
|
+
return "";
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
return rawValue
|
|
3483
|
+
.replace(/\\/g, "/")
|
|
3484
|
+
.replace(/\/{2,}/g, "/")
|
|
3485
|
+
.replace(/^\/+|\/+$/g, "");
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
function normalizeSurfacePathForMutation(value = "", { context = "toSurfacePath" } = {}) {
|
|
3489
|
+
const rawValue = String(value || "").trim();
|
|
3490
|
+
if (!rawValue) {
|
|
3491
|
+
return "";
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
const normalized = rawValue
|
|
3495
|
+
.replace(/\\/g, "/")
|
|
3496
|
+
.replace(/\/{2,}/g, "/")
|
|
3497
|
+
.replace(/^\.\/+/, "")
|
|
3498
|
+
.replace(/^\/+/, "");
|
|
3499
|
+
const segments = normalized.split("/");
|
|
3500
|
+
const materializedSegments = [];
|
|
3501
|
+
for (const segmentValue of segments) {
|
|
3502
|
+
const segment = String(segmentValue || "").trim();
|
|
3503
|
+
if (!segment || segment === ".") {
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
if (segment === "..") {
|
|
3507
|
+
throw createCliError(`Invalid ${context}: path traversal is not allowed.`);
|
|
3508
|
+
}
|
|
3509
|
+
materializedSegments.push(segment);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
return materializedSegments.join("/");
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
function resolveSurfaceDefinitionFromConfigForMutation({
|
|
3516
|
+
configContext = {},
|
|
3517
|
+
surfaceId = "",
|
|
3518
|
+
packageId = ""
|
|
3519
|
+
} = {}) {
|
|
3520
|
+
const normalizedSurfaceId = normalizeSurfaceIdForMutation(surfaceId);
|
|
3521
|
+
if (!normalizedSurfaceId) {
|
|
3522
|
+
throw createCliError(`Invalid files mutation in ${packageId}: "toSurface" is required when using surface targeting.`);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
const publicConfig = ensureObject(configContext.public);
|
|
3526
|
+
const mergedConfig = ensureObject(configContext.merged);
|
|
3527
|
+
const sourceDefinitions = ensureObject(publicConfig.surfaceDefinitions);
|
|
3528
|
+
const fallbackDefinitions = ensureObject(mergedConfig.surfaceDefinitions);
|
|
3529
|
+
const surfaceDefinitions =
|
|
3530
|
+
Object.keys(sourceDefinitions).length > 0 ? sourceDefinitions : fallbackDefinitions;
|
|
3531
|
+
|
|
3532
|
+
for (const [key, value] of Object.entries(surfaceDefinitions)) {
|
|
3533
|
+
const definition = ensureObject(value);
|
|
3534
|
+
const definitionId = normalizeSurfaceIdForMutation(definition.id || key);
|
|
3535
|
+
if (definitionId !== normalizedSurfaceId) {
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
if (definition.enabled === false) {
|
|
3539
|
+
throw createCliError(
|
|
3540
|
+
`Invalid files mutation in ${packageId}: surface "${normalizedSurfaceId}" is disabled.`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
if (!Object.prototype.hasOwnProperty.call(definition, "pagesRoot")) {
|
|
3544
|
+
throw createCliError(
|
|
3545
|
+
`Invalid files mutation in ${packageId}: surface "${normalizedSurfaceId}" is missing pagesRoot in config/public.js.`
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
return Object.freeze({
|
|
3550
|
+
...definition,
|
|
3551
|
+
id: definitionId,
|
|
3552
|
+
pagesRoot: normalizeSurfacePagesRootForMutation(definition.pagesRoot)
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
throw createCliError(
|
|
3557
|
+
`Invalid files mutation in ${packageId}: unknown surface "${normalizedSurfaceId}" in config/public.js.`
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
function resolveSurfaceTargetPathsForMutation({
|
|
3562
|
+
appRoot,
|
|
3563
|
+
packageId,
|
|
3564
|
+
mutation,
|
|
3565
|
+
configContext
|
|
3566
|
+
} = {}) {
|
|
3567
|
+
const normalizedSurfaceIds = parseSurfaceIdListForMutation(mutation.toSurface);
|
|
3568
|
+
if (normalizedSurfaceIds.length < 1) {
|
|
3569
|
+
throw createCliError(`Invalid files mutation in ${packageId}: "toSurface" is required when using surface targeting.`);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
if (mutation.toSurfaceRoot === true) {
|
|
3573
|
+
if (String(mutation.toSurfacePath || "").trim()) {
|
|
3574
|
+
throw createCliError(
|
|
3575
|
+
`Invalid files mutation in ${packageId}: "toSurfacePath" cannot be combined with "toSurfaceRoot".`
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
const targetPaths = [];
|
|
3580
|
+
for (const surfaceId of normalizedSurfaceIds) {
|
|
3581
|
+
const definition = resolveSurfaceDefinitionFromConfigForMutation({
|
|
3582
|
+
configContext,
|
|
3583
|
+
surfaceId,
|
|
3584
|
+
packageId
|
|
3585
|
+
});
|
|
3586
|
+
if (!definition.pagesRoot) {
|
|
3587
|
+
throw createCliError(
|
|
3588
|
+
`Invalid files mutation in ${packageId}: root surface "${surfaceId}" cannot use "toSurfaceRoot".`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
targetPaths.push(path.join(appRoot, "src/pages", `${definition.pagesRoot}.vue`));
|
|
3592
|
+
}
|
|
3593
|
+
return Object.freeze(targetPaths);
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
const normalizedSurfacePath = normalizeSurfacePathForMutation(mutation.toSurfacePath, {
|
|
3597
|
+
context: "toSurfacePath"
|
|
3598
|
+
});
|
|
3599
|
+
if (!normalizedSurfacePath) {
|
|
3600
|
+
throw createCliError(
|
|
3601
|
+
`Invalid files mutation in ${packageId}: "toSurfacePath" is required when using "toSurface".`
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
const targetPaths = [];
|
|
3606
|
+
for (const surfaceId of normalizedSurfaceIds) {
|
|
3607
|
+
const definition = resolveSurfaceDefinitionFromConfigForMutation({
|
|
3608
|
+
configContext,
|
|
3609
|
+
surfaceId,
|
|
3610
|
+
packageId
|
|
3611
|
+
});
|
|
3612
|
+
const basePagesDirectory = definition.pagesRoot
|
|
3613
|
+
? path.join(appRoot, "src/pages", definition.pagesRoot)
|
|
3614
|
+
: path.join(appRoot, "src/pages");
|
|
3615
|
+
targetPaths.push(path.join(basePagesDirectory, normalizedSurfacePath));
|
|
3616
|
+
}
|
|
3617
|
+
return Object.freeze(targetPaths);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
async function applyFileMutations(
|
|
3621
|
+
packageEntry,
|
|
3622
|
+
options,
|
|
3623
|
+
appRoot,
|
|
3624
|
+
fileMutations,
|
|
3625
|
+
managedFiles,
|
|
3626
|
+
managedMigrations,
|
|
3627
|
+
touchedFiles,
|
|
3628
|
+
warnings = []
|
|
3629
|
+
) {
|
|
3630
|
+
for (const mutationValue of fileMutations) {
|
|
3631
|
+
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
3632
|
+
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
3633
|
+
const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
3634
|
+
if (
|
|
3635
|
+
!shouldApplyMutationWhen(normalizedMutation.when, {
|
|
3636
|
+
options,
|
|
3637
|
+
configContext,
|
|
3638
|
+
packageId: packageEntry.packageId,
|
|
3639
|
+
mutationContext: "files mutation"
|
|
3640
|
+
})
|
|
3641
|
+
) {
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
|
|
3646
|
+
const operation = mutation.op || "copy-file";
|
|
3647
|
+
|
|
3648
|
+
if (operation === "install-migration") {
|
|
3649
|
+
const rawMutation = ensureObject(mutationValue);
|
|
3650
|
+
if (Object.hasOwn(rawMutation, "preserveOnRemove")) {
|
|
3651
|
+
warnings.push(
|
|
3652
|
+
`${packageEntry.packageId}: install-migration ignores preserveOnRemove (migrations are always preserved on remove).`
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
const from = mutation.from;
|
|
3657
|
+
const toDir = mutation.toDir || "migrations";
|
|
3658
|
+
if (!from) {
|
|
3659
|
+
throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: \"from\" is required.`);
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
const slug = normalizeMigrationSlug(mutation.slug, packageEntry.packageId);
|
|
3663
|
+
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
3664
|
+
if (!(await fileExists(sourcePath))) {
|
|
3665
|
+
throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
const sourceContent = await readFile(sourcePath, "utf8");
|
|
3669
|
+
const renderedSourceContent = sourceContent.includes("${")
|
|
3670
|
+
? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || slug}.source`)
|
|
3671
|
+
: sourceContent;
|
|
3672
|
+
const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
|
|
3673
|
+
const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
|
|
3674
|
+
const migrationId =
|
|
3675
|
+
String(mutation.id || "").trim() ||
|
|
3676
|
+
extractMigrationIdFromSource(renderedSourceContent) ||
|
|
3677
|
+
`${packageEntry.packageId}:${slug}`;
|
|
3678
|
+
const existingMigration = await findExistingMigrationById({
|
|
3679
|
+
appRoot,
|
|
3680
|
+
migrationsDirectory: toDir,
|
|
3681
|
+
migrationId
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
if (existingMigration) {
|
|
3685
|
+
warnings.push(
|
|
3686
|
+
`${packageEntry.packageId}: skipped migration ${migrationId} (already installed at ${existingMigration.path}).`
|
|
3687
|
+
);
|
|
3688
|
+
managedMigrations.push({
|
|
3689
|
+
id: migrationId,
|
|
3690
|
+
path: existingMigration.path,
|
|
3691
|
+
skipped: true,
|
|
3692
|
+
reason: mutation.reason,
|
|
3693
|
+
category: mutation.category
|
|
3694
|
+
});
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
const migrationsDirectoryAbsolute = path.join(appRoot, toDir);
|
|
3699
|
+
await mkdir(migrationsDirectoryAbsolute, { recursive: true });
|
|
3700
|
+
|
|
3701
|
+
const baseNow = Date.now();
|
|
3702
|
+
let targetPath = "";
|
|
3703
|
+
let offsetSeconds = 0;
|
|
3704
|
+
while (offsetSeconds < 86400) {
|
|
3705
|
+
const timestamp = formatMigrationTimestamp(new Date(baseNow + offsetSeconds * 1000));
|
|
3706
|
+
const fileName = `${timestamp}_${slug}${extension}`;
|
|
3707
|
+
const candidatePath = path.join(migrationsDirectoryAbsolute, fileName);
|
|
3708
|
+
if (!(await fileExists(candidatePath))) {
|
|
3709
|
+
targetPath = candidatePath;
|
|
3710
|
+
break;
|
|
3711
|
+
}
|
|
3712
|
+
offsetSeconds += 1;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
if (!targetPath) {
|
|
3716
|
+
throw createCliError(`Unable to allocate migration filename for ${packageEntry.packageId}:${migrationId}.`);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
await writeFile(targetPath, renderedSourceContent, "utf8");
|
|
3720
|
+
const relativePath = normalizeRelativePath(appRoot, targetPath);
|
|
3721
|
+
touchedFiles.add(relativePath);
|
|
3722
|
+
managedMigrations.push({
|
|
3723
|
+
id: migrationId,
|
|
3724
|
+
path: relativePath,
|
|
3725
|
+
skipped: false,
|
|
3726
|
+
reason: mutation.reason,
|
|
3727
|
+
category: mutation.category
|
|
3728
|
+
});
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
if (operation !== "copy-file") {
|
|
3733
|
+
throw createCliError(`Unsupported files mutation op \"${operation}\" in ${packageEntry.packageId}.`);
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
const from = mutation.from;
|
|
3737
|
+
const to = mutation.to;
|
|
3738
|
+
const toSurface = mutation.toSurface;
|
|
3739
|
+
if (to && toSurface) {
|
|
3740
|
+
throw createCliError(
|
|
3741
|
+
`Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
if (!from || (!to && !toSurface)) {
|
|
3745
|
+
throw createCliError(
|
|
3746
|
+
`Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
3751
|
+
if (!(await fileExists(sourcePath))) {
|
|
3752
|
+
throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
const targetPaths = toSurface
|
|
3756
|
+
? resolveSurfaceTargetPathsForMutation({
|
|
3757
|
+
appRoot,
|
|
3758
|
+
packageId: packageEntry.packageId,
|
|
3759
|
+
mutation,
|
|
3760
|
+
configContext
|
|
3761
|
+
})
|
|
3762
|
+
: [path.join(appRoot, to)];
|
|
3763
|
+
for (const targetPath of targetPaths) {
|
|
3764
|
+
const previous = await readFileBufferIfExists(targetPath);
|
|
3765
|
+
await copyTemplateFile(
|
|
3766
|
+
sourcePath,
|
|
3767
|
+
targetPath,
|
|
3768
|
+
options,
|
|
3769
|
+
packageEntry.packageId,
|
|
3770
|
+
`${mutation.id || to || from}.source`
|
|
3771
|
+
);
|
|
3772
|
+
const nextBuffer = await readFile(targetPath);
|
|
3773
|
+
|
|
3774
|
+
managedFiles.push({
|
|
3775
|
+
path: normalizeRelativePath(appRoot, targetPath),
|
|
3776
|
+
hash: hashBuffer(nextBuffer),
|
|
3777
|
+
hadPrevious: previous.exists,
|
|
3778
|
+
previousContentBase64: previous.exists ? previous.buffer.toString("base64") : "",
|
|
3779
|
+
preserveOnRemove: mutation.preserveOnRemove,
|
|
3780
|
+
reason: mutation.reason,
|
|
3781
|
+
category: mutation.category,
|
|
3782
|
+
id: mutation.id
|
|
3783
|
+
});
|
|
3784
|
+
touchedFiles.add(normalizeRelativePath(appRoot, targetPath));
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
|
|
3790
|
+
for (const mutation of textMutations) {
|
|
3791
|
+
const when = normalizeMutationWhen(mutation?.when);
|
|
3792
|
+
const configContext = when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
3793
|
+
if (
|
|
3794
|
+
!shouldApplyMutationWhen(when, {
|
|
3795
|
+
options,
|
|
3796
|
+
configContext,
|
|
3797
|
+
packageId: packageEntry.packageId,
|
|
3798
|
+
mutationContext: "text mutation"
|
|
3799
|
+
})
|
|
3800
|
+
) {
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
const operation = String(mutation?.op || "").trim();
|
|
3805
|
+
if (operation === "upsert-env") {
|
|
3806
|
+
const relativeFile = String(mutation?.file || "").trim();
|
|
3807
|
+
const key = String(mutation?.key || "").trim();
|
|
3808
|
+
if (!relativeFile || !key) {
|
|
3809
|
+
throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: "file" and "key" are required.`);
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
const absoluteFile = path.join(appRoot, relativeFile);
|
|
3813
|
+
const previous = await readFileBufferIfExists(absoluteFile);
|
|
3814
|
+
const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
|
|
3815
|
+
const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, key);
|
|
3816
|
+
const upserted = upsertEnvValue(previousContent, key, resolvedValue);
|
|
3817
|
+
|
|
3818
|
+
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
3819
|
+
await writeFile(absoluteFile, upserted.content, "utf8");
|
|
3820
|
+
|
|
3821
|
+
const recordKey = `${relativeFile}::${String(mutation?.id || key)}`;
|
|
3822
|
+
managedText[recordKey] = {
|
|
3823
|
+
file: relativeFile,
|
|
3824
|
+
op: "upsert-env",
|
|
3825
|
+
key,
|
|
3826
|
+
value: resolvedValue,
|
|
3827
|
+
hadPrevious: upserted.hadPrevious,
|
|
3828
|
+
previousValue: upserted.previousValue,
|
|
3829
|
+
reason: String(mutation?.reason || ""),
|
|
3830
|
+
category: String(mutation?.category || ""),
|
|
3831
|
+
id: String(mutation?.id || "")
|
|
3832
|
+
};
|
|
3833
|
+
touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
if (operation === "append-text") {
|
|
3838
|
+
const relativeFile = String(mutation?.file || "").trim();
|
|
3839
|
+
const snippet = String(mutation?.value || "");
|
|
3840
|
+
const position = String(mutation?.position || "bottom").trim().toLowerCase();
|
|
3841
|
+
if (!relativeFile) {
|
|
3842
|
+
throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "file" is required.`);
|
|
3843
|
+
}
|
|
3844
|
+
if (position !== "top" && position !== "bottom") {
|
|
3845
|
+
throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "position" must be "top" or "bottom".`);
|
|
3846
|
+
}
|
|
3847
|
+
await validateSettingsFieldsContractMutationTarget({
|
|
3848
|
+
appRoot,
|
|
3849
|
+
relativeFile,
|
|
3850
|
+
packageId: packageEntry.packageId
|
|
3851
|
+
});
|
|
3852
|
+
|
|
3853
|
+
const absoluteFile = path.join(appRoot, relativeFile);
|
|
3854
|
+
const previous = await readFileBufferIfExists(absoluteFile);
|
|
3855
|
+
const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
|
|
3856
|
+
const mutationId = String(mutation?.id || "").trim() || "append-text";
|
|
3857
|
+
const resolvedSnippet = interpolateOptionValue(snippet, options, packageEntry.packageId, mutationId);
|
|
3858
|
+
const skipChecks = normalizeSkipChecks(mutation?.skipIfContains)
|
|
3859
|
+
.map((entry) => interpolateOptionValue(entry, options, packageEntry.packageId, `${mutationId}.skipIfContains`))
|
|
3860
|
+
.filter((entry) => String(entry || "").trim().length > 0);
|
|
3861
|
+
|
|
3862
|
+
const shouldSkip = skipChecks.some((pattern) => previousContent.includes(String(pattern)));
|
|
3863
|
+
if (shouldSkip) {
|
|
3864
|
+
continue;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
const appended = appendTextSnippet(previousContent, resolvedSnippet, position);
|
|
3868
|
+
if (!appended.changed) {
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
3873
|
+
await writeFile(absoluteFile, appended.content, "utf8");
|
|
3874
|
+
|
|
3875
|
+
const recordKey = `${relativeFile}::${mutationId}`;
|
|
3876
|
+
managedText[recordKey] = {
|
|
3877
|
+
file: relativeFile,
|
|
3878
|
+
op: "append-text",
|
|
3879
|
+
value: resolvedSnippet,
|
|
3880
|
+
position,
|
|
3881
|
+
reason: String(mutation?.reason || ""),
|
|
3882
|
+
category: String(mutation?.category || ""),
|
|
3883
|
+
id: String(mutation?.id || "")
|
|
3884
|
+
};
|
|
3885
|
+
touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
throw createCliError(`Unsupported text mutation op "${operation}" in ${packageEntry.packageId}.`);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
function normalizeMutationRelativeFilePath(value = "") {
|
|
3894
|
+
return String(value || "")
|
|
3895
|
+
.trim()
|
|
3896
|
+
.replace(/\\/g, "/")
|
|
3897
|
+
.replace(/\/{2,}/g, "/")
|
|
3898
|
+
.replace(/^\.\/+/, "")
|
|
3899
|
+
.replace(/^\/+/, "");
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
function resolveSettingsFieldsContractTarget(relativeFile = "") {
|
|
3903
|
+
const normalizedRelativeFile = normalizeMutationRelativeFilePath(relativeFile);
|
|
3904
|
+
if (!normalizedRelativeFile) {
|
|
3905
|
+
return null;
|
|
3906
|
+
}
|
|
3907
|
+
const target = SETTINGS_FIELDS_CONTRACT_TARGETS[normalizedRelativeFile];
|
|
3908
|
+
if (!target) {
|
|
3909
|
+
return null;
|
|
3910
|
+
}
|
|
3911
|
+
return {
|
|
3912
|
+
normalizedRelativeFile,
|
|
3913
|
+
target
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
async function validateSettingsFieldsContractMutationTarget({
|
|
3918
|
+
appRoot,
|
|
3919
|
+
relativeFile,
|
|
3920
|
+
packageId
|
|
3921
|
+
} = {}) {
|
|
3922
|
+
const contractTarget = resolveSettingsFieldsContractTarget(relativeFile);
|
|
3923
|
+
if (!contractTarget) {
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
const { normalizedRelativeFile, target } = contractTarget;
|
|
3928
|
+
const absoluteFile = path.join(appRoot, normalizedRelativeFile);
|
|
3929
|
+
const existing = await readFileBufferIfExists(absoluteFile);
|
|
3930
|
+
if (!existing.exists) {
|
|
3931
|
+
throw createCliError(
|
|
3932
|
+
`Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing. ` +
|
|
3933
|
+
`Install @jskit-ai/users-core to scaffold ${target.contractId}.`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const source = existing.buffer.toString("utf8");
|
|
3938
|
+
if (!source.includes(target.marker)) {
|
|
3939
|
+
throw createCliError(
|
|
3940
|
+
`Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing contract marker "${target.marker}".`
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
for (const snippet of target.requiredSnippets) {
|
|
3944
|
+
if (source.includes(snippet)) {
|
|
3945
|
+
continue;
|
|
3946
|
+
}
|
|
3947
|
+
throw createCliError(
|
|
3948
|
+
`Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} must include "${snippet}" for ${target.contractId}.`
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
function isPositioningTextMutation(value = {}) {
|
|
3954
|
+
const mutation = ensureObject(value);
|
|
3955
|
+
const operation = String(mutation.op || "").trim();
|
|
3956
|
+
if (operation !== "append-text") {
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
return normalizeMutationRelativeFilePath(mutation.file) === "src/placement.js";
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
function resolvePositioningMutations(descriptorMutations = {}) {
|
|
3963
|
+
const mutations = ensureObject(descriptorMutations);
|
|
3964
|
+
const files = ensureArray(mutations.files).filter((mutationValue) => {
|
|
3965
|
+
const normalized = normalizeFileMutationRecord(mutationValue);
|
|
3966
|
+
return Boolean(normalized.toSurface);
|
|
3967
|
+
});
|
|
3968
|
+
const text = ensureArray(mutations.text).filter((mutationValue) => isPositioningTextMutation(mutationValue));
|
|
3969
|
+
return {
|
|
3970
|
+
files,
|
|
3971
|
+
text
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function cloneManagedMap(value = {}) {
|
|
3976
|
+
const cloned = {};
|
|
3977
|
+
for (const [key, entry] of Object.entries(ensureObject(value))) {
|
|
3978
|
+
cloned[key] = {
|
|
3979
|
+
...ensureObject(entry)
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
return cloned;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
function cloneManagedArray(value = []) {
|
|
3986
|
+
return ensureArray(value).map((entry) => ({
|
|
3987
|
+
...ensureObject(entry)
|
|
3988
|
+
}));
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
function resolveManagedSourceRecord(packageEntry, existingInstall = {}) {
|
|
3992
|
+
const existingSource = ensureObject(existingInstall.source);
|
|
3993
|
+
if (Object.keys(existingSource).length > 0) {
|
|
3994
|
+
return {
|
|
3995
|
+
...existingSource
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
const sourceRecord = {
|
|
4000
|
+
type: String(packageEntry?.sourceType || "packages-directory"),
|
|
4001
|
+
...ensureObject(packageEntry?.source)
|
|
4002
|
+
};
|
|
4003
|
+
if (!sourceRecord.descriptorPath && String(packageEntry?.descriptorRelativePath || "").trim()) {
|
|
4004
|
+
sourceRecord.descriptorPath = String(packageEntry.descriptorRelativePath).trim();
|
|
4005
|
+
}
|
|
4006
|
+
return sourceRecord;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
async function applyPackagePositioning({
|
|
4010
|
+
packageEntry,
|
|
4011
|
+
packageOptions,
|
|
4012
|
+
appRoot,
|
|
4013
|
+
lock,
|
|
4014
|
+
touchedFiles
|
|
4015
|
+
}) {
|
|
4016
|
+
const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
|
|
4017
|
+
if (Object.keys(existingInstall).length < 1) {
|
|
4018
|
+
throw createCliError(`Package is not installed: ${packageEntry.packageId}`);
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
const existingManaged = ensureObject(existingInstall.managed);
|
|
4022
|
+
const existingPackageJsonManaged = ensureObject(existingManaged.packageJson);
|
|
4023
|
+
const nextManaged = {
|
|
4024
|
+
packageJson: {
|
|
4025
|
+
dependencies: cloneManagedMap(existingPackageJsonManaged.dependencies),
|
|
4026
|
+
devDependencies: cloneManagedMap(existingPackageJsonManaged.devDependencies),
|
|
4027
|
+
scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
|
|
4028
|
+
},
|
|
4029
|
+
text: cloneManagedMap(existingManaged.text),
|
|
4030
|
+
vite: cloneManagedMap(existingManaged.vite),
|
|
4031
|
+
files: cloneManagedArray(existingManaged.files),
|
|
4032
|
+
migrations: cloneManagedArray(existingManaged.migrations)
|
|
4033
|
+
};
|
|
4034
|
+
|
|
4035
|
+
const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
|
|
4036
|
+
const packageEntryForMutations =
|
|
4037
|
+
templateRoot === packageEntry.rootDir
|
|
4038
|
+
? packageEntry
|
|
4039
|
+
: {
|
|
4040
|
+
...packageEntry,
|
|
4041
|
+
rootDir: templateRoot
|
|
4042
|
+
};
|
|
4043
|
+
|
|
4044
|
+
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
4045
|
+
const positioningMutations = resolvePositioningMutations(mutations);
|
|
4046
|
+
const appliedManagedFiles = [];
|
|
4047
|
+
const appliedManagedText = {};
|
|
4048
|
+
if (positioningMutations.files.length > 0) {
|
|
4049
|
+
await applyFileMutations(
|
|
4050
|
+
packageEntryForMutations,
|
|
4051
|
+
packageOptions,
|
|
4052
|
+
appRoot,
|
|
4053
|
+
positioningMutations.files,
|
|
4054
|
+
appliedManagedFiles,
|
|
4055
|
+
[],
|
|
4056
|
+
touchedFiles
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
if (positioningMutations.text.length > 0) {
|
|
4060
|
+
await applyTextMutations(
|
|
4061
|
+
packageEntryForMutations,
|
|
4062
|
+
appRoot,
|
|
4063
|
+
positioningMutations.text,
|
|
4064
|
+
packageOptions,
|
|
4065
|
+
appliedManagedText,
|
|
4066
|
+
touchedFiles
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
if (appliedManagedFiles.length > 0) {
|
|
4071
|
+
const replacedPaths = new Set(
|
|
4072
|
+
appliedManagedFiles
|
|
4073
|
+
.map((entry) => String(ensureObject(entry).path || "").trim())
|
|
4074
|
+
.filter(Boolean)
|
|
4075
|
+
);
|
|
4076
|
+
const retainedFiles = nextManaged.files.filter((entry) => {
|
|
4077
|
+
const managedPath = String(ensureObject(entry).path || "").trim();
|
|
4078
|
+
return !managedPath || !replacedPaths.has(managedPath);
|
|
4079
|
+
});
|
|
4080
|
+
nextManaged.files = [...retainedFiles, ...appliedManagedFiles];
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
if (Object.keys(appliedManagedText).length > 0) {
|
|
4084
|
+
nextManaged.text = {
|
|
4085
|
+
...nextManaged.text,
|
|
4086
|
+
...appliedManagedText
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
const managedRecord = {
|
|
4091
|
+
...existingInstall,
|
|
4092
|
+
packageId: packageEntry.packageId,
|
|
4093
|
+
version: packageEntry.version,
|
|
4094
|
+
source: resolveManagedSourceRecord(packageEntry, existingInstall),
|
|
4095
|
+
managed: nextManaged,
|
|
4096
|
+
options: {
|
|
4097
|
+
...ensureObject(packageOptions)
|
|
4098
|
+
},
|
|
4099
|
+
installedAt: String(existingInstall.installedAt || new Date().toISOString())
|
|
4100
|
+
};
|
|
4101
|
+
lock.installedPackages[packageEntry.packageId] = managedRecord;
|
|
4102
|
+
return managedRecord;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
async function applyPackageInstall({
|
|
4106
|
+
packageEntry,
|
|
4107
|
+
packageOptions,
|
|
4108
|
+
appRoot,
|
|
4109
|
+
appPackageJson,
|
|
4110
|
+
lock,
|
|
4111
|
+
packageRegistry,
|
|
4112
|
+
touchedFiles
|
|
4113
|
+
}) {
|
|
4114
|
+
const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
|
|
4115
|
+
const existingManaged = ensureObject(existingInstall.managed);
|
|
4116
|
+
await removeManagedViteProxyEntries({
|
|
4117
|
+
appRoot,
|
|
4118
|
+
packageId: packageEntry.packageId,
|
|
4119
|
+
managedViteChanges: ensureObject(existingManaged.vite),
|
|
4120
|
+
touchedFiles
|
|
4121
|
+
});
|
|
4122
|
+
|
|
4123
|
+
const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
|
|
4124
|
+
const cloneOnlyPackage = isCloneOnlyPackageEntry(packageEntry);
|
|
4125
|
+
const mutationWarnings = [];
|
|
4126
|
+
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
4127
|
+
const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
|
|
4128
|
+
const packageEntryForMutations =
|
|
4129
|
+
templateRoot === packageEntry.rootDir
|
|
4130
|
+
? packageEntry
|
|
4131
|
+
: {
|
|
4132
|
+
...packageEntry,
|
|
4133
|
+
rootDir: templateRoot
|
|
4134
|
+
};
|
|
4135
|
+
const mutationDependencies = ensureObject(mutations.dependencies);
|
|
4136
|
+
const runtimeDependencies = ensureObject(mutationDependencies.runtime);
|
|
4137
|
+
const devDependencies = ensureObject(mutationDependencies.dev);
|
|
4138
|
+
const mutationScripts = ensureObject(ensureObject(mutations.packageJson).scripts);
|
|
4139
|
+
|
|
4140
|
+
for (const [rawDependencyId, rawDependencyVersion] of Object.entries(runtimeDependencies)) {
|
|
4141
|
+
const dependencyId = interpolateOptionValue(
|
|
4142
|
+
rawDependencyId,
|
|
4143
|
+
packageOptions,
|
|
4144
|
+
packageEntry.packageId,
|
|
4145
|
+
`dependencies.runtime.${rawDependencyId}.id`
|
|
4146
|
+
);
|
|
4147
|
+
const dependencyVersion = interpolateOptionValue(
|
|
4148
|
+
String(rawDependencyVersion || ""),
|
|
4149
|
+
packageOptions,
|
|
4150
|
+
packageEntry.packageId,
|
|
4151
|
+
`dependencies.runtime.${rawDependencyId}.value`
|
|
4152
|
+
);
|
|
4153
|
+
if (!dependencyId) {
|
|
4154
|
+
throw createCliError(
|
|
4155
|
+
`Invalid runtime dependency key after option interpolation in ${packageEntry.packageId}: ${rawDependencyId}`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
const localPackage = packageRegistry.get(dependencyId);
|
|
4160
|
+
const existingRuntimeDependencyValue = String(ensureObject(appPackageJson.dependencies)[dependencyId] || "").trim();
|
|
4161
|
+
const resolvedValue = localPackage
|
|
4162
|
+
? resolvePackageDependencySpecifier(localPackage, { existingValue: existingRuntimeDependencyValue })
|
|
4163
|
+
: String(dependencyVersion);
|
|
4164
|
+
const applied = applyPackageJsonField(appPackageJson, "dependencies", dependencyId, resolvedValue);
|
|
4165
|
+
if (applied.changed) {
|
|
4166
|
+
managedRecord.managed.packageJson.dependencies[dependencyId] = applied.managed;
|
|
4167
|
+
touchedFiles.add("package.json");
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
for (const [rawDependencyId, rawDependencyVersion] of Object.entries(devDependencies)) {
|
|
4172
|
+
const dependencyId = interpolateOptionValue(
|
|
4173
|
+
rawDependencyId,
|
|
4174
|
+
packageOptions,
|
|
4175
|
+
packageEntry.packageId,
|
|
4176
|
+
`dependencies.dev.${rawDependencyId}.id`
|
|
4177
|
+
);
|
|
4178
|
+
const dependencyVersion = interpolateOptionValue(
|
|
4179
|
+
String(rawDependencyVersion || ""),
|
|
4180
|
+
packageOptions,
|
|
4181
|
+
packageEntry.packageId,
|
|
4182
|
+
`dependencies.dev.${rawDependencyId}.value`
|
|
4183
|
+
);
|
|
4184
|
+
if (!dependencyId) {
|
|
4185
|
+
throw createCliError(
|
|
4186
|
+
`Invalid dev dependency key after option interpolation in ${packageEntry.packageId}: ${rawDependencyId}`
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
const localPackage = packageRegistry.get(dependencyId);
|
|
4191
|
+
const existingDevDependencyValue = String(ensureObject(appPackageJson.devDependencies)[dependencyId] || "").trim();
|
|
4192
|
+
const resolvedValue = localPackage
|
|
4193
|
+
? resolvePackageDependencySpecifier(localPackage, { existingValue: existingDevDependencyValue })
|
|
4194
|
+
: String(dependencyVersion);
|
|
4195
|
+
const applied = applyPackageJsonField(appPackageJson, "devDependencies", dependencyId, resolvedValue);
|
|
4196
|
+
if (applied.changed) {
|
|
4197
|
+
managedRecord.managed.packageJson.devDependencies[dependencyId] = applied.managed;
|
|
4198
|
+
touchedFiles.add("package.json");
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
if (cloneOnlyPackage) {
|
|
4203
|
+
const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
|
|
4204
|
+
const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
|
|
4205
|
+
if (removedRuntimeDependency || removedDevDependency) {
|
|
4206
|
+
touchedFiles.add("package.json");
|
|
4207
|
+
}
|
|
4208
|
+
} else {
|
|
4209
|
+
const existingSelfDependencyValue = String(ensureObject(appPackageJson.dependencies)[packageEntry.packageId] || "").trim();
|
|
4210
|
+
const selfDependencyValue = resolvePackageDependencySpecifier(packageEntry, {
|
|
4211
|
+
existingValue: existingSelfDependencyValue
|
|
4212
|
+
});
|
|
4213
|
+
const selfApplied = applyPackageJsonField(appPackageJson, "dependencies", packageEntry.packageId, selfDependencyValue);
|
|
4214
|
+
if (selfApplied.changed) {
|
|
4215
|
+
managedRecord.managed.packageJson.dependencies[packageEntry.packageId] = selfApplied.managed;
|
|
4216
|
+
touchedFiles.add("package.json");
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
for (const [scriptName, scriptValue] of Object.entries(mutationScripts)) {
|
|
4221
|
+
const applied = applyPackageJsonField(appPackageJson, "scripts", scriptName, scriptValue);
|
|
4222
|
+
if (applied.changed) {
|
|
4223
|
+
managedRecord.managed.packageJson.scripts[scriptName] = applied.managed;
|
|
4224
|
+
touchedFiles.add("package.json");
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
await applyFileMutations(
|
|
4229
|
+
packageEntryForMutations,
|
|
4230
|
+
packageOptions,
|
|
4231
|
+
appRoot,
|
|
4232
|
+
ensureArray(mutations.files),
|
|
4233
|
+
managedRecord.managed.files,
|
|
4234
|
+
managedRecord.managed.migrations,
|
|
4235
|
+
touchedFiles,
|
|
4236
|
+
mutationWarnings
|
|
4237
|
+
);
|
|
4238
|
+
|
|
4239
|
+
await applyTextMutations(
|
|
4240
|
+
packageEntryForMutations,
|
|
4241
|
+
appRoot,
|
|
4242
|
+
ensureArray(mutations.text),
|
|
4243
|
+
packageOptions,
|
|
4244
|
+
managedRecord.managed.text,
|
|
4245
|
+
touchedFiles
|
|
4246
|
+
);
|
|
4247
|
+
|
|
4248
|
+
await applyViteMutations(
|
|
4249
|
+
packageEntryForMutations,
|
|
4250
|
+
appRoot,
|
|
4251
|
+
ensureObject(mutations.vite),
|
|
4252
|
+
packageOptions,
|
|
4253
|
+
managedRecord.managed.vite,
|
|
4254
|
+
touchedFiles
|
|
4255
|
+
);
|
|
4256
|
+
|
|
4257
|
+
if (cloneOnlyPackage) {
|
|
4258
|
+
delete lock.installedPackages[packageEntry.packageId];
|
|
4259
|
+
} else {
|
|
4260
|
+
lock.installedPackages[packageEntry.packageId] = managedRecord;
|
|
4261
|
+
}
|
|
4262
|
+
if (mutationWarnings.length > 0) {
|
|
4263
|
+
managedRecord.warnings = mutationWarnings;
|
|
4264
|
+
}
|
|
4265
|
+
return managedRecord;
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
async function adoptAppLocalPackageDependencies({
|
|
4269
|
+
appRoot,
|
|
4270
|
+
appPackageJson,
|
|
4271
|
+
lock
|
|
4272
|
+
}) {
|
|
4273
|
+
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
4274
|
+
const runtimeDependencies = ensureObject(appPackageJson.dependencies);
|
|
4275
|
+
const adoptedPackageIds = [];
|
|
4276
|
+
|
|
4277
|
+
for (const dependencyId of sortStrings(Object.keys(runtimeDependencies))) {
|
|
4278
|
+
if (lock.installedPackages[dependencyId]) {
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
const localPackageEntry = appLocalRegistry.get(dependencyId);
|
|
4283
|
+
if (!localPackageEntry) {
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
lock.installedPackages[dependencyId] = createManagedRecordBase(localPackageEntry, {});
|
|
4288
|
+
adoptedPackageIds.push(dependencyId);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
return {
|
|
4292
|
+
appLocalRegistry,
|
|
4293
|
+
adoptedPackageIds: sortStrings(adoptedPackageIds)
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
const commandHandlers = createCommandHandlers(
|
|
4298
|
+
createCommandHandlerDeps({
|
|
4299
|
+
createCliError,
|
|
4300
|
+
createColorFormatter,
|
|
4301
|
+
resolveWrapWidth,
|
|
4302
|
+
writeWrappedItems,
|
|
4303
|
+
normalizeRelativePath,
|
|
4304
|
+
normalizeRelativePosixPath,
|
|
4305
|
+
resolveAppRootFromCwd,
|
|
4306
|
+
loadLockFile,
|
|
4307
|
+
loadPackageRegistry,
|
|
4308
|
+
loadBundleRegistry,
|
|
4309
|
+
loadAppLocalPackageRegistry,
|
|
4310
|
+
mergePackageRegistries,
|
|
4311
|
+
resolvePackageIdInput,
|
|
4312
|
+
resolveInstalledPackageIdInput,
|
|
4313
|
+
resolveInstalledNodeModulePackageEntry,
|
|
4314
|
+
hydratePackageRegistryFromInstalledNodeModules,
|
|
4315
|
+
validateInlineOptionsForPackage,
|
|
4316
|
+
resolveLocalDependencyOrder,
|
|
4317
|
+
validatePlannedCapabilityClosure,
|
|
4318
|
+
resolvePackageOptions,
|
|
4319
|
+
applyPackageInstall,
|
|
4320
|
+
applyPackagePositioning,
|
|
4321
|
+
adoptAppLocalPackageDependencies,
|
|
4322
|
+
loadAppPackageJson,
|
|
4323
|
+
resolveLocalPackageId,
|
|
4324
|
+
createLocalPackageScaffoldFiles,
|
|
4325
|
+
fileExists,
|
|
4326
|
+
applyPackageJsonField,
|
|
4327
|
+
toFileDependencySpecifier,
|
|
4328
|
+
writeJsonFile,
|
|
4329
|
+
writeFile,
|
|
4330
|
+
mkdir,
|
|
4331
|
+
path,
|
|
4332
|
+
inspectPackageOfferings,
|
|
4333
|
+
buildFileWriteGroups,
|
|
4334
|
+
listDeclaredCapabilities,
|
|
4335
|
+
buildCapabilityDetailsForPackage,
|
|
4336
|
+
formatPackageSubpathImport,
|
|
4337
|
+
normalizePlacementOutlets,
|
|
4338
|
+
normalizePlacementContributions,
|
|
4339
|
+
shouldShowPackageExportTarget,
|
|
4340
|
+
classifyExportedSymbols,
|
|
4341
|
+
deriveProviderDisplayName,
|
|
4342
|
+
restorePackageJsonField,
|
|
4343
|
+
readFileBufferIfExists,
|
|
4344
|
+
removeEnvValue,
|
|
4345
|
+
removeManagedViteProxyEntries,
|
|
4346
|
+
hashBuffer,
|
|
4347
|
+
rm
|
|
4348
|
+
})
|
|
4349
|
+
);
|
|
4350
|
+
|
|
4351
|
+
const runCli = createRunCli({
|
|
4352
|
+
parseArgs,
|
|
4353
|
+
printUsage,
|
|
4354
|
+
commandHandlers,
|
|
4355
|
+
cleanupMaterializedPackageRoots,
|
|
4356
|
+
createCliError
|
|
4357
|
+
});
|
|
4358
|
+
|
|
4359
|
+
export { runCli };
|