@smallpen/local-package 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -34,6 +34,38 @@ export async function initWebpDecoder() {
34
34
  await loadDecoder();
35
35
  }
36
36
 
37
+ // Reads the canvas size from the RIFF header without decoding, so callers can
38
+ // apply size limits before libwebp allocates pixels. Returns undefined when
39
+ // the header is not recognized (the decoder then reports the error).
40
+ export function webpDimensions(bytes) {
41
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
42
+ const tag = (offset) => String.fromCharCode(...view.subarray(offset, offset + 4));
43
+ if (view.length < 30 || tag(0) !== "RIFF" || tag(8) !== "WEBP") {
44
+ return undefined;
45
+ }
46
+ const chunk = tag(12);
47
+ if (chunk === "VP8 ") {
48
+ return {
49
+ height: (view[28] | (view[29] << 8)) & 0x3fff,
50
+ width: (view[26] | (view[27] << 8)) & 0x3fff,
51
+ };
52
+ }
53
+ if (chunk === "VP8L") {
54
+ const bits = view[21] | (view[22] << 8) | (view[23] << 16) | (view[24] << 24);
55
+ return {
56
+ height: ((bits >>> 14) & 0x3fff) + 1,
57
+ width: (bits & 0x3fff) + 1,
58
+ };
59
+ }
60
+ if (chunk === "VP8X") {
61
+ return {
62
+ height: (view[27] | (view[28] << 8) | (view[29] << 16)) + 1,
63
+ width: (view[24] | (view[25] << 8) | (view[26] << 16)) + 1,
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+
37
69
  // Synchronous decode of a complete WebP image to RGBA pixels. Requires a
38
70
  // prior initWebpDecoder(); the renderer initializes it once per projection.
39
71
  export function decodeWebpSync(bytes, diagnostics, path) {
package/src/workspace.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { realpath } from "node:fs/promises";
2
- import { dirname, isAbsolute, relative, resolve } from "node:path";
2
+ import { dirname, isAbsolute, posix, resolve, win32 } from "node:path";
3
3
 
4
4
  import {
5
5
  combineContextAxes,
@@ -9,16 +9,63 @@ import {
9
9
  SmallPenError,
10
10
  } from "@smallpen/core";
11
11
 
12
+ import { isWithinRoot } from "./fs-utils.mjs";
12
13
  import { openPackage } from "./local-package.mjs";
13
- import { openRemoteLibrary } from "./remote-library.mjs";
14
+ import { openRemoteLibrary, remoteManifestUrl } from "./remote-library.mjs";
14
15
 
15
16
  function isRecord(value) {
16
17
  return value !== null && typeof value === "object" && !Array.isArray(value);
17
18
  }
18
19
 
19
- function isWithinRoot(rootPath, candidatePath) {
20
- const child = relative(rootPath, candidatePath);
21
- return child === "" || (!child.startsWith("..") && !isAbsolute(child));
20
+ // Local Library paths are relative to the declaring Package's directory, like
21
+ // Foundation dependency paths. Unlike dependencies they may climb out of the
22
+ // Workspace ("../libraries/x.smallpen" is the documented form and resolves
23
+ // with an external_library_path warning), but absolute, drive-letter, UNC,
24
+ // and backslash paths are machine-specific and are rejected.
25
+ export function localLibraryLocator(ownerLocator, source) {
26
+ const path = source?.path;
27
+ if (
28
+ typeof path !== "string" ||
29
+ path.length === 0 ||
30
+ path.includes("\0") ||
31
+ path.includes("\\") ||
32
+ isAbsolute(path) ||
33
+ posix.isAbsolute(path) ||
34
+ win32.isAbsolute(path)
35
+ ) {
36
+ throw new SmallPenError(
37
+ "invalid_library_source",
38
+ "Local Library path must be relative to the declaring Package",
39
+ { path },
40
+ );
41
+ }
42
+ return resolve(dirname(ownerLocator), path);
43
+ }
44
+
45
+ // `remoteLibrarySnapshots` (a Map keyed by manifest URL) lets a long-lived
46
+ // caller reuse remote snapshots it already verified; with
47
+ // `reuseRemoteLibrarySnapshots` the network and disk cache are skipped.
48
+ async function remoteLibrarySnapshot(library, options) {
49
+ const memo =
50
+ options.remoteLibrarySnapshots instanceof Map
51
+ ? options.remoteLibrarySnapshots
52
+ : undefined;
53
+ const key = remoteManifestUrl(library.source.url);
54
+ if (
55
+ memo?.has(key) &&
56
+ options.reuseRemoteLibrarySnapshots === true &&
57
+ options.refreshRemoteLibraries !== true
58
+ ) {
59
+ return memo.get(key);
60
+ }
61
+ const snapshot = await openRemoteLibrary(library.source.url, {
62
+ cacheRoot: options.libraryCacheRoot,
63
+ expectedPackageId: library.packageId,
64
+ fetchImpl: options.fetchImpl,
65
+ refresh: options.refreshRemoteLibraries === true,
66
+ });
67
+ memo?.set(key, snapshot);
68
+ return snapshot;
22
69
  }
23
70
 
24
71
  function chooseFoundation(dependency) {
@@ -213,7 +260,8 @@ function validateExternalReferences(product, libraries) {
213
260
 
214
261
  function contextSelections(product, foundation) {
215
262
  const axes = [...combineContextAxes(product, foundation).values()].sort(
216
- (left, right) => left.id.localeCompare(right.id),
263
+ // Code-unit order: localeCompare would follow the host LANG.
264
+ (left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
217
265
  );
218
266
  let selections = [{}];
219
267
  for (const axis of axes) {
@@ -313,14 +361,35 @@ function chooseLibrarySource(library) {
313
361
  };
314
362
  }
315
363
 
364
+ // Remote Libraries declare their own child Libraries, so a Library server
365
+ // could generate an endless chain of new Package IDs (or a huge fan-out) and
366
+ // keep resolution downloading and caching forever. Every declaration the
367
+ // resolution follows counts against one budget.
368
+ export const MAX_LIBRARY_DECLARATIONS = 256;
369
+
316
370
  async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
317
371
  const conflicts = [];
318
372
  const records = new Map();
319
373
  const warnings = [];
374
+ let declarations = 0;
320
375
 
321
376
  const visit = async (owner, library, index, depth, stack) => {
322
377
  const path = `manifest.json.libraries[${index}]`;
323
378
  const choice = chooseLibrarySource(library);
379
+ declarations += 1;
380
+ if (declarations > MAX_LIBRARY_DECLARATIONS) {
381
+ if (declarations === MAX_LIBRARY_DECLARATIONS + 1) {
382
+ conflicts.push(
383
+ conflict(
384
+ "library_limit_exceeded",
385
+ `Library resolution stopped after ${MAX_LIBRARY_DECLARATIONS} Library declarations`,
386
+ path,
387
+ [choice],
388
+ ),
389
+ );
390
+ }
391
+ return;
392
+ }
324
393
  if (stack.has(library.packageId)) {
325
394
  conflicts.push(
326
395
  conflict(
@@ -335,12 +404,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
335
404
  let snapshot;
336
405
  try {
337
406
  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
- });
407
+ snapshot = await remoteLibrarySnapshot(library, options);
344
408
  } else {
345
409
  if (owner.remote) {
346
410
  throw new SmallPenError(
@@ -349,7 +413,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
349
413
  { packageId: library.packageId },
350
414
  );
351
415
  }
352
- const candidate = resolve(dirname(owner.locator), library.source.path);
416
+ const candidate = localLibraryLocator(owner.locator, library.source);
353
417
  const locator = await realpath(candidate);
354
418
  snapshot = await openPackage(locator);
355
419
  if (!isWithinRoot(workspaceRoot, locator)) {
@@ -363,10 +427,16 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
363
427
  }
364
428
  }
365
429
  } catch (error) {
430
+ // A Package chooses its URL Libraries: report only the typed code, never
431
+ // upstream status or transport text a Package could use as a probe.
432
+ const reason =
433
+ library.source.type === "url"
434
+ ? (error?.code ?? "remote_library_unavailable")
435
+ : error.message;
366
436
  conflicts.push(
367
437
  conflict(
368
438
  "library_unavailable",
369
- `Library ${library.packageId} cannot be opened: ${error.message}`,
439
+ `Library ${library.packageId} cannot be opened: ${reason}`,
370
440
  `${path}.source`,
371
441
  [choice],
372
442
  ),
@@ -407,7 +477,7 @@ async function resolveDeclaredLibraries(owners, options, workspaceRoot) {
407
477
  warnings.push(
408
478
  warning(
409
479
  "remote_library_offline",
410
- `Library ${snapshot.manifest.name} is using its last verified cache: ${snapshot.remote.warning}`,
480
+ `Library ${snapshot.manifest.name} is using its last verified cache: ${snapshot.remote.warningCode}`,
411
481
  `${path}.source.url`,
412
482
  ),
413
483
  );