@markii/pack-build 0.17.0-next.3 → 0.17.0-next.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.
package/README.md CHANGED
@@ -7,4 +7,77 @@ esbuild's WebAssembly build, writes the prebuilt form, or produces a
7
7
  toolchain, for authoring hosts and pack build scripts; an app that only
8
8
  installs prebuilt packs does not need it. Node only.
9
9
 
10
+ ## Entry points
11
+
12
+ - `buildPackRegistrationScript(pack, cacheDir, options?)` compiles the
13
+ pack's React sources into one registration script, `webview.js`, and an
14
+ optional stylesheet. It caches the result under `cacheDir`, which must
15
+ be a folder you own and never the pack's own folder.
16
+ - `buildPackTerminalScript(pack, cacheDir, options?)` does the same for
17
+ the pack's `ansi` sources and produces `terminal.js`. It reports
18
+ `skipped` when no component names an `ansi` source.
19
+ - `exportPackArchive({ pack, cacheDir, build, buildTerminal?, fs })`
20
+ builds through those functions and returns the bytes of a
21
+ `<name>-<version>.mkp` archive. It writes nothing itself.
22
+ - `exportPack` writes the prebuilt form into a destination folder instead
23
+ of an archive.
24
+ - `lintPackCss` returns the class-prefix and color warnings for a
25
+ stylesheet.
26
+
27
+ Every build function returns an outcome and never throws. The outcome is
28
+ `built`, `failed` with a reason, or `skipped`.
29
+
30
+ ## The pack source
31
+
32
+ Each function takes a pack as `discoverPacks` from `@markii/host`
33
+ returns it. A discovered pack carries its `folder`, its parsed
34
+ `manifest`, the `manifestText`, the absolute `componentPaths` of its
35
+ React sources, `componentPathsByEngine` for other engines, and its
36
+ `scriptsDir` for shared Lua. `discoverPacks(folders, readFile,
37
+ listDirectory)` takes the folders to scan and two read functions.
38
+ `createNodePackDiscoveryIo()` from `@markii/host/node` supplies both.
39
+
40
+ ## Example
41
+
42
+ This script discovers the packs in a folder and writes one `.mkp` archive
43
+ for each.
44
+
45
+ ```ts
46
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
47
+ import { join } from 'node:path';
48
+ import { discoverPacks } from '@markii/host';
49
+ import { createNodePackDiscoveryIo } from '@markii/host/node';
50
+ import {
51
+ buildPackRegistrationScript,
52
+ buildPackTerminalScript,
53
+ exportPackArchive,
54
+ } from '@markii/pack-build';
55
+
56
+ const cacheDir = join(process.cwd(), '.pack-cache');
57
+ await mkdir(cacheDir, { recursive: true });
58
+
59
+ const io = createNodePackDiscoveryIo();
60
+ const { packs, skipped } = await discoverPacks(
61
+ ['./packs'],
62
+ io.readFile,
63
+ io.listDirectory,
64
+ );
65
+ for (const folder of skipped) console.warn(folder);
66
+
67
+ for (const pack of packs) {
68
+ const result = await exportPackArchive({
69
+ pack,
70
+ cacheDir,
71
+ build: buildPackRegistrationScript,
72
+ buildTerminal: buildPackTerminalScript,
73
+ fs: {
74
+ readFile: (path) => readFile(path, 'utf8').catch(() => undefined),
75
+ listDirectory: (path) => readdir(path).catch(() => []),
76
+ },
77
+ });
78
+ if (result.kind === 'failed') throw new Error(result.reason);
79
+ await writeFile(result.fileName, result.bytes);
80
+ }
81
+ ```
82
+
10
83
  See `docs/packs.md` in the repository for the pack contract.
@@ -12,6 +12,8 @@ export interface PackBuildSource {
12
12
  /** The pack's own folder on disk — `resolveDir` for the synthetic entry `entrySource` compiles. */
13
13
  readonly folder: string;
14
14
  readonly manifest: PackManifest;
15
+ /** The text of the pack's own `pack.json`, embedded unchanged in the compiled script. Absent means the parsed manifest is serialized instead, which can lose fields the parser normalizes. */
16
+ readonly manifestText?: string;
15
17
  /** Local component name -> absolute path to its declared source file (`manifest.components[name]`, resolved against `folder`). */
16
18
  readonly componentPaths: Readonly<Record<string, string>>;
17
19
  /** Engine id -> (local component name -> absolute source path). A terminal build reads its `ansi` slice here; `componentPaths` stays the `react` slice. Absent means the pack names no sources for any other engine. */
@@ -151,7 +151,7 @@ import { existsSync } from 'node:fs';
151
151
  import { packComponents } from '@markii/pack';
152
152
  import { lintPackCss } from './pack-css-lint.js';
153
153
  /** Bumped whenever this module's OUTPUT for the same pack sources would change (a shim/plugin tweak, an esbuild option change) — folded into the cache key (`computeCacheKey`) so a stale cached script from a previous version of this builder is never reused after an upgrade. Bumped again for the `inject`-based JSX shim (replacing `banner`): a pack built before this version leaves `__markiiJSX` as a global on `window`, so every pack must rebuild once to pick up the fix. */
154
- const BUILDER_VERSION = 5;
154
+ const BUILDER_VERSION = 6;
155
155
  /**
156
156
  * `esbuild-wasm`'s browser entry unconditionally reaches for the `self`
157
157
  * global while assembling its Go/WebAssembly runtime bridge — confirmed
@@ -509,18 +509,26 @@ function resolveFailure(text) {
509
509
  * rejection is an explicit `errors` result, never `undefined` — so
510
510
  * esbuild's own filesystem-less default resolver is never reached.
511
511
  */
512
- function virtualSourcePlugin(packName, fileContents, jailRealRoot, virtualOnlyPaths) {
512
+ function virtualSourcePlugin(packName, packFolder, fileContents, jailRealRoot, virtualOnlyPaths) {
513
+ // esbuild names each module in its output by the path a plugin returns,
514
+ // so a module is handed back under its pack-relative key (never its
515
+ // absolute build-machine path) and mapped to the real file only here.
516
+ const keyFor = (absolutePath) => path.relative(packFolder, absolutePath).split(path.sep).join('/');
517
+ const absoluteFor = (key) => path.resolve(packFolder, key);
513
518
  return {
514
519
  name: 'markii-pack-source',
515
520
  setup(build) {
516
521
  build.onResolve({ filter: /.*/ }, async (args) => {
522
+ const importer = args.namespace === VIRTUAL_SOURCE_NAMESPACE
523
+ ? absoluteFor(args.importer)
524
+ : args.importer;
517
525
  let base;
518
526
  if (path.isAbsolute(args.path)) {
519
527
  base = args.path;
520
528
  }
521
529
  else if (args.path.startsWith('./') ||
522
530
  args.path.startsWith('../')) {
523
- base = path.resolve(path.dirname(args.importer), args.path);
531
+ base = path.resolve(path.dirname(importer), args.path);
524
532
  }
525
533
  else {
526
534
  return resolveFailure(`pack "${packName}": cannot resolve "${args.path}" — a pack import must be a relative path within the pack, or "react"/"react-dom"; other bare package specifiers are not supported (packs are self-contained).`);
@@ -529,13 +537,16 @@ function virtualSourcePlugin(packName, fileContents, jailRealRoot, virtualOnlyPa
529
537
  ? base
530
538
  : await resolveImportCandidate(base);
531
539
  if (resolved === undefined) {
532
- return resolveFailure(`pack "${packName}": could not resolve "${args.path}" imported from "${args.importer}"`);
540
+ return resolveFailure(`pack "${packName}": could not resolve "${args.path}" imported from "${importer}"`);
533
541
  }
534
542
  if (virtualOnlyPaths.has(resolved)) {
535
543
  // Never exists on disk (the JSX shim) — realpath would throw
536
544
  // ENOENT. Its content was written by this builder, not read
537
545
  // from the pack, so the jail check has nothing to verify.
538
- return { path: resolved, namespace: VIRTUAL_SOURCE_NAMESPACE };
546
+ return {
547
+ path: keyFor(resolved),
548
+ namespace: VIRTUAL_SOURCE_NAMESPACE,
549
+ };
539
550
  }
540
551
  let real;
541
552
  try {
@@ -558,13 +569,17 @@ function virtualSourcePlugin(packName, fileContents, jailRealRoot, virtualOnlyPa
558
569
  }
559
570
  fileContents.set(resolved, text);
560
571
  }
561
- return { path: resolved, namespace: VIRTUAL_SOURCE_NAMESPACE };
572
+ return {
573
+ path: keyFor(resolved),
574
+ namespace: VIRTUAL_SOURCE_NAMESPACE,
575
+ };
562
576
  });
563
577
  build.onLoad({ filter: /.*/, namespace: VIRTUAL_SOURCE_NAMESPACE }, (args) => {
578
+ const absolutePath = absoluteFor(args.path);
564
579
  return {
565
- contents: fileContents.get(args.path) ?? '',
566
- loader: loaderForPath(args.path),
567
- resolveDir: path.dirname(args.path),
580
+ contents: fileContents.get(absolutePath) ?? '',
581
+ loader: loaderForPath(absolutePath),
582
+ resolveDir: path.dirname(absolutePath),
568
583
  };
569
584
  });
570
585
  },
@@ -610,6 +625,10 @@ const JSX_SHIM_SOURCE = [
610
625
  const WEBVIEW_ENGINE = 'react';
611
626
  /** The engine id whose sources the terminal build compiles (`@markii/ansi`'s `ANSI_ENGINE_ID`). */
612
627
  const TERMINAL_ENGINE = 'ansi';
628
+ /** The manifest text a compiled script registers: the pack's own file text when the caller kept it, else the parsed manifest re-serialized. */
629
+ function manifestSourceText(pack) {
630
+ return pack.manifestText ?? JSON.stringify(pack.manifest);
631
+ }
613
632
  /** One component's declared local name and the absolute path to its source, in `manifest.components` declaration order. Reads the manifest through `@markii/pack`'s `packComponents()` (the one accessor that normalizes both the string shorthand and the object form) rather than walking `pack.manifest.components` directly. */
614
633
  function orderedComponents(pack, engine = WEBVIEW_ENGINE) {
615
634
  const paths = engine === WEBVIEW_ENGINE
@@ -643,7 +662,7 @@ function entrySource(pack, components) {
643
662
  const picks = components
644
663
  .map((c, i) => ` ${JSON.stringify(c.localName)}: __markiiPick(__markiiMod${i}),`)
645
664
  .join('\n');
646
- const manifestJsonLiteral = JSON.stringify(JSON.stringify(pack.manifest));
665
+ const manifestJsonLiteral = JSON.stringify(manifestSourceText(pack));
647
666
  // Which local names the manifest declares `kind: "inline"` for, baked
648
667
  // into the artifact so a standalone consumer of the registration
649
668
  // convention sees a truthful `inline` flag (loadPack re-derives it from
@@ -711,7 +730,7 @@ function terminalEntrySource(pack, components) {
711
730
  const picks = components
712
731
  .map((c, i) => ` ${JSON.stringify(c.localName)}: __markiiPick(__markiiMod${i}),`)
713
732
  .join('\n');
714
- const manifestJsonLiteral = JSON.stringify(JSON.stringify(pack.manifest));
733
+ const manifestJsonLiteral = JSON.stringify(manifestSourceText(pack));
715
734
  const inlineNames = packComponents(pack.manifest)
716
735
  .filter((listing) => listing.kind === 'inline')
717
736
  .map((listing) => listing.localName);
@@ -799,7 +818,7 @@ function staticCssImportPaths(source, sourceDir) {
799
818
  export function computeCacheKey(pack, componentSources, cssSources = new Map()) {
800
819
  const hash = createHash('sha256');
801
820
  hash.update(`builder:${BUILDER_VERSION}\n`);
802
- hash.update(`manifest:${JSON.stringify(pack.manifest)}\n`);
821
+ hash.update(`manifest:${manifestSourceText(pack)}\n`);
803
822
  const entries = [...componentSources.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
804
823
  for (const [sourcePath, source] of entries) {
805
824
  hash.update(`component:${sourcePath}\n`);
@@ -1154,7 +1173,7 @@ async function buildPackScript(pack, cacheDir, options, kind) {
1154
1173
  terminal
1155
1174
  ? lazyGlobalModulePlugin(TERMINAL_LAZY_GLOBAL_MODULES, 'globalThis')
1156
1175
  : lazyGlobalModulePlugin(),
1157
- virtualSourcePlugin(pack.manifest.name, fileContents, jailRealRoot, virtualOnlyPaths),
1176
+ virtualSourcePlugin(pack.manifest.name, pack.folder, fileContents, jailRealRoot, virtualOnlyPaths),
1158
1177
  ],
1159
1178
  logLevel: 'silent',
1160
1179
  });
@@ -24,7 +24,7 @@
24
24
  import * as path from 'node:path';
25
25
  import { zipSync } from 'fflate';
26
26
  import { packArchiveFileName } from '@markii/pack';
27
- import { PREBUILT_TERMINAL_SCRIPT_FILENAME, emptyWebviewScript, } from '@markii/host';
27
+ import { PREBUILT_TERMINAL_SCRIPT_FILENAME, emptyWebviewScript, } from '@markii/host/internal';
28
28
  const MANIFEST_FILENAME = 'pack.json';
29
29
  const SCRIPT_FILENAME = 'webview.js';
30
30
  const STYLESHEET_FILENAME = 'webview.css';
@@ -23,7 +23,7 @@
23
23
  * is injected too.
24
24
  */
25
25
  import * as path from 'node:path';
26
- import { PREBUILT_SCRIPT_FILENAME, PREBUILT_STYLESHEET_FILENAME, PREBUILT_TERMINAL_SCRIPT_FILENAME, emptyWebviewScript, } from '@markii/host';
26
+ import { PREBUILT_SCRIPT_FILENAME, PREBUILT_STYLESHEET_FILENAME, PREBUILT_TERMINAL_SCRIPT_FILENAME, emptyWebviewScript, } from '@markii/host/internal';
27
27
  const MANIFEST_FILENAME = 'pack.json';
28
28
  const SCRIPTS_DIRNAME = 'scripts';
29
29
  /**
@@ -174,7 +174,7 @@ export async function exportPack(options) {
174
174
  reason: `pack "${packName}" was not built`,
175
175
  };
176
176
  }
177
- scriptText = emptyWebviewScript(JSON.stringify(pack.manifest));
177
+ scriptText = emptyWebviewScript(manifestText);
178
178
  }
179
179
  else {
180
180
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/pack-build",
3
- "version": "0.17.0-next.3",
3
+ "version": "0.17.0-next.5",
4
4
  "description": "Pack compilation for Markii authoring hosts: compiles a pack's component sources (and imported CSS) with esbuild-wasm's in-process path, and writes the prebuilt form or a .mkp archive. Node only.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -29,6 +29,9 @@
29
29
  "default": "./dist/index.js"
30
30
  }
31
31
  },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
32
35
  "scripts": {
33
36
  "test": "vitest run",
34
37
  "build": "tsc --noEmit -p tsconfig.json",
@@ -36,8 +39,8 @@
36
39
  "build:dist": "rm -rf dist && tsc -p tsconfig.build.json"
37
40
  },
38
41
  "dependencies": {
39
- "@markii/host": "^0.17.0-next.3",
40
- "@markii/pack": "^0.17.0-next.3",
42
+ "@markii/host": "^0.17.0-next.5",
43
+ "@markii/pack": "^0.17.0-next.5",
41
44
  "esbuild-wasm": "^0.28.2",
42
45
  "fflate": "^0.8.3"
43
46
  },