@smallpen/local-package 0.1.0-alpha.1 → 0.1.0-alpha.11
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-OFL.txt +46 -0
- package/assets/fonts/WorkSans-OFL.txt +46 -0
- package/package.json +4 -4
- package/runtime/fonts/OFL.txt +46 -0
- package/runtime/fonts/sourcesanspro-black.ttf +0 -0
- package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-light.ttf +0 -0
- package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
- package/runtime/manifest.json +7 -0
- package/runtime/render-wasm.js +15 -0
- package/runtime/render-wasm.wasm +0 -0
- package/runtime/renderer.mjs +169652 -0
- package/src/draft-import.mjs +1 -11
- package/src/font-convert.mjs +23 -1
- package/src/fs-utils.mjs +95 -0
- package/src/index.mjs +19 -3
- package/src/initialize.mjs +363 -136
- package/src/kiwi-runtime.mjs +38 -36
- package/src/library-network.mjs +189 -0
- package/src/local-package-backend.mjs +215 -24
- package/src/local-package.mjs +589 -177
- package/src/local-workspace-session.mjs +278 -96
- package/src/migrate-themes.mjs +116 -0
- package/src/remote-library.mjs +167 -28
- package/src/render-fonts.mjs +172 -0
- package/src/render-shared.mjs +346 -0
- package/src/render.mjs +71 -3557
- package/src/workspace.mjs +178 -17
- package/src/webp-decode.mjs +0 -73
package/src/workspace.mjs
CHANGED
|
@@ -1,24 +1,73 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute,
|
|
2
|
+
import { dirname, isAbsolute, posix, resolve, win32 } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
closestComponentVariant,
|
|
5
6
|
combineContextAxes,
|
|
7
|
+
findComponentVariant,
|
|
6
8
|
listEffectiveTokens,
|
|
7
9
|
projectScenario,
|
|
8
10
|
projectScreen,
|
|
9
11
|
SmallPenError,
|
|
10
12
|
} from "@smallpen/core";
|
|
11
13
|
|
|
14
|
+
import { isWithinRoot } from "./fs-utils.mjs";
|
|
12
15
|
import { openPackage } from "./local-package.mjs";
|
|
13
|
-
import { openRemoteLibrary } from "./remote-library.mjs";
|
|
16
|
+
import { openRemoteLibrary, remoteManifestUrl } from "./remote-library.mjs";
|
|
14
17
|
|
|
15
18
|
function isRecord(value) {
|
|
16
19
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// Local Library paths are relative to the declaring Package's directory, like
|
|
23
|
+
// Foundation dependency paths. Unlike dependencies they may climb out of the
|
|
24
|
+
// Workspace ("../libraries/x.smallpen" is the documented form and resolves
|
|
25
|
+
// with an external_library_path warning), but absolute, drive-letter, UNC,
|
|
26
|
+
// and backslash paths are machine-specific and are rejected.
|
|
27
|
+
export function localLibraryLocator(ownerLocator, source) {
|
|
28
|
+
const path = source?.path;
|
|
29
|
+
if (
|
|
30
|
+
typeof path !== "string" ||
|
|
31
|
+
path.length === 0 ||
|
|
32
|
+
path.includes("\0") ||
|
|
33
|
+
path.includes("\\") ||
|
|
34
|
+
isAbsolute(path) ||
|
|
35
|
+
posix.isAbsolute(path) ||
|
|
36
|
+
win32.isAbsolute(path)
|
|
37
|
+
) {
|
|
38
|
+
throw new SmallPenError(
|
|
39
|
+
"invalid_library_source",
|
|
40
|
+
"Local Library path must be relative to the declaring Package",
|
|
41
|
+
{ path },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return resolve(dirname(ownerLocator), path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `remoteLibrarySnapshots` (a Map keyed by manifest URL) lets a long-lived
|
|
48
|
+
// caller reuse remote snapshots it already verified; with
|
|
49
|
+
// `reuseRemoteLibrarySnapshots` the network and disk cache are skipped.
|
|
50
|
+
async function remoteLibrarySnapshot(library, options) {
|
|
51
|
+
const memo =
|
|
52
|
+
options.remoteLibrarySnapshots instanceof Map
|
|
53
|
+
? options.remoteLibrarySnapshots
|
|
54
|
+
: undefined;
|
|
55
|
+
const key = remoteManifestUrl(library.source.url);
|
|
56
|
+
if (
|
|
57
|
+
memo?.has(key) &&
|
|
58
|
+
options.reuseRemoteLibrarySnapshots === true &&
|
|
59
|
+
options.refreshRemoteLibraries !== true
|
|
60
|
+
) {
|
|
61
|
+
return memo.get(key);
|
|
62
|
+
}
|
|
63
|
+
const snapshot = await openRemoteLibrary(library.source.url, {
|
|
64
|
+
cacheRoot: options.libraryCacheRoot,
|
|
65
|
+
expectedPackageId: library.packageId,
|
|
66
|
+
fetchImpl: options.fetchImpl,
|
|
67
|
+
refresh: options.refreshRemoteLibraries === true,
|
|
68
|
+
});
|
|
69
|
+
memo?.set(key, snapshot);
|
|
70
|
+
return snapshot;
|
|
22
71
|
}
|
|
23
72
|
|
|
24
73
|
function chooseFoundation(dependency) {
|
|
@@ -213,7 +262,8 @@ function validateExternalReferences(product, libraries) {
|
|
|
213
262
|
|
|
214
263
|
function contextSelections(product, foundation) {
|
|
215
264
|
const axes = [...combineContextAxes(product, foundation).values()].sort(
|
|
216
|
-
|
|
265
|
+
// Code-unit order: localeCompare would follow the host LANG.
|
|
266
|
+
(left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
|
|
217
267
|
);
|
|
218
268
|
let selections = [{}];
|
|
219
269
|
for (const axis of axes) {
|
|
@@ -246,13 +296,17 @@ const DEPENDENCY_PROJECTION_CODES = new Set([
|
|
|
246
296
|
]);
|
|
247
297
|
|
|
248
298
|
function validateWorkspaceProjections(product, foundation, libraries = []) {
|
|
299
|
+
// The Presentation being projected, so a failing Instance is found in it
|
|
300
|
+
// and not in another Screen that reuses its node id.
|
|
301
|
+
let projecting;
|
|
249
302
|
try {
|
|
250
303
|
for (const context of contextSelections(product, foundation)) {
|
|
251
304
|
listEffectiveTokens(product, { context, foundation, libraries });
|
|
252
305
|
}
|
|
253
306
|
for (const entry of product.manifest.entries.screens) {
|
|
254
307
|
const screen = product.entries[entry];
|
|
255
|
-
for (const presentation of screen.presentations) {
|
|
308
|
+
for (const [index, presentation] of screen.presentations.entries()) {
|
|
309
|
+
projecting = `${entry}.presentations[${index}]`;
|
|
256
310
|
projectScreen(product, screen.id, {
|
|
257
311
|
foundation,
|
|
258
312
|
libraries,
|
|
@@ -260,6 +314,7 @@ function validateWorkspaceProjections(product, foundation, libraries = []) {
|
|
|
260
314
|
});
|
|
261
315
|
}
|
|
262
316
|
}
|
|
317
|
+
projecting = undefined;
|
|
263
318
|
for (const scenario of product.domain.scenarios.values()) {
|
|
264
319
|
projectScenario(product, scenario.id, { foundation, libraries });
|
|
265
320
|
}
|
|
@@ -276,7 +331,9 @@ function validateWorkspaceProjections(product, foundation, libraries = []) {
|
|
|
276
331
|
if (!path && error.details.instanceId) {
|
|
277
332
|
const suffix = `.nodes.${error.details.instanceId}.instance.component`;
|
|
278
333
|
const usage = externalReferences(product).find((candidate) =>
|
|
279
|
-
|
|
334
|
+
projecting
|
|
335
|
+
? candidate.path === `${projecting}${suffix}`
|
|
336
|
+
: candidate.path.endsWith(suffix),
|
|
280
337
|
);
|
|
281
338
|
path = usage?.path;
|
|
282
339
|
reference = usage?.reference;
|
|
@@ -305,6 +362,83 @@ function validateWorkspaceProjections(product, foundation, libraries = []) {
|
|
|
305
362
|
}
|
|
306
363
|
}
|
|
307
364
|
|
|
365
|
+
// Instances of the Product whose selection no variant of their Component
|
|
366
|
+
// Set has any more (the Foundation renamed a value or deleted the variant).
|
|
367
|
+
// They still draw, with the closest variant (`degraded` conflicts let reads
|
|
368
|
+
// go on); the Product waits in Repair for one of the typed choices: select
|
|
369
|
+
// a variant, or remove the Instance (or retarget it, as any reference).
|
|
370
|
+
function staleInstanceConflicts(product, foundation, libraries = []) {
|
|
371
|
+
const sets = new Map([[product.manifest.packageId, product.domain.componentSets]]);
|
|
372
|
+
for (const owner of [foundation, ...libraries]) {
|
|
373
|
+
if (owner) sets.set(owner.manifest.packageId, owner.domain.componentSets);
|
|
374
|
+
}
|
|
375
|
+
const conflicts = [];
|
|
376
|
+
const check = (instance, path, nodePath, target) => {
|
|
377
|
+
const reference = instance?.component;
|
|
378
|
+
const componentSet = sets.get(reference?.packageId)?.get(reference?.assetId);
|
|
379
|
+
if (!componentSet || findComponentVariant(componentSet, instance.variant ?? {}).variant) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const closest = closestComponentVariant(componentSet, instance.variant);
|
|
383
|
+
const select = closest
|
|
384
|
+
? [
|
|
385
|
+
{
|
|
386
|
+
action: "select-instance-variant",
|
|
387
|
+
referencePath: `${nodePath}.instance`,
|
|
388
|
+
selection: structuredClone(closest.selection),
|
|
389
|
+
validSelections: componentSet.variants.map((variant) =>
|
|
390
|
+
structuredClone(variant.selection),
|
|
391
|
+
),
|
|
392
|
+
...target,
|
|
393
|
+
},
|
|
394
|
+
]
|
|
395
|
+
: [];
|
|
396
|
+
conflicts.push({
|
|
397
|
+
...conflict(
|
|
398
|
+
"missing_variant",
|
|
399
|
+
`Instance ${target.nodeId} selects ${JSON.stringify(instance.variant ?? {})}, ` +
|
|
400
|
+
`which no variant of ${reference.assetId} has. It draws as ` +
|
|
401
|
+
`${JSON.stringify(closest?.selection ?? {})} until it is repaired`,
|
|
402
|
+
`${path}`,
|
|
403
|
+
[...select, ...referenceRepairChoices(path, reference)],
|
|
404
|
+
),
|
|
405
|
+
degraded: true,
|
|
406
|
+
reference: structuredClone(reference),
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
for (const entry of product.manifest.entries.screens) {
|
|
410
|
+
const screen = product.entries[entry];
|
|
411
|
+
screen.presentations.forEach((presentation, index) => {
|
|
412
|
+
for (const [nodeId, node] of Object.entries(presentation.nodes)) {
|
|
413
|
+
if (!node.instance) continue;
|
|
414
|
+
const nodePath = `${entry}.presentations[${index}].nodes.${nodeId}`;
|
|
415
|
+
check(node.instance, `${nodePath}.instance.component`, nodePath, {
|
|
416
|
+
nodeId,
|
|
417
|
+
presentationId: presentation.id,
|
|
418
|
+
screenId: screen.id,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
for (const entry of product.manifest.entries.components) {
|
|
424
|
+
(product.entries[entry].componentSets ?? []).forEach((componentSet, setIndex) => {
|
|
425
|
+
componentSet.variants.forEach((variant, variantIndex) => {
|
|
426
|
+
for (const [nodeId, node] of Object.entries(variant.nodes)) {
|
|
427
|
+
if (!node.instance) continue;
|
|
428
|
+
const nodePath =
|
|
429
|
+
`${entry}.componentSets[${setIndex}].variants[${variantIndex}].nodes.${nodeId}`;
|
|
430
|
+
check(node.instance, `${nodePath}.instance.component`, nodePath, {
|
|
431
|
+
componentSetId: componentSet.id,
|
|
432
|
+
nodeId,
|
|
433
|
+
variantId: variant.id,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return conflicts;
|
|
440
|
+
}
|
|
441
|
+
|
|
308
442
|
function chooseLibrarySource(library) {
|
|
309
443
|
return {
|
|
310
444
|
action: "choose-library-source",
|
|
@@ -313,14 +447,35 @@ function chooseLibrarySource(library) {
|
|
|
313
447
|
};
|
|
314
448
|
}
|
|
315
449
|
|
|
450
|
+
// Remote Libraries declare their own child Libraries, so a Library server
|
|
451
|
+
// could generate an endless chain of new Package IDs (or a huge fan-out) and
|
|
452
|
+
// keep resolution downloading and caching forever. Every declaration the
|
|
453
|
+
// resolution follows counts against one budget.
|
|
454
|
+
export const MAX_LIBRARY_DECLARATIONS = 256;
|
|
455
|
+
|
|
316
456
|
async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
317
457
|
const conflicts = [];
|
|
318
458
|
const records = new Map();
|
|
319
459
|
const warnings = [];
|
|
460
|
+
let declarations = 0;
|
|
320
461
|
|
|
321
462
|
const visit = async (owner, library, index, depth, stack) => {
|
|
322
463
|
const path = `manifest.json.libraries[${index}]`;
|
|
323
464
|
const choice = chooseLibrarySource(library);
|
|
465
|
+
declarations += 1;
|
|
466
|
+
if (declarations > MAX_LIBRARY_DECLARATIONS) {
|
|
467
|
+
if (declarations === MAX_LIBRARY_DECLARATIONS + 1) {
|
|
468
|
+
conflicts.push(
|
|
469
|
+
conflict(
|
|
470
|
+
"library_limit_exceeded",
|
|
471
|
+
`Library resolution stopped after ${MAX_LIBRARY_DECLARATIONS} Library declarations`,
|
|
472
|
+
path,
|
|
473
|
+
[choice],
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
324
479
|
if (stack.has(library.packageId)) {
|
|
325
480
|
conflicts.push(
|
|
326
481
|
conflict(
|
|
@@ -335,12 +490,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
|
335
490
|
let snapshot;
|
|
336
491
|
try {
|
|
337
492
|
if (library.source.type === "url") {
|
|
338
|
-
snapshot = await
|
|
339
|
-
cacheRoot: options.libraryCacheRoot,
|
|
340
|
-
expectedPackageId: library.packageId,
|
|
341
|
-
fetchImpl: options.fetchImpl,
|
|
342
|
-
refresh: options.refreshRemoteLibraries === true,
|
|
343
|
-
});
|
|
493
|
+
snapshot = await remoteLibrarySnapshot(library, options);
|
|
344
494
|
} else {
|
|
345
495
|
if (owner.remote) {
|
|
346
496
|
throw new SmallPenError(
|
|
@@ -349,7 +499,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
|
349
499
|
{ packageId: library.packageId },
|
|
350
500
|
);
|
|
351
501
|
}
|
|
352
|
-
const candidate =
|
|
502
|
+
const candidate = localLibraryLocator(owner.locator, library.source);
|
|
353
503
|
const locator = await realpath(candidate);
|
|
354
504
|
snapshot = await openPackage(locator);
|
|
355
505
|
if (!isWithinRoot(workspaceRoot, locator)) {
|
|
@@ -363,10 +513,16 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
|
363
513
|
}
|
|
364
514
|
}
|
|
365
515
|
} catch (error) {
|
|
516
|
+
// A Package chooses its URL Libraries: report only the typed code, never
|
|
517
|
+
// upstream status or transport text a Package could use as a probe.
|
|
518
|
+
const reason =
|
|
519
|
+
library.source.type === "url"
|
|
520
|
+
? (error?.code ?? "remote_library_unavailable")
|
|
521
|
+
: error.message;
|
|
366
522
|
conflicts.push(
|
|
367
523
|
conflict(
|
|
368
524
|
"library_unavailable",
|
|
369
|
-
`Library ${library.packageId} cannot be opened: ${
|
|
525
|
+
`Library ${library.packageId} cannot be opened: ${reason}`,
|
|
370
526
|
`${path}.source`,
|
|
371
527
|
[choice],
|
|
372
528
|
),
|
|
@@ -407,7 +563,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
|
|
|
407
563
|
warnings.push(
|
|
408
564
|
warning(
|
|
409
565
|
"remote_library_offline",
|
|
410
|
-
`Library ${snapshot.manifest.name} is using its last verified cache: ${snapshot.remote.
|
|
566
|
+
`Library ${snapshot.manifest.name} is using its last verified cache: ${snapshot.remote.warningCode}`,
|
|
411
567
|
`${path}.source.url`,
|
|
412
568
|
),
|
|
413
569
|
);
|
|
@@ -563,6 +719,11 @@ export async function resolveWorkspace(productLocator, options = {}) {
|
|
|
563
719
|
...validateWorkspaceProjections(product, foundation, resolved.libraries),
|
|
564
720
|
);
|
|
565
721
|
}
|
|
722
|
+
if (conflicts.length === 0) {
|
|
723
|
+
conflicts.push(
|
|
724
|
+
...staleInstanceConflicts(product, foundation, resolved.libraries),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
566
727
|
if (conflicts.length > 0) {
|
|
567
728
|
return repair(product, conflicts, foundation, resolved.libraries, warnings);
|
|
568
729
|
}
|
package/src/webp-decode.mjs
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|