@markii/pack-build 0.0.0-stage → 0.17.0-next.2

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.
@@ -0,0 +1,335 @@
1
+ /**
2
+ * The internal compose-and-write path pack packaging builds on (VS Code is
3
+ * the AUTHORING host and owns pack packaging; Obsidian only ever CONSUMES a
4
+ * pack another host produced). Compiles a pack with the existing
5
+ * `./pack-build.ts` machinery, through its normal host-owned cache, exactly
6
+ * like an ordinary preview build; only its OUTPUT is copied. Then writes a
7
+ * clean, distributable folder at a CALLER-CHOSEN destination: `pack.json`
8
+ * (copied verbatim from the pack's own folder), `webview.js`,
9
+ * `webview.css` when the build emitted one, and every `*.lua` file the
10
+ * pack's own `scripts/` directory holds.
11
+ *
12
+ * The pack's SOURCE folder is NEVER written to. Two callers compose
13
+ * through this module: `apps/vscode/src/packs/build-bundled-packs.ts`
14
+ * writes a pack's build output into the extension's own `dist/packs` at
15
+ * build time, and `./pack-export-archive.ts` reuses its `PackExportBuilder`
16
+ * and `PackExportFs` types to build the ONE shape the "Markii: Export
17
+ * Pack" command itself produces, a single `.mkp` archive.
18
+ *
19
+ * Filesystem access is injected (`PackExportFs`), matching every other
20
+ * module in this directory, so this module is testable with in-memory
21
+ * fakes and carries no host-specific save-dialog/notification behavior of
22
+ * its own; `confirmOverwrite` is the one piece of user interaction, and it
23
+ * is injected too.
24
+ */
25
+ import * as path from 'node:path';
26
+ import { PREBUILT_SCRIPT_FILENAME, PREBUILT_STYLESHEET_FILENAME, PREBUILT_TERMINAL_SCRIPT_FILENAME, emptyWebviewScript, } from '@markii/host';
27
+ const MANIFEST_FILENAME = 'pack.json';
28
+ const SCRIPTS_DIRNAME = 'scripts';
29
+ /**
30
+ * Path jail. Joins `segments` onto `root`, or `undefined` when any segment
31
+ * is not a plain path segment (empty, `.`, `..`, containing `/` or `\`, or
32
+ * absolute) or the resolved join would land outside `root`. This is the
33
+ * ONLY function `exportPack` uses to compute a write target: nothing is
34
+ * ever written to a path this function did not itself derive. The
35
+ * caller-supplied `exportName` goes through this exact same validation as
36
+ * every other segment — an `exportName` carrying a separator, `..`, or an
37
+ * absolute path is refused here, before anything is written.
38
+ */
39
+ export function resolveExportTarget(root, ...segments) {
40
+ if (segments.length === 0)
41
+ return undefined;
42
+ for (const segment of segments) {
43
+ if (segment.length === 0)
44
+ return undefined;
45
+ if (segment === '.' || segment === '..')
46
+ return undefined;
47
+ if (segment.includes('/') || segment.includes('\\'))
48
+ return undefined;
49
+ if (path.isAbsolute(segment))
50
+ return undefined;
51
+ }
52
+ const resolvedRoot = path.resolve(root);
53
+ const target = path.resolve(resolvedRoot, ...segments);
54
+ if (target !== resolvedRoot && !target.startsWith(resolvedRoot + path.sep)) {
55
+ return undefined;
56
+ }
57
+ return target;
58
+ }
59
+ function describeThrown(err) {
60
+ return err instanceof Error ? err.message : String(err);
61
+ }
62
+ /** `manifest.name` unless empty/whitespace, in which case a placeholder that still reads sensibly in a failure message. */
63
+ function packNameOf(pack) {
64
+ return pack.manifest.name;
65
+ }
66
+ /**
67
+ * NEVER throws: every failure (an unsafe `exportName`, a build failure, an
68
+ * unreadable build output or `pack.json`, a write that fails) comes back as
69
+ * `{ kind: 'failed', reason }`. A declined overwrite comes back as
70
+ * `{ kind: 'cancelled' }` with nothing written.
71
+ *
72
+ * Order of operations: resolve the destination folder and every write
73
+ * target inside it (path-jailed against `destinationDir`, including the
74
+ * user-supplied `exportName` itself); run the build through the normal
75
+ * cache; read `pack.json` and the built script (and stylesheet, when the
76
+ * build produced one) back as text; list the pack's `scripts/` directory
77
+ * for `.lua` files; ask `confirmOverwrite` once, only if the destination
78
+ * folder already holds files this export would replace; then create the
79
+ * destination folder and write everything.
80
+ */
81
+ export async function exportPack(options) {
82
+ const { pack, cacheDir, destinationDir, exportName, build, buildTerminal, fs, confirmOverwrite, } = options;
83
+ const packName = packNameOf(pack);
84
+ const destinationFolder = resolveExportTarget(destinationDir, exportName);
85
+ if (destinationFolder === undefined) {
86
+ return {
87
+ kind: 'failed',
88
+ packName,
89
+ reason: `"${exportName}" is not a valid export folder name`,
90
+ };
91
+ }
92
+ const manifestTarget = resolveExportTarget(destinationFolder, MANIFEST_FILENAME);
93
+ const scriptTarget = resolveExportTarget(destinationFolder, PREBUILT_SCRIPT_FILENAME);
94
+ const stylesheetTarget = resolveExportTarget(destinationFolder, PREBUILT_STYLESHEET_FILENAME);
95
+ const terminalTarget = resolveExportTarget(destinationFolder, PREBUILT_TERMINAL_SCRIPT_FILENAME);
96
+ const scriptsDirTarget = resolveExportTarget(destinationFolder, SCRIPTS_DIRNAME);
97
+ if (manifestTarget === undefined ||
98
+ scriptTarget === undefined ||
99
+ stylesheetTarget === undefined ||
100
+ terminalTarget === undefined ||
101
+ scriptsDirTarget === undefined) {
102
+ return {
103
+ kind: 'failed',
104
+ packName,
105
+ reason: `could not resolve a safe write location inside "${destinationFolder}"`,
106
+ };
107
+ }
108
+ let manifestText;
109
+ try {
110
+ manifestText = await fs.readFile(path.join(pack.folder, MANIFEST_FILENAME));
111
+ }
112
+ catch (err) {
113
+ return { kind: 'failed', packName, reason: describeThrown(err) };
114
+ }
115
+ if (manifestText === undefined) {
116
+ return {
117
+ kind: 'failed',
118
+ packName,
119
+ reason: `pack.json for pack "${packName}" could not be read`,
120
+ };
121
+ }
122
+ let outcome;
123
+ try {
124
+ outcome = await build(pack, cacheDir);
125
+ }
126
+ catch (err) {
127
+ return { kind: 'failed', packName, reason: describeThrown(err) };
128
+ }
129
+ if (outcome.kind === 'failed') {
130
+ // Verbatim: a host-specific build-failure reason arrives through here,
131
+ // and its single home is that app's own diagnostics module — never
132
+ // reworded on this path.
133
+ return { kind: 'failed', packName, reason: outcome.reason };
134
+ }
135
+ let terminalText;
136
+ const hasTerminalSources = Object.keys(pack.componentPathsByEngine?.['ansi'] ?? {}).length > 0;
137
+ if (buildTerminal !== undefined && hasTerminalSources) {
138
+ let terminalOutcome;
139
+ try {
140
+ terminalOutcome = await buildTerminal(pack, cacheDir);
141
+ }
142
+ catch (err) {
143
+ return { kind: 'failed', packName, reason: describeThrown(err) };
144
+ }
145
+ if (terminalOutcome.kind === 'failed') {
146
+ return {
147
+ kind: 'failed',
148
+ packName,
149
+ reason: `terminal build: ${terminalOutcome.reason}`,
150
+ };
151
+ }
152
+ if (terminalOutcome.kind === 'built') {
153
+ try {
154
+ terminalText = await fs.readFile(terminalOutcome.scriptPath);
155
+ }
156
+ catch {
157
+ terminalText = undefined;
158
+ }
159
+ if (terminalText === undefined) {
160
+ return {
161
+ kind: 'failed',
162
+ packName,
163
+ reason: `built terminal script for pack "${packName}" could not be read back`,
164
+ };
165
+ }
166
+ }
167
+ }
168
+ let scriptText;
169
+ if (outcome.kind === 'skipped') {
170
+ if (terminalText === undefined) {
171
+ return {
172
+ kind: 'failed',
173
+ packName,
174
+ reason: `pack "${packName}" was not built`,
175
+ };
176
+ }
177
+ scriptText = emptyWebviewScript(JSON.stringify(pack.manifest));
178
+ }
179
+ else {
180
+ try {
181
+ scriptText = await fs.readFile(outcome.scriptPath);
182
+ }
183
+ catch (err) {
184
+ return { kind: 'failed', packName, reason: describeThrown(err) };
185
+ }
186
+ if (scriptText === undefined) {
187
+ return {
188
+ kind: 'failed',
189
+ packName,
190
+ reason: `built script for pack "${packName}" could not be read back`,
191
+ };
192
+ }
193
+ }
194
+ let stylesheetText;
195
+ if (outcome.kind === 'built' && outcome.stylesheetPath !== undefined) {
196
+ try {
197
+ stylesheetText = await fs.readFile(outcome.stylesheetPath);
198
+ }
199
+ catch {
200
+ stylesheetText = undefined;
201
+ }
202
+ // A read failure here is treated as "no stylesheet" rather than failing
203
+ // the whole export — the script alone is still a usable pack.
204
+ }
205
+ // The pack's own scripts/ directory, copied through verbatim. Absent or
206
+ // unreadable simply yields no script files, matching every other
207
+ // filesystem-adjacent module in this directory's "quiet skip" posture for
208
+ // an individually unreadable entry.
209
+ let scriptEntries = [];
210
+ try {
211
+ scriptEntries = await fs.listDirectory(pack.scriptsDir);
212
+ }
213
+ catch {
214
+ scriptEntries = [];
215
+ }
216
+ const luaFilenames = scriptEntries.filter((name) => name.endsWith('.lua'));
217
+ const luaFiles = [];
218
+ for (const filename of luaFilenames) {
219
+ const target = resolveExportTarget(scriptsDirTarget, filename);
220
+ if (target === undefined)
221
+ continue; // an unsafe entry name is skipped, not fatal
222
+ let text;
223
+ try {
224
+ text = await fs.readFile(path.join(pack.scriptsDir, filename));
225
+ }
226
+ catch {
227
+ text = undefined;
228
+ }
229
+ if (text === undefined)
230
+ continue; // unreadable individual file: quietly skipped
231
+ luaFiles.push({ target, text });
232
+ }
233
+ let destinationExists;
234
+ try {
235
+ destinationExists = await fs.exists(destinationFolder);
236
+ }
237
+ catch (err) {
238
+ return { kind: 'failed', packName, reason: describeThrown(err) };
239
+ }
240
+ const existingPaths = [];
241
+ if (destinationExists) {
242
+ const candidates = [
243
+ manifestTarget,
244
+ scriptTarget,
245
+ ...(terminalText !== undefined ? [terminalTarget] : []),
246
+ ...(stylesheetText !== undefined ? [stylesheetTarget] : []),
247
+ ...luaFiles.map((file) => file.target),
248
+ ];
249
+ try {
250
+ for (const candidate of candidates) {
251
+ if (await fs.exists(candidate)) {
252
+ existingPaths.push(candidate);
253
+ }
254
+ }
255
+ // A stale webview.css this export would remove (see below) also
256
+ // counts as "a file this export would replace".
257
+ if (stylesheetText === undefined && (await fs.exists(stylesheetTarget))) {
258
+ existingPaths.push(stylesheetTarget);
259
+ }
260
+ if (terminalText === undefined && (await fs.exists(terminalTarget))) {
261
+ existingPaths.push(terminalTarget);
262
+ }
263
+ }
264
+ catch (err) {
265
+ return { kind: 'failed', packName, reason: describeThrown(err) };
266
+ }
267
+ }
268
+ if (existingPaths.length > 0) {
269
+ let proceed;
270
+ try {
271
+ proceed = await confirmOverwrite({ packName, existingPaths });
272
+ }
273
+ catch (err) {
274
+ return { kind: 'failed', packName, reason: describeThrown(err) };
275
+ }
276
+ if (!proceed) {
277
+ return { kind: 'cancelled', packName };
278
+ }
279
+ }
280
+ try {
281
+ await fs.makeDirectory(destinationFolder);
282
+ await fs.writeFile(manifestTarget, manifestText);
283
+ await fs.writeFile(scriptTarget, scriptText);
284
+ if (terminalText !== undefined) {
285
+ await fs.writeFile(terminalTarget, terminalText);
286
+ }
287
+ else if (await fs.exists(terminalTarget)) {
288
+ // A stale terminal.js from an earlier export would otherwise keep
289
+ // rendering views this build no longer has.
290
+ await fs.deleteFile(terminalTarget);
291
+ }
292
+ let stylesheetBytes;
293
+ let removedStylesheetPath;
294
+ if (stylesheetText !== undefined) {
295
+ await fs.writeFile(stylesheetTarget, stylesheetText);
296
+ stylesheetBytes = Buffer.byteLength(stylesheetText, 'utf8');
297
+ }
298
+ else if (await fs.exists(stylesheetTarget)) {
299
+ // A stale webview.css from an earlier export would otherwise keep
300
+ // styling the pack even though this build produced none.
301
+ await fs.deleteFile(stylesheetTarget);
302
+ removedStylesheetPath = stylesheetTarget;
303
+ }
304
+ if (luaFiles.length > 0) {
305
+ await fs.makeDirectory(scriptsDirTarget);
306
+ for (const file of luaFiles) {
307
+ await fs.writeFile(file.target, file.text);
308
+ }
309
+ }
310
+ return {
311
+ kind: 'written',
312
+ packName,
313
+ destinationFolder,
314
+ manifestPath: manifestTarget,
315
+ manifestBytes: Buffer.byteLength(manifestText, 'utf8'),
316
+ scriptPath: scriptTarget,
317
+ scriptBytes: Buffer.byteLength(scriptText, 'utf8'),
318
+ ...(terminalText !== undefined
319
+ ? {
320
+ terminalScriptPath: terminalTarget,
321
+ terminalScriptBytes: Buffer.byteLength(terminalText, 'utf8'),
322
+ }
323
+ : {}),
324
+ ...(stylesheetText !== undefined
325
+ ? { stylesheetPath: stylesheetTarget, stylesheetBytes }
326
+ : {}),
327
+ ...(removedStylesheetPath !== undefined ? { removedStylesheetPath } : {}),
328
+ scriptFilesCopied: luaFiles.length,
329
+ warnings: outcome.kind === 'built' ? outcome.warnings : [],
330
+ };
331
+ }
332
+ catch (err) {
333
+ return { kind: 'failed', packName, reason: describeThrown(err) };
334
+ }
335
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@markii/pack-build",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.17.0-next.2",
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
+ "keywords": [
6
+ "markdown",
7
+ "mk.md",
8
+ "markii",
9
+ "pack",
10
+ "esbuild"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "sadigaxund",
14
+ "homepage": "https://github.com/markii-org/markii#readme",
15
+ "bugs": "https://github.com/markii-org/markii/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/markii-org/markii.git",
19
+ "directory": "packages/markii-pack-build"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "scripts": {
33
+ "test": "vitest run",
34
+ "build": "tsc --noEmit -p tsconfig.json",
35
+ "lint": "eslint .",
36
+ "build:dist": "rm -rf dist && tsc -p tsconfig.build.json"
37
+ },
38
+ "dependencies": {
39
+ "@markii/host": "^0.17.0-next.2",
40
+ "@markii/pack": "^0.17.0-next.2",
41
+ "esbuild-wasm": "^0.28.2",
42
+ "fflate": "^0.8.3"
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }