@smallpen/local-package 0.1.0-alpha.1
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/assets/fonts/SourceSansPro-Bold.ttf +0 -0
- package/assets/fonts/SourceSansPro-Regular.ttf +0 -0
- package/assets/fonts/SourceSansPro-Semibold.ttf +0 -0
- package/assets/fonts/WorkSans-Regular.ttf +0 -0
- package/package.json +23 -0
- package/src/draft-import.mjs +282 -0
- package/src/font-convert.mjs +132 -0
- package/src/index.mjs +34 -0
- package/src/initialize.mjs +533 -0
- package/src/kiwi-runtime.mjs +526 -0
- package/src/local-package-backend.mjs +460 -0
- package/src/local-package.mjs +842 -0
- package/src/local-workspace-session.mjs +486 -0
- package/src/media-inspect.mjs +184 -0
- package/src/remote-library.mjs +301 -0
- package/src/render.mjs +3605 -0
- package/src/webp-decode.mjs +73 -0
- package/src/workspace.mjs +591 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { SmallPenError } from "@smallpen/core";
|
|
7
|
+
|
|
8
|
+
// WebP static decoding via Google's libwebp compiled to WebAssembly
|
|
9
|
+
// (@jsquash/webp, BSD-3). The emscripten codec is pre-loaded with its .wasm
|
|
10
|
+
// bytes read from disk so decoding also works in the Node CLI (SP-031).
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const require_ = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
let decoderModule;
|
|
15
|
+
let decoderInitPromise;
|
|
16
|
+
|
|
17
|
+
async function loadDecoder() {
|
|
18
|
+
if (decoderModule) return decoderModule;
|
|
19
|
+
decoderInitPromise ??= (async () => {
|
|
20
|
+
const { default: webpDec } = await import(
|
|
21
|
+
"@jsquash/webp/codec/dec/webp_dec.js"
|
|
22
|
+
);
|
|
23
|
+
// Resolve the wasm via Node module resolution (works in workspace and
|
|
24
|
+
// packaged CLI layouts).
|
|
25
|
+
const wasmPath = require_.resolve("@jsquash/webp/codec/dec/webp_dec.wasm");
|
|
26
|
+
const wasmBinary = await readFile(wasmPath);
|
|
27
|
+
decoderModule = await webpDec({ wasmBinary, noInitialRun: true });
|
|
28
|
+
return decoderModule;
|
|
29
|
+
})();
|
|
30
|
+
return decoderInitPromise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function initWebpDecoder() {
|
|
34
|
+
await loadDecoder();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Synchronous decode of a complete WebP image to RGBA pixels. Requires a
|
|
38
|
+
// prior initWebpDecoder(); the renderer initializes it once per projection.
|
|
39
|
+
export function decodeWebpSync(bytes, diagnostics, path) {
|
|
40
|
+
let module;
|
|
41
|
+
try {
|
|
42
|
+
module = decoderModule;
|
|
43
|
+
} catch {
|
|
44
|
+
module = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!module) {
|
|
47
|
+
throw new SmallPenError(
|
|
48
|
+
"media_decode_failed",
|
|
49
|
+
"WebP decoder is not initialized",
|
|
50
|
+
{ path },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = module.decode(
|
|
55
|
+
bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes),
|
|
56
|
+
);
|
|
57
|
+
if (!result) throw new Error("decoder returned no image");
|
|
58
|
+
return {
|
|
59
|
+
width: result.width,
|
|
60
|
+
height: result.height,
|
|
61
|
+
pixels: new Uint8ClampedArray(result.data),
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
diagnostics?.push({
|
|
65
|
+
code: "media_decode_failed",
|
|
66
|
+
message: `Renderer could not decode WebP image: ${
|
|
67
|
+
error instanceof Error ? error.message : String(error)
|
|
68
|
+
}`,
|
|
69
|
+
path,
|
|
70
|
+
});
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
combineContextAxes,
|
|
6
|
+
listEffectiveTokens,
|
|
7
|
+
projectScenario,
|
|
8
|
+
projectScreen,
|
|
9
|
+
SmallPenError,
|
|
10
|
+
} from "@smallpen/core";
|
|
11
|
+
|
|
12
|
+
import { openPackage } from "./local-package.mjs";
|
|
13
|
+
import { openRemoteLibrary } from "./remote-library.mjs";
|
|
14
|
+
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
20
|
+
const child = relative(rootPath, candidatePath);
|
|
21
|
+
return child === "" || (!child.startsWith("..") && !isAbsolute(child));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function chooseFoundation(dependency) {
|
|
25
|
+
return {
|
|
26
|
+
action: "choose-foundation",
|
|
27
|
+
currentPath: dependency.path,
|
|
28
|
+
expectedPackageId: dependency.packageId,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function conflict(code, message, path, choices) {
|
|
33
|
+
return { choices, code, message, path, severity: "error" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function warning(code, message, path) {
|
|
37
|
+
return { code, message, path, severity: "warning" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function repair(product, conflicts, foundation, libraries = [], warnings = []) {
|
|
41
|
+
return {
|
|
42
|
+
conflicts,
|
|
43
|
+
...(foundation ? { foundation } : {}),
|
|
44
|
+
libraries,
|
|
45
|
+
product,
|
|
46
|
+
status: "repair",
|
|
47
|
+
warnings,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function recreatedAssetKind(reference) {
|
|
52
|
+
if (reference.assetId.startsWith("tok_")) return "token";
|
|
53
|
+
if (reference.assetId.startsWith("cmp_")) return "component";
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dependentUsagePath(path) {
|
|
58
|
+
for (const pattern of [
|
|
59
|
+
/^(.*\.presentations\[\d+\]\.nodes\.[^.]+)\.instance\.component$/,
|
|
60
|
+
/^(.*\.scenarios\[\d+\])\.target\.(?:component|screen)$/,
|
|
61
|
+
/^(.*\.requirements\[\d+\]\.links\[\d+\])(?:\.screen)?$/,
|
|
62
|
+
/^(.*\.annotations\[\d+\])\.target(?:\.screen)?$/,
|
|
63
|
+
]) {
|
|
64
|
+
const match = pattern.exec(path);
|
|
65
|
+
if (match) return match[1];
|
|
66
|
+
}
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function referenceRepairChoices(path, reference) {
|
|
71
|
+
const assetKind = recreatedAssetKind(reference);
|
|
72
|
+
return [
|
|
73
|
+
{ action: "retarget-reference", reference, referencePath: path },
|
|
74
|
+
...(assetKind
|
|
75
|
+
? [
|
|
76
|
+
{
|
|
77
|
+
action: "recreate-product-asset",
|
|
78
|
+
assetKind,
|
|
79
|
+
reference,
|
|
80
|
+
referencePath: path,
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
: []),
|
|
84
|
+
{
|
|
85
|
+
action: "remove-dependent-usage",
|
|
86
|
+
referencePath: dependentUsagePath(path),
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function externalReferences(product) {
|
|
92
|
+
const result = [];
|
|
93
|
+
const visit = (value, path) => {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
value.forEach((child, index) => visit(child, `${path}[${index}]`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(value)) return;
|
|
99
|
+
if (
|
|
100
|
+
typeof value.packageId === "string" &&
|
|
101
|
+
typeof value.assetId === "string"
|
|
102
|
+
) {
|
|
103
|
+
if (value.packageId !== product.manifest.packageId) {
|
|
104
|
+
result.push({ path, reference: structuredClone(value) });
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const [field, child] of Object.entries(value)) {
|
|
109
|
+
visit(child, path ? `${path}.${field}` : field);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const entry of Object.keys(product.entries).sort()) {
|
|
113
|
+
visit(product.entries[entry], entry);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function packageAssets(foundation) {
|
|
119
|
+
const assets = new Map();
|
|
120
|
+
const add = (id, value, kind) => {
|
|
121
|
+
if (typeof id !== "string") return;
|
|
122
|
+
assets.set(id, {
|
|
123
|
+
kind,
|
|
124
|
+
visibility:
|
|
125
|
+
value?.visibility ??
|
|
126
|
+
value?.$extensions?.smallpen?.visibility ??
|
|
127
|
+
"public",
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const visitDtcg = (value) => {
|
|
131
|
+
if (!isRecord(value)) return;
|
|
132
|
+
add(value.$extensions?.smallpen?.id, value, "token");
|
|
133
|
+
for (const child of Object.values(value)) visitDtcg(child);
|
|
134
|
+
};
|
|
135
|
+
for (const entry of foundation.manifest.entries.tokens) {
|
|
136
|
+
const value = foundation.entries[entry];
|
|
137
|
+
if (Array.isArray(value?.sets)) {
|
|
138
|
+
for (const set of value.sets) {
|
|
139
|
+
for (const token of set.tokens ?? []) add(token.id, token, "token");
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
visitDtcg(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const entry of foundation.manifest.entries.components) {
|
|
146
|
+
const value = foundation.entries[entry];
|
|
147
|
+
add(value?.id, value, "component");
|
|
148
|
+
for (const componentSet of value?.componentSets ?? []) {
|
|
149
|
+
add(componentSet.id, componentSet, "component");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const entry of foundation.manifest.entries.assets) {
|
|
153
|
+
const library = foundation.entries[entry];
|
|
154
|
+
for (const field of ["colors", "fonts", "media", "typographies"]) {
|
|
155
|
+
for (const asset of library?.[field] ?? []) add(asset.id, asset, field);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const entry of foundation.manifest.entries.screens) {
|
|
159
|
+
add(foundation.entries[entry]?.id, foundation.entries[entry], "screen");
|
|
160
|
+
}
|
|
161
|
+
return assets;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateExternalReferences(product, libraries) {
|
|
165
|
+
const packages = new Map(
|
|
166
|
+
libraries.map((library) => [
|
|
167
|
+
library.manifest.packageId,
|
|
168
|
+
{ assets: packageAssets(library), library },
|
|
169
|
+
]),
|
|
170
|
+
);
|
|
171
|
+
const conflicts = [];
|
|
172
|
+
for (const usage of externalReferences(product)) {
|
|
173
|
+
const choices = referenceRepairChoices(usage.path, usage.reference);
|
|
174
|
+
const target = packages.get(usage.reference.packageId);
|
|
175
|
+
if (!target) {
|
|
176
|
+
conflicts.push({
|
|
177
|
+
...conflict(
|
|
178
|
+
"undeclared_package_reference",
|
|
179
|
+
`Asset references undeclared Library ${usage.reference.packageId}`,
|
|
180
|
+
usage.path,
|
|
181
|
+
choices,
|
|
182
|
+
),
|
|
183
|
+
reference: usage.reference,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const asset = target.assets.get(usage.reference.assetId);
|
|
188
|
+
const foundation = target.library.manifest.role === "foundation";
|
|
189
|
+
if (!asset) {
|
|
190
|
+
conflicts.push({
|
|
191
|
+
...conflict(
|
|
192
|
+
foundation ? "missing_foundation_asset" : "missing_library_asset",
|
|
193
|
+
`${foundation ? "Foundation" : "Library"} asset is missing: ${usage.reference.assetId}`,
|
|
194
|
+
usage.path,
|
|
195
|
+
choices,
|
|
196
|
+
),
|
|
197
|
+
reference: usage.reference,
|
|
198
|
+
});
|
|
199
|
+
} else if (asset.visibility === "private") {
|
|
200
|
+
conflicts.push({
|
|
201
|
+
...conflict(
|
|
202
|
+
foundation ? "private_foundation_asset" : "private_library_asset",
|
|
203
|
+
`Product cannot reference private ${foundation ? "Foundation" : "Library"} asset: ${usage.reference.assetId}`,
|
|
204
|
+
usage.path,
|
|
205
|
+
choices,
|
|
206
|
+
),
|
|
207
|
+
reference: usage.reference,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return conflicts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function contextSelections(product, foundation) {
|
|
215
|
+
const axes = [...combineContextAxes(product, foundation).values()].sort(
|
|
216
|
+
(left, right) => left.id.localeCompare(right.id),
|
|
217
|
+
);
|
|
218
|
+
let selections = [{}];
|
|
219
|
+
for (const axis of axes) {
|
|
220
|
+
selections = selections.flatMap((selection) =>
|
|
221
|
+
axis.values.map(({ id }) => ({ ...selection, [axis.id]: id })),
|
|
222
|
+
);
|
|
223
|
+
if (selections.length > 4096) {
|
|
224
|
+
throw new SmallPenError(
|
|
225
|
+
"context_matrix_too_large",
|
|
226
|
+
"Workspace Context matrix exceeds 4096 combinations",
|
|
227
|
+
{ count: selections.length },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return selections;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const DEPENDENCY_PROJECTION_CODES = new Set([
|
|
235
|
+
"ambiguous_token_context_rule",
|
|
236
|
+
"binding_type_mismatch",
|
|
237
|
+
"duplicate_product_component_replacement",
|
|
238
|
+
"duplicate_product_token_override",
|
|
239
|
+
"duplicate_workspace_context_axis",
|
|
240
|
+
"invalid_scenario_context",
|
|
241
|
+
"invalid_token_context_rule",
|
|
242
|
+
"missing_component",
|
|
243
|
+
"missing_token",
|
|
244
|
+
"missing_variant",
|
|
245
|
+
"product_token_override_type_mismatch",
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
function validateWorkspaceProjections(product, foundation, libraries = []) {
|
|
249
|
+
try {
|
|
250
|
+
for (const context of contextSelections(product, foundation)) {
|
|
251
|
+
listEffectiveTokens(product, { context, foundation, libraries });
|
|
252
|
+
}
|
|
253
|
+
for (const entry of product.manifest.entries.screens) {
|
|
254
|
+
const screen = product.entries[entry];
|
|
255
|
+
for (const presentation of screen.presentations) {
|
|
256
|
+
projectScreen(product, screen.id, {
|
|
257
|
+
foundation,
|
|
258
|
+
libraries,
|
|
259
|
+
presentationId: presentation.id,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const scenario of product.domain.scenarios.values()) {
|
|
264
|
+
projectScenario(product, scenario.id, { foundation, libraries });
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (
|
|
269
|
+
!(error instanceof SmallPenError) ||
|
|
270
|
+
!DEPENDENCY_PROJECTION_CODES.has(error.code)
|
|
271
|
+
) {
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
let path = error.details.path;
|
|
275
|
+
let reference = error.details.reference;
|
|
276
|
+
if (!path && error.details.instanceId) {
|
|
277
|
+
const suffix = `.nodes.${error.details.instanceId}.instance.component`;
|
|
278
|
+
const usage = externalReferences(product).find((candidate) =>
|
|
279
|
+
candidate.path.endsWith(suffix),
|
|
280
|
+
);
|
|
281
|
+
path = usage?.path;
|
|
282
|
+
reference = usage?.reference;
|
|
283
|
+
}
|
|
284
|
+
if (!path && error.details.scenarioId) {
|
|
285
|
+
for (const entry of product.manifest.entries.scenarios) {
|
|
286
|
+
const index = product.entries[entry].scenarios.findIndex(
|
|
287
|
+
({ id }) => id === error.details.scenarioId,
|
|
288
|
+
);
|
|
289
|
+
if (index >= 0) {
|
|
290
|
+
path = `${entry}.scenarios[${index}]`;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
path ??= "workspace.projection";
|
|
296
|
+
const choices = reference
|
|
297
|
+
? referenceRepairChoices(path, reference)
|
|
298
|
+
: [{ action: "remove-dependent-usage", referencePath: path }];
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
...conflict(error.code, error.message, path, choices),
|
|
302
|
+
...(reference ? { reference } : {}),
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function chooseLibrarySource(library) {
|
|
309
|
+
return {
|
|
310
|
+
action: "choose-library-source",
|
|
311
|
+
expectedPackageId: library.packageId,
|
|
312
|
+
source: structuredClone(library.source),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
317
|
+
const conflicts = [];
|
|
318
|
+
const records = new Map();
|
|
319
|
+
const warnings = [];
|
|
320
|
+
|
|
321
|
+
const visit = async (owner, library, index, depth, stack) => {
|
|
322
|
+
const path = `manifest.json.libraries[${index}]`;
|
|
323
|
+
const choice = chooseLibrarySource(library);
|
|
324
|
+
if (stack.has(library.packageId)) {
|
|
325
|
+
conflicts.push(
|
|
326
|
+
conflict(
|
|
327
|
+
"circular_library_reference",
|
|
328
|
+
`Library cycle includes ${library.packageId}`,
|
|
329
|
+
path,
|
|
330
|
+
[choice],
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let snapshot;
|
|
336
|
+
try {
|
|
337
|
+
if (library.source.type === "url") {
|
|
338
|
+
snapshot = await openRemoteLibrary(library.source.url, {
|
|
339
|
+
cacheRoot: options.libraryCacheRoot,
|
|
340
|
+
expectedPackageId: library.packageId,
|
|
341
|
+
fetchImpl: options.fetchImpl,
|
|
342
|
+
refresh: options.refreshRemoteLibraries === true,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
if (owner.remote) {
|
|
346
|
+
throw new SmallPenError(
|
|
347
|
+
"remote_library_local_dependency",
|
|
348
|
+
"A Remote Library cannot resolve a machine-local child Library",
|
|
349
|
+
{ packageId: library.packageId },
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const candidate = resolve(dirname(owner.locator), library.source.path);
|
|
353
|
+
const locator = await realpath(candidate);
|
|
354
|
+
snapshot = await openPackage(locator);
|
|
355
|
+
if (!isWithinRoot(workspaceRoot, locator)) {
|
|
356
|
+
warnings.push(
|
|
357
|
+
warning(
|
|
358
|
+
"external_library_path",
|
|
359
|
+
`Library ${snapshot.manifest.name} is outside the Workspace directory and may be harder to move or share`,
|
|
360
|
+
`${path}.source.path`,
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
conflicts.push(
|
|
367
|
+
conflict(
|
|
368
|
+
"library_unavailable",
|
|
369
|
+
`Library ${library.packageId} cannot be opened: ${error.message}`,
|
|
370
|
+
`${path}.source`,
|
|
371
|
+
[choice],
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (snapshot.manifest.packageId !== library.packageId) {
|
|
377
|
+
conflicts.push(
|
|
378
|
+
conflict(
|
|
379
|
+
"library_id_mismatch",
|
|
380
|
+
`Library Package id ${snapshot.manifest.packageId} does not match expected ${library.packageId}`,
|
|
381
|
+
`${path}.packageId`,
|
|
382
|
+
[choice],
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const existing = records.get(library.packageId);
|
|
388
|
+
if (existing && existing.snapshot.revision !== snapshot.revision) {
|
|
389
|
+
conflicts.push(
|
|
390
|
+
conflict(
|
|
391
|
+
"ambiguous_library_source",
|
|
392
|
+
`Library ${library.packageId} resolves to more than one revision`,
|
|
393
|
+
path,
|
|
394
|
+
[choice],
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const record = existing ?? {
|
|
400
|
+
direct: depth === 0,
|
|
401
|
+
snapshot,
|
|
402
|
+
source: structuredClone(library.source),
|
|
403
|
+
};
|
|
404
|
+
record.direct ||= depth === 0;
|
|
405
|
+
records.set(library.packageId, record);
|
|
406
|
+
if (snapshot.remote?.cache === "stale") {
|
|
407
|
+
warnings.push(
|
|
408
|
+
warning(
|
|
409
|
+
"remote_library_offline",
|
|
410
|
+
`Library ${snapshot.manifest.name} is using its last verified cache: ${snapshot.remote.warning}`,
|
|
411
|
+
`${path}.source.url`,
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (existing) return;
|
|
416
|
+
const nextStack = new Set(stack);
|
|
417
|
+
nextStack.add(library.packageId);
|
|
418
|
+
for (const [childIndex, child] of (
|
|
419
|
+
snapshot.manifest.libraries ?? []
|
|
420
|
+
).entries()) {
|
|
421
|
+
await visit(snapshot, child, childIndex, depth + 1, nextStack);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const rootIds = new Set(owners.map(({ manifest }) => manifest.packageId));
|
|
426
|
+
for (const owner of owners) {
|
|
427
|
+
for (const [index, library] of (owner.manifest.libraries ?? []).entries()) {
|
|
428
|
+
await visit(owner, library, index, 0, rootIds);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
conflicts,
|
|
433
|
+
libraries: [...records.values()].map(({ snapshot }) => snapshot),
|
|
434
|
+
librarySources: [...records.values()].map((record) => ({
|
|
435
|
+
...(record.snapshot.remote?.cache
|
|
436
|
+
? { cache: record.snapshot.remote.cache }
|
|
437
|
+
: {}),
|
|
438
|
+
direct: record.direct,
|
|
439
|
+
fileId: record.snapshot.runtime.file,
|
|
440
|
+
name: record.snapshot.manifest.name,
|
|
441
|
+
packageId: record.snapshot.manifest.packageId,
|
|
442
|
+
revision: record.snapshot.revision,
|
|
443
|
+
role: record.snapshot.manifest.role,
|
|
444
|
+
source: record.source,
|
|
445
|
+
})),
|
|
446
|
+
warnings,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export async function resolveWorkspace(productLocator, options = {}) {
|
|
451
|
+
const product = await openPackage(productLocator);
|
|
452
|
+
const workspaceRoot = dirname(product.locator);
|
|
453
|
+
if (product.manifest.role === "foundation") {
|
|
454
|
+
const resolved = await resolveDeclaredLibraries(
|
|
455
|
+
[product],
|
|
456
|
+
options,
|
|
457
|
+
workspaceRoot,
|
|
458
|
+
);
|
|
459
|
+
const referenceConflicts =
|
|
460
|
+
resolved.conflicts.length === 0
|
|
461
|
+
? validateExternalReferences(product, resolved.libraries)
|
|
462
|
+
: [];
|
|
463
|
+
const conflicts = [...resolved.conflicts, ...referenceConflicts];
|
|
464
|
+
if (conflicts.length > 0) {
|
|
465
|
+
return repair(
|
|
466
|
+
product,
|
|
467
|
+
conflicts,
|
|
468
|
+
undefined,
|
|
469
|
+
resolved.libraries,
|
|
470
|
+
resolved.warnings,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
status: "ready",
|
|
475
|
+
warnings: resolved.warnings,
|
|
476
|
+
workspace: {
|
|
477
|
+
libraries: resolved.libraries,
|
|
478
|
+
librarySources: resolved.librarySources,
|
|
479
|
+
product,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const dependency = product.manifest.dependencies[0];
|
|
484
|
+
const choice = chooseFoundation(dependency);
|
|
485
|
+
const candidatePath = resolve(workspaceRoot, dependency.path);
|
|
486
|
+
let foundationPath;
|
|
487
|
+
try {
|
|
488
|
+
foundationPath = await realpath(candidatePath);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return repair(product, [
|
|
491
|
+
conflict(
|
|
492
|
+
"foundation_unavailable",
|
|
493
|
+
`Foundation cannot be opened at ${dependency.path}: ${error.message}`,
|
|
494
|
+
"manifest.json.dependencies[0].path",
|
|
495
|
+
[choice],
|
|
496
|
+
),
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
499
|
+
let foundation;
|
|
500
|
+
try {
|
|
501
|
+
foundation = await openPackage(foundationPath);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return repair(product, [
|
|
504
|
+
conflict(
|
|
505
|
+
"foundation_invalid",
|
|
506
|
+
`Foundation Package is invalid: ${error.message}`,
|
|
507
|
+
dependency.path,
|
|
508
|
+
[choice],
|
|
509
|
+
),
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
const conflicts = [];
|
|
513
|
+
const warnings = [];
|
|
514
|
+
if (!isWithinRoot(workspaceRoot, foundationPath)) {
|
|
515
|
+
warnings.push(
|
|
516
|
+
warning(
|
|
517
|
+
"external_foundation_path",
|
|
518
|
+
"Foundation is outside the Workspace directory and may be harder to move or share",
|
|
519
|
+
"manifest.json.dependencies[0].path",
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
if (foundation.manifest.role !== "foundation") {
|
|
524
|
+
conflicts.push(
|
|
525
|
+
conflict(
|
|
526
|
+
"dependency_not_foundation",
|
|
527
|
+
"Product dependency must have the Foundation role",
|
|
528
|
+
"manifest.json.dependencies[0].path",
|
|
529
|
+
[choice],
|
|
530
|
+
),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (foundation.manifest.packageId !== dependency.packageId) {
|
|
534
|
+
conflicts.push(
|
|
535
|
+
conflict(
|
|
536
|
+
"dependency_id_mismatch",
|
|
537
|
+
`Foundation Package id ${foundation.manifest.packageId} does not match expected ${dependency.packageId}`,
|
|
538
|
+
"manifest.json.dependencies[0].packageId",
|
|
539
|
+
[choice],
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const resolved =
|
|
544
|
+
conflicts.length === 0
|
|
545
|
+
? await resolveDeclaredLibraries(
|
|
546
|
+
[product, foundation],
|
|
547
|
+
options,
|
|
548
|
+
workspaceRoot,
|
|
549
|
+
)
|
|
550
|
+
: { conflicts: [], libraries: [], librarySources: [], warnings: [] };
|
|
551
|
+
conflicts.push(...resolved.conflicts);
|
|
552
|
+
warnings.push(...resolved.warnings);
|
|
553
|
+
if (conflicts.length === 0) {
|
|
554
|
+
conflicts.push(
|
|
555
|
+
...validateExternalReferences(product, [
|
|
556
|
+
foundation,
|
|
557
|
+
...resolved.libraries,
|
|
558
|
+
]),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (conflicts.length === 0) {
|
|
562
|
+
conflicts.push(
|
|
563
|
+
...validateWorkspaceProjections(product, foundation, resolved.libraries),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
if (conflicts.length > 0) {
|
|
567
|
+
return repair(product, conflicts, foundation, resolved.libraries, warnings);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
status: "ready",
|
|
571
|
+
warnings,
|
|
572
|
+
workspace: {
|
|
573
|
+
foundation,
|
|
574
|
+
libraries: resolved.libraries,
|
|
575
|
+
librarySources: resolved.librarySources,
|
|
576
|
+
product,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function openWorkspace(productLocator, options = {}) {
|
|
582
|
+
const resolution = await resolveWorkspace(productLocator, options);
|
|
583
|
+
if (resolution.status === "repair") {
|
|
584
|
+
throw new SmallPenError(
|
|
585
|
+
"repair_required",
|
|
586
|
+
"SmallPen workspace requires dependency Repair",
|
|
587
|
+
{ conflicts: resolution.conflicts },
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return resolution.workspace;
|
|
591
|
+
}
|