gutterpress 0.10.11 → 0.11.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/README.md +1 -1
- package/dist/{README-7x5akcns.md → README-3nj919rm.md} +10 -8
- package/dist/{README-3zk4tgnj.md → README-8b33qbjw.md} +14 -21
- package/dist/api/index.js +2 -2
- package/dist/{audit-ebbbhpx7.js → audit-9vv4ncjv.js} +4 -4
- package/dist/{build-zrajk3s2.js → build-ea05bvs0.js} +4 -4
- package/dist/{cli-gxq7yz5f.js → cli-6gg8vwaj.js} +1 -1
- package/dist/{cli-hz44te1m.js → cli-cwv73tkx.js} +1426 -1405
- package/dist/{cli-k8f9eb12.js → cli-sekhwex4.js} +5 -5
- package/dist/{cli-36r50yr3.js → cli-y4vrycpn.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-78qft9g7.js → doctor-k529b5et.js} +2 -2
- package/dist/{engine-qtmvaqt7.js → engine-cbc17ne4.js} +1 -1
- package/dist/{engine-pt2acjb5.js → engine-w5ed5hmb.js} +2 -2
- package/dist/{ext-3g1mk8mb.js → ext-hbt1sdg5.js} +4 -4
- package/dist/{index-a0e2253t.js → index-12pvhbyz.js} +1 -1
- package/dist/{index-5dgxeevv.js → index-6n15e1ze.js} +5 -5
- package/dist/{index-03cqyyyk.js → index-qmvmvetj.js} +1764 -1748
- package/dist/index.js +3 -3
- package/dist/lib/markdown/index.d.ts +8 -16
- package/dist/lib/markdown/plugins.d.ts +9 -4
- package/dist/lib/markdown/renderer.d.ts +18 -0
- package/dist/lib/project-scaffold.d.ts +1 -1
- package/dist/{lint-va95zh6j.js → lint-cwb6h2n5.js} +4 -4
- package/dist/{new-3ex11eyq.js → new-sja9wn5c.js} +4 -4
- package/dist/plugin-spbcem4j.css +103 -0
- package/dist/{plugin.test.js-k2r87k04.tpl → plugin.test.js-nw7c84s6.tpl} +5 -6
- package/dist/{preflight-q8w6dnjh.js → preflight-maw8h45k.js} +4 -4
- package/dist/{preview-azatsrtp.js → preview-vjwpj4tf.js} +4 -4
- package/dist/{publish-5tqzepkb.js → publish-spej40gd.js} +4 -4
- package/dist/render.js +1 -1
- package/dist/{tokens-2sracx1w.css → tokens-7984461t.css} +8 -8
- package/dist/{validate-rp613ejg.js → validate-3ppsdngq.js} +4 -4
- package/package.json +1 -1
- package/dist/plugin-m2w7g47v.css +0 -114
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
stampCreator,
|
|
14
14
|
stripAnnotations,
|
|
15
15
|
warn
|
|
16
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-y4vrycpn.js";
|
|
17
17
|
import {
|
|
18
18
|
DEBOUNCE,
|
|
19
19
|
UsageError,
|
|
20
20
|
resolvePort
|
|
21
|
-
} from "./cli-
|
|
21
|
+
} from "./cli-6gg8vwaj.js";
|
|
22
22
|
import {
|
|
23
23
|
MARGIN_BOX_IGNORED_PROPERTIES,
|
|
24
24
|
RENDER_TIMEOUT_MS,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
getAssetsDir,
|
|
29
29
|
getBrowser,
|
|
30
30
|
prewarmBrowser
|
|
31
|
-
} from "./cli-
|
|
31
|
+
} from "./cli-sekhwex4.js";
|
|
32
32
|
import {
|
|
33
33
|
INSTALL_HINTS,
|
|
34
34
|
execCapture,
|
|
@@ -5165,14 +5165,30 @@ function applyPlugins(md, plugins) {
|
|
|
5165
5165
|
}
|
|
5166
5166
|
}
|
|
5167
5167
|
}
|
|
5168
|
-
function collectPluginCss(plugins) {
|
|
5169
|
-
return plugins.map((p) => p.css).filter((css) => typeof css === "string" && css.length > 0).join(`
|
|
5170
|
-
|
|
5171
|
-
`);
|
|
5172
|
-
}
|
|
5173
5168
|
function collectPluginStylePaths(plugins) {
|
|
5174
5169
|
return plugins.flatMap((p) => p.styles ?? []);
|
|
5175
5170
|
}
|
|
5171
|
+
function collectPluginStyleGroups(plugins) {
|
|
5172
|
+
const taken = new Set;
|
|
5173
|
+
const groups = [];
|
|
5174
|
+
for (const p of plugins) {
|
|
5175
|
+
const paths = p.styles ?? [];
|
|
5176
|
+
const css = typeof p.css === "string" && p.css.length > 0 ? p.css : undefined;
|
|
5177
|
+
if (paths.length === 0 && !css)
|
|
5178
|
+
continue;
|
|
5179
|
+
const base = `ext.${layerSlug(p.name)}`;
|
|
5180
|
+
let layer = base;
|
|
5181
|
+
for (let n = 2;taken.has(layer); n++)
|
|
5182
|
+
layer = `${base}-${n}`;
|
|
5183
|
+
taken.add(layer);
|
|
5184
|
+
groups.push({ name: p.name, layer, paths, ...css ? { css } : {} });
|
|
5185
|
+
}
|
|
5186
|
+
return groups;
|
|
5187
|
+
}
|
|
5188
|
+
function layerSlug(name) {
|
|
5189
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5190
|
+
return /^[a-z]/.test(slug) ? slug : `x${slug}`;
|
|
5191
|
+
}
|
|
5176
5192
|
|
|
5177
5193
|
// src/lib/markdown/plugins.ts
|
|
5178
5194
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -6085,12 +6101,12 @@ async function loadPlugins(configs, baseDir, onError) {
|
|
|
6085
6101
|
}
|
|
6086
6102
|
async function loadPluginsWithCss(configs, baseDir, onError) {
|
|
6087
6103
|
if (!configs || configs.length === 0) {
|
|
6088
|
-
return { plugins: undefined,
|
|
6104
|
+
return { plugins: undefined, pluginStyles: [], pluginStylePaths: [] };
|
|
6089
6105
|
}
|
|
6090
6106
|
const plugins = await loadPlugins(configs, baseDir, onError);
|
|
6091
6107
|
return {
|
|
6092
6108
|
plugins,
|
|
6093
|
-
|
|
6109
|
+
pluginStyles: collectPluginStyleGroups(plugins),
|
|
6094
6110
|
pluginStylePaths: collectPluginStylePaths(plugins)
|
|
6095
6111
|
};
|
|
6096
6112
|
}
|
|
@@ -7635,7 +7651,7 @@ ${MARKER_CSS.trim()}
|
|
|
7635
7651
|
@layer gp.vocab {
|
|
7636
7652
|
${GUTTERPRESS_CSS.trim()}
|
|
7637
7653
|
}`,
|
|
7638
|
-
pluginCss ? `/*
|
|
7654
|
+
pluginCss ? `/* extension css */
|
|
7639
7655
|
${pluginCss.trim()}` : null,
|
|
7640
7656
|
projectCss ? `/* project css */
|
|
7641
7657
|
${projectCss.trim()}` : null
|
|
@@ -7669,16 +7685,34 @@ async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
|
|
|
7669
7685
|
async function renderChapters(inputDir, opts = {}) {
|
|
7670
7686
|
const styles = await resolveActiveStyles(inputDir, opts.styles);
|
|
7671
7687
|
const inlined = await inlineStyles(inputDir, styles);
|
|
7672
|
-
const
|
|
7673
|
-
const
|
|
7688
|
+
const blocks = [];
|
|
7689
|
+
const layers = [];
|
|
7690
|
+
const styleWarnings = [];
|
|
7691
|
+
const cssAssetCopies = [];
|
|
7692
|
+
for (const group of opts.pluginStyles ?? []) {
|
|
7693
|
+
const groupInlined = await inlineStyles(inputDir, group.paths);
|
|
7694
|
+
styleWarnings.push(...groupInlined.warnings);
|
|
7695
|
+
cssAssetCopies.push(...groupInlined.copies);
|
|
7696
|
+
const css = [groupInlined.css, group.css].filter((s) => !!s && s.trim().length > 0).join(`
|
|
7697
|
+
|
|
7698
|
+
`);
|
|
7699
|
+
if (!css)
|
|
7700
|
+
continue;
|
|
7701
|
+
layers.push(group.layer);
|
|
7702
|
+
blocks.push(`/* ${group.name} */
|
|
7703
|
+
@layer ${group.layer} {
|
|
7704
|
+
${css.trim()}
|
|
7705
|
+
}`);
|
|
7706
|
+
}
|
|
7707
|
+
styleWarnings.push(...inlined.warnings);
|
|
7674
7708
|
if (styleWarnings.length > 0)
|
|
7675
7709
|
opts.onStyleWarnings?.(styleWarnings);
|
|
7676
|
-
|
|
7710
|
+
cssAssetCopies.push(...inlined.copies);
|
|
7677
7711
|
if (cssAssetCopies.length > 0)
|
|
7678
7712
|
opts.onCssAssets?.(cssAssetCopies);
|
|
7679
|
-
const pluginCss =
|
|
7713
|
+
const pluginCss = blocks.length > 0 ? [`@layer ${layers.join(", ")};`, ...blocks].join(`
|
|
7680
7714
|
|
|
7681
|
-
`);
|
|
7715
|
+
`) : "";
|
|
7682
7716
|
const files = await resolveActiveMarkdownFiles(inputDir, opts.files);
|
|
7683
7717
|
if (files.length === 0) {
|
|
7684
7718
|
throw new Error(`No markdown files found in ${inputDir}`);
|
|
@@ -7687,7 +7721,7 @@ async function renderChapters(inputDir, opts = {}) {
|
|
|
7687
7721
|
files,
|
|
7688
7722
|
readText: (relPath) => readFile19(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
|
|
7689
7723
|
projectCss: inlined.css,
|
|
7690
|
-
preloadImages:
|
|
7724
|
+
preloadImages: cssAssetCopies.map((c) => c.to),
|
|
7691
7725
|
title: opts.title,
|
|
7692
7726
|
plugins: opts.plugins,
|
|
7693
7727
|
pluginCss,
|
|
@@ -7704,8 +7738,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
|
|
|
7704
7738
|
styles: opts.styles,
|
|
7705
7739
|
files: opts.files,
|
|
7706
7740
|
plugins: opts.plugins,
|
|
7707
|
-
|
|
7708
|
-
pluginStylePaths: opts.pluginStylePaths,
|
|
7741
|
+
pluginStyles: opts.pluginStyles,
|
|
7709
7742
|
onChapterWarnings: opts.onChapterWarnings,
|
|
7710
7743
|
onImageRefs: opts.onImageRefs,
|
|
7711
7744
|
onCssAssets: opts.onCssAssets,
|
|
@@ -8581,7 +8614,7 @@ async function renderBook(ctx) {
|
|
|
8581
8614
|
} else {
|
|
8582
8615
|
log.info("Using all .md files in alphabetical order");
|
|
8583
8616
|
}
|
|
8584
|
-
const { plugins,
|
|
8617
|
+
const { plugins, pluginStyles } = await loadBuildPlugins(ctx);
|
|
8585
8618
|
const imageRefs = [];
|
|
8586
8619
|
const cssAssets = [];
|
|
8587
8620
|
const htmlFile = await renderChaptersToFile(renderDir, workDir, {
|
|
@@ -8589,8 +8622,7 @@ async function renderBook(ctx) {
|
|
|
8589
8622
|
styles: config.styles,
|
|
8590
8623
|
files: config.source.files,
|
|
8591
8624
|
plugins,
|
|
8592
|
-
|
|
8593
|
-
pluginStylePaths,
|
|
8625
|
+
pluginStyles,
|
|
8594
8626
|
onChapterWarnings: (file, warnings) => {
|
|
8595
8627
|
for (const w of warnings) {
|
|
8596
8628
|
const key = layoutWarningKey(path9.resolve(renderDir, file), w.line, w.message);
|
|
@@ -8736,7 +8768,7 @@ class PdfOutput {
|
|
|
8736
8768
|
const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
|
|
8737
8769
|
await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
|
|
8738
8770
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
8739
|
-
const { buildNativePdf } = await import("./engine-
|
|
8771
|
+
const { buildNativePdf } = await import("./engine-cbc17ne4.js");
|
|
8740
8772
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
8741
8773
|
title: config.title,
|
|
8742
8774
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
@@ -8902,14 +8934,13 @@ function incrementalPreviewEnabled() {
|
|
|
8902
8934
|
return process.env.GUTTERPRESS_PREVIEW_INCREMENTAL !== "0";
|
|
8903
8935
|
}
|
|
8904
8936
|
async function renderPreviewBook(inputPath, config, opts) {
|
|
8905
|
-
const { plugins,
|
|
8937
|
+
const { plugins, pluginStyles } = await loadPluginsWithCss(config.extensions, inputPath, (ref, err) => warn(`Skipping plugin "${ref}" in preview — ${err.message}`));
|
|
8906
8938
|
return renderChapters(inputPath, {
|
|
8907
8939
|
title: config.title ?? "Document",
|
|
8908
8940
|
styles: config.styles,
|
|
8909
8941
|
files: opts.files,
|
|
8910
8942
|
plugins,
|
|
8911
|
-
|
|
8912
|
-
pluginStylePaths,
|
|
8943
|
+
pluginStyles,
|
|
8913
8944
|
wrapChapters: opts.wrapChapters,
|
|
8914
8945
|
annotateSourceChapters: true,
|
|
8915
8946
|
...opts.onCssAssets ? { onCssAssets: opts.onCssAssets } : {},
|
|
@@ -10153,11 +10184,10 @@ async function startPreviewServer(options) {
|
|
|
10153
10184
|
};
|
|
10154
10185
|
}
|
|
10155
10186
|
|
|
10156
|
-
// src/lib/
|
|
10157
|
-
import { cp as cp2, mkdir as
|
|
10158
|
-
import { existsSync as existsSync11 } from "node:fs";
|
|
10159
|
-
import
|
|
10160
|
-
import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
|
|
10187
|
+
// src/lib/project-scaffold.ts
|
|
10188
|
+
import { access, copyFile, cp as cp2, mkdir as mkdir6, readdir as readdir5, readFile as readFile25, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
|
|
10189
|
+
import { constants as FS, existsSync as existsSync11 } from "node:fs";
|
|
10190
|
+
import path15 from "node:path";
|
|
10161
10191
|
|
|
10162
10192
|
// src/lib/manifest-doc.ts
|
|
10163
10193
|
import { existsSync as existsSync10 } from "node:fs";
|
|
@@ -10212,1518 +10242,1509 @@ function ensureSeq(doc, key) {
|
|
|
10212
10242
|
return seq;
|
|
10213
10243
|
}
|
|
10214
10244
|
|
|
10215
|
-
// src/lib/
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
} from "node:fs/promises";
|
|
10228
|
-
import path15 from "node:path";
|
|
10229
|
-
import { Transform } from "node:stream";
|
|
10230
|
-
import { pipeline } from "node:stream/promises";
|
|
10231
|
-
import { createGunzip } from "node:zlib";
|
|
10232
|
-
import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
|
|
10233
|
-
|
|
10234
|
-
// src/lib/fetch-timeout.ts
|
|
10235
|
-
class FetchPassthroughError extends Error {
|
|
10236
|
-
}
|
|
10237
|
-
|
|
10238
|
-
class FriendlyHttpError extends FetchPassthroughError {
|
|
10245
|
+
// src/lib/project-scaffold.ts
|
|
10246
|
+
function starterOverridesCss() {
|
|
10247
|
+
return `/**
|
|
10248
|
+
* Your book's own styles.
|
|
10249
|
+
*
|
|
10250
|
+
* This file loads AFTER every extension, so any rule you add here overrides
|
|
10251
|
+
* a look's defaults. A new book has no look until you add one:
|
|
10252
|
+
* gutterpress ext add clean-book <book> --look (a built-in look)
|
|
10253
|
+
* gutterpress ext add <package> <book> (a look from npm)
|
|
10254
|
+
* A book has one look at a time; adding another turns the current one off.
|
|
10255
|
+
*/
|
|
10256
|
+
`;
|
|
10239
10257
|
}
|
|
10240
10258
|
|
|
10241
|
-
class
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
timeout
|
|
10248
|
-
};
|
|
10249
|
-
}
|
|
10250
|
-
async function withFetchTimeout(options, run) {
|
|
10251
|
-
const composed = composeSignal(options.signal, options.timeoutMs);
|
|
10252
|
-
try {
|
|
10253
|
-
return await run(composed.signal);
|
|
10254
|
-
} catch (cause) {
|
|
10255
|
-
if (cause instanceof FetchPassthroughError)
|
|
10256
|
-
throw cause;
|
|
10257
|
-
if (options.signal?.aborted)
|
|
10258
|
-
throw cause;
|
|
10259
|
-
const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
|
|
10260
|
-
const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
|
|
10261
|
-
if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
|
|
10262
|
-
throw cause;
|
|
10263
|
-
throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
|
|
10259
|
+
class CreateProjectErrorImpl extends Error {
|
|
10260
|
+
code;
|
|
10261
|
+
constructor(code, message) {
|
|
10262
|
+
super(message);
|
|
10263
|
+
this.name = "CreateProjectError";
|
|
10264
|
+
this.code = code;
|
|
10264
10265
|
}
|
|
10265
10266
|
}
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
var DEFAULT_LIMITS = {
|
|
10269
|
-
metadataBytes: 15 * 1024 * 1024,
|
|
10270
|
-
packageTarballBytes: 50 * 1024 * 1024,
|
|
10271
|
-
totalNetworkBytes: 250 * 1024 * 1024,
|
|
10272
|
-
packageFileBytes: 50 * 1024 * 1024,
|
|
10273
|
-
totalUnpackedBytes: 512 * 1024 * 1024,
|
|
10274
|
-
totalFiles: 50000,
|
|
10275
|
-
totalPackages: 512,
|
|
10276
|
-
dependencyDepth: 64
|
|
10277
|
-
};
|
|
10278
|
-
var METADATA_TIMEOUT_MS = 30000;
|
|
10279
|
-
var TARBALL_TIMEOUT_MS = 120000;
|
|
10280
|
-
|
|
10281
|
-
class SecurityInstallError extends FetchPassthroughError {
|
|
10282
|
-
}
|
|
10283
|
-
|
|
10284
|
-
class PackageUnavailableError extends Error {
|
|
10267
|
+
function slugifyProjectName(name) {
|
|
10268
|
+
return slugify(name);
|
|
10285
10269
|
}
|
|
10286
|
-
function
|
|
10287
|
-
return
|
|
10270
|
+
function escapeYamlScalar(value) {
|
|
10271
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
10288
10272
|
}
|
|
10289
|
-
|
|
10290
|
-
|
|
10273
|
+
var DEFAULT_AUTHOR = "Anonymous";
|
|
10274
|
+
function isPositivePoints(value) {
|
|
10275
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
10291
10276
|
}
|
|
10292
|
-
function
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
const result = {};
|
|
10297
|
-
for (const [name, selector] of Object.entries(map)) {
|
|
10298
|
-
if (typeof selector === "string" && selector.trim())
|
|
10299
|
-
result[name] = selector.trim();
|
|
10277
|
+
function requirePreset(preset, customPage) {
|
|
10278
|
+
if (!preset || !PRESET_IDS.includes(preset)) {
|
|
10279
|
+
const got = preset ? ` (got "${preset}")` : "";
|
|
10280
|
+
throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
|
|
10300
10281
|
}
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
}
|
|
10310
|
-
function mergeLimits(overrides) {
|
|
10311
|
-
const limits = { ...DEFAULT_LIMITS, ...overrides };
|
|
10312
|
-
for (const [name, value] of Object.entries(limits)) {
|
|
10313
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
10314
|
-
throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
|
|
10282
|
+
if (preset === "custom") {
|
|
10283
|
+
const missing = [];
|
|
10284
|
+
if (!customPage || !isPositivePoints(customPage.width))
|
|
10285
|
+
missing.push("page width");
|
|
10286
|
+
if (!customPage || !isPositivePoints(customPage.height))
|
|
10287
|
+
missing.push("page height");
|
|
10288
|
+
if (missing.length > 0) {
|
|
10289
|
+
throw new CreateProjectErrorImpl("custom-page-required", `The custom preset needs a trim size: ${missing.join(" and ")} in points ` + `(72pt = 1in — e.g. US Letter is 612 x 792).`);
|
|
10315
10290
|
}
|
|
10291
|
+
} else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
|
|
10292
|
+
throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
|
|
10316
10293
|
}
|
|
10317
|
-
|
|
10294
|
+
if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
|
|
10295
|
+
throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
|
|
10296
|
+
}
|
|
10297
|
+
return preset;
|
|
10318
10298
|
}
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
10299
|
+
function resolveScaffoldTargets(requested, preset) {
|
|
10300
|
+
const ids = requested ?? [...PRESETS[preset].defaultTargets];
|
|
10301
|
+
const unknown = ids.filter((id) => !TARGETS[id]);
|
|
10302
|
+
if (unknown.length > 0) {
|
|
10303
|
+
throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
|
|
10325
10304
|
}
|
|
10326
|
-
|
|
10327
|
-
|
|
10305
|
+
return [...new Set(ids)];
|
|
10306
|
+
}
|
|
10307
|
+
async function scaffoldProject(options) {
|
|
10308
|
+
const name = (options.name ?? "").trim();
|
|
10309
|
+
if (!name) {
|
|
10310
|
+
throw new CreateProjectErrorImpl("invalid-name", "A project name is required.");
|
|
10311
|
+
}
|
|
10312
|
+
const slug = slugifyProjectName(name);
|
|
10313
|
+
const folderName = (options.folderName ?? slug).trim();
|
|
10314
|
+
if (!folderName || folderName !== slugifyProjectName(folderName)) {
|
|
10315
|
+
throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
10316
|
+
}
|
|
10317
|
+
const parentDir = options.parentDir;
|
|
10318
|
+
if (!parentDir || !path15.isAbsolute(parentDir)) {
|
|
10319
|
+
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
10328
10320
|
}
|
|
10329
|
-
const reader = response.body.getReader();
|
|
10330
|
-
let total = 0;
|
|
10331
10321
|
try {
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10322
|
+
await access(parentDir, FS.W_OK);
|
|
10323
|
+
} catch {
|
|
10324
|
+
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
10325
|
+
}
|
|
10326
|
+
const projectDir = path15.join(parentDir, folderName);
|
|
10327
|
+
let targetExists = true;
|
|
10328
|
+
try {
|
|
10329
|
+
await access(projectDir, FS.F_OK);
|
|
10330
|
+
} catch {
|
|
10331
|
+
targetExists = false;
|
|
10332
|
+
}
|
|
10333
|
+
if (targetExists) {
|
|
10334
|
+
throw new CreateProjectErrorImpl("target-exists", `A folder named "${folderName}" already exists here. Choose a different name or location.`);
|
|
10335
|
+
}
|
|
10336
|
+
const template = options.template ?? "book";
|
|
10337
|
+
const customTemplateDir = options.templateDir;
|
|
10338
|
+
const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
|
|
10339
|
+
const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
|
|
10340
|
+
try {
|
|
10341
|
+
await mkdir6(projectDir, { recursive: true });
|
|
10342
|
+
if (customTemplateDir) {
|
|
10343
|
+
const entries = await readdir5(customTemplateDir, { withFileTypes: true });
|
|
10344
|
+
for (const entry of entries) {
|
|
10345
|
+
if (entry.name === ".gutterpress-template.json")
|
|
10346
|
+
continue;
|
|
10347
|
+
await cp2(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
|
|
10340
10348
|
}
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10349
|
+
await mkdir6(path15.join(projectDir, "assets"), { recursive: true });
|
|
10350
|
+
} else {
|
|
10351
|
+
const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
|
|
10352
|
+
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
10353
|
+
await mkdir6(path15.join(projectDir, "assets"), { recursive: true });
|
|
10354
|
+
await copyFile(tplManifest, path15.join(projectDir, MANIFEST_FILENAMES[0]));
|
|
10355
|
+
await copyFile(tplChapter, path15.join(projectDir, "chapter-01.md"));
|
|
10356
|
+
await mkdir6(path15.join(projectDir, "styles"), { recursive: true });
|
|
10357
|
+
await writeFile6(path15.join(projectDir, "styles", "book.css"), starterOverridesCss(), "utf8");
|
|
10358
|
+
}
|
|
10359
|
+
} catch (e) {
|
|
10360
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
10361
|
+
}
|
|
10362
|
+
try {
|
|
10363
|
+
await ensureGitignoreHasDist(projectDir);
|
|
10364
|
+
} catch (e) {
|
|
10365
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
|
|
10366
|
+
}
|
|
10367
|
+
const manifestPath = MANIFEST_FILENAMES.map((name2) => path15.join(projectDir, name2)).find(existsSync11) ?? path15.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
10368
|
+
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
10369
|
+
const substitutions = {
|
|
10370
|
+
"{{TITLE}}": escapeYamlScalar(name),
|
|
10371
|
+
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
10372
|
+
};
|
|
10373
|
+
let openFile = path15.join(projectDir, "chapter-01.md");
|
|
10374
|
+
try {
|
|
10375
|
+
await fillTemplateFile(manifestPath, substitutions);
|
|
10376
|
+
if (preset) {
|
|
10377
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
10378
|
+
doc.set("preset", preset);
|
|
10379
|
+
doc.set("targets", doc.createNode(targets ?? []));
|
|
10380
|
+
if (options.customPage) {
|
|
10381
|
+
const page = {
|
|
10382
|
+
width: options.customPage.width,
|
|
10383
|
+
height: options.customPage.height
|
|
10384
|
+
};
|
|
10385
|
+
if (options.customPage.tolerance !== undefined) {
|
|
10386
|
+
page.tolerance = options.customPage.tolerance;
|
|
10387
|
+
}
|
|
10388
|
+
doc.set("page", doc.createNode(page));
|
|
10344
10389
|
}
|
|
10345
|
-
|
|
10346
|
-
await onChunk(value);
|
|
10390
|
+
await writeManifestDoc(file, doc);
|
|
10347
10391
|
}
|
|
10348
|
-
|
|
10349
|
-
|
|
10392
|
+
const firstSource = await firstSourceFile(manifestPath);
|
|
10393
|
+
if (firstSource)
|
|
10394
|
+
openFile = path15.join(projectDir, firstSource);
|
|
10395
|
+
await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
|
|
10396
|
+
} catch (e) {
|
|
10397
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
10350
10398
|
}
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
if (pending)
|
|
10356
|
-
return pending;
|
|
10357
|
-
pending = withFetchTimeout({
|
|
10358
|
-
timeoutMs: METADATA_TIMEOUT_MS,
|
|
10359
|
-
signal: ctx.signal,
|
|
10360
|
-
timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
|
|
10361
|
-
offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
10362
|
-
}, async (signal) => {
|
|
10363
|
-
const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
|
|
10364
|
-
signal,
|
|
10365
|
-
redirect: "error",
|
|
10366
|
-
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
10367
|
-
});
|
|
10368
|
-
if (response.status === 404) {
|
|
10369
|
-
throw new FriendlyHttpError(`npm package "${name}" was not found.`);
|
|
10370
|
-
}
|
|
10371
|
-
if (!response.ok) {
|
|
10372
|
-
throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
|
|
10373
|
-
}
|
|
10374
|
-
const chunks = [];
|
|
10375
|
-
const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
|
|
10376
|
-
chunks.push(chunk2);
|
|
10377
|
-
});
|
|
10378
|
-
const bytes = new Uint8Array(size);
|
|
10379
|
-
let offset = 0;
|
|
10380
|
-
for (const chunk2 of chunks) {
|
|
10381
|
-
bytes.set(chunk2, offset);
|
|
10382
|
-
offset += chunk2.length;
|
|
10383
|
-
}
|
|
10384
|
-
let metadata = null;
|
|
10399
|
+
const requested = options.versionHistory ?? "local-git";
|
|
10400
|
+
let versionHistory = "none";
|
|
10401
|
+
let versionHistoryError;
|
|
10402
|
+
if (requested === "local-git") {
|
|
10385
10403
|
try {
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10404
|
+
const { providerFor } = await import("./source-provider-51g6mt6y.js");
|
|
10405
|
+
const provider = providerFor({ type: "local-folder", path: projectDir });
|
|
10406
|
+
await provider.initVersionHistory({
|
|
10407
|
+
projectDir,
|
|
10408
|
+
authorName: options.author?.trim() || undefined,
|
|
10409
|
+
initialMessage: "Created project"
|
|
10410
|
+
});
|
|
10411
|
+
versionHistory = "local-git";
|
|
10412
|
+
} catch (e) {
|
|
10413
|
+
versionHistory = "none";
|
|
10414
|
+
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
10415
|
+
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
10416
|
+
versionHistoryError = enclosingRepoDir ? `This book was added to ${path15.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
|
|
10390
10417
|
}
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10418
|
+
}
|
|
10419
|
+
const result = {
|
|
10420
|
+
projectDir,
|
|
10421
|
+
manifestPath,
|
|
10422
|
+
openFile,
|
|
10423
|
+
versionHistory
|
|
10424
|
+
};
|
|
10425
|
+
if (versionHistoryError !== undefined) {
|
|
10426
|
+
result.versionHistoryError = versionHistoryError;
|
|
10427
|
+
}
|
|
10428
|
+
return result;
|
|
10398
10429
|
}
|
|
10399
|
-
function
|
|
10400
|
-
const
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
}
|
|
10430
|
+
async function ensureGitignoreHasDist(projectDir) {
|
|
10431
|
+
const gitignorePath = path15.join(projectDir, ".gitignore");
|
|
10432
|
+
let existing = null;
|
|
10433
|
+
try {
|
|
10434
|
+
existing = await readFile25(gitignorePath, "utf8");
|
|
10435
|
+
} catch {}
|
|
10436
|
+
if (existing === null) {
|
|
10437
|
+
await writeFile6(gitignorePath, `dist/
|
|
10438
|
+
`, "utf8");
|
|
10439
|
+
return;
|
|
10410
10440
|
}
|
|
10411
|
-
const
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10441
|
+
const alreadyIgnoresDist = existing.split(`
|
|
10442
|
+
`).some((line) => /^\/?dist\/?$/.test(line.trim()));
|
|
10443
|
+
if (alreadyIgnoresDist)
|
|
10444
|
+
return;
|
|
10445
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
|
|
10446
|
+
`);
|
|
10447
|
+
await writeFile6(gitignorePath, existing + (needsLeadingNewline ? `
|
|
10448
|
+
` : "") + `dist/
|
|
10449
|
+
`, "utf8");
|
|
10450
|
+
}
|
|
10451
|
+
async function firstSourceFile(manifestPath) {
|
|
10452
|
+
let text;
|
|
10453
|
+
try {
|
|
10454
|
+
text = await readFile25(manifestPath, "utf8");
|
|
10455
|
+
} catch {
|
|
10456
|
+
return;
|
|
10419
10457
|
}
|
|
10420
|
-
|
|
10421
|
-
|
|
10458
|
+
const filesIdx = text.search(/^\s*files:\s*$/m);
|
|
10459
|
+
if (filesIdx === -1)
|
|
10460
|
+
return;
|
|
10461
|
+
const rest = text.slice(filesIdx);
|
|
10462
|
+
const m = rest.match(/^\s*-\s*"?([^"\n]+?)"?\s*$/m);
|
|
10463
|
+
return m ? m[1].trim() : undefined;
|
|
10464
|
+
}
|
|
10465
|
+
async function fillTemplateFile(filePath, substitutions) {
|
|
10466
|
+
let text = await readFile25(filePath, "utf8");
|
|
10467
|
+
for (const [token, value] of Object.entries(substitutions)) {
|
|
10468
|
+
text = text.split(token).join(value);
|
|
10422
10469
|
}
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10470
|
+
await writeFile6(filePath, text, "utf8");
|
|
10471
|
+
}
|
|
10472
|
+
|
|
10473
|
+
// src/lib/extension-scaffold.ts
|
|
10474
|
+
import { access as access2, mkdir as mkdir7, readFile as readFile26, writeFile as writeFile7 } from "node:fs/promises";
|
|
10475
|
+
import { constants as FS2 } from "node:fs";
|
|
10476
|
+
import path16 from "node:path";
|
|
10477
|
+
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
10478
|
+
var RESERVED_PREFIX = "gp-";
|
|
10479
|
+
var TEMPLATE_FILES = {
|
|
10480
|
+
plugin: [
|
|
10481
|
+
["package.json", "package.json"],
|
|
10482
|
+
["README.md", "README.md"],
|
|
10483
|
+
["plugin.js.tpl", "plugin.js"],
|
|
10484
|
+
["styles/plugin.css", "styles/plugin.css"],
|
|
10485
|
+
["snippets/term-box.md", "snippets/term-box.md"],
|
|
10486
|
+
["test/fixture.md", "test/fixture.md"],
|
|
10487
|
+
["test/expected.html", "test/expected.html"],
|
|
10488
|
+
["test/plugin.test.js.tpl", "test/plugin.test.js"]
|
|
10489
|
+
],
|
|
10490
|
+
theme: [
|
|
10491
|
+
["package.json", "package.json"],
|
|
10492
|
+
["README.md", "README.md"],
|
|
10493
|
+
["components.yaml", "components.yaml"],
|
|
10494
|
+
["snippets/callout.md", "snippets/callout.md"],
|
|
10495
|
+
["styles/tokens.css", "styles/tokens.css"],
|
|
10496
|
+
["styles/base.css", "styles/base.css"],
|
|
10497
|
+
["styles/components.css", "styles/components.css"],
|
|
10498
|
+
["styles/page-templates.css", "styles/page-templates.css"],
|
|
10499
|
+
["styles/page-rules.css", "styles/page-rules.css"],
|
|
10500
|
+
["styles/book.css", "styles/book.css"]
|
|
10501
|
+
]
|
|
10502
|
+
};
|
|
10503
|
+
var OPEN_FILE = {
|
|
10504
|
+
plugin: "plugin.js",
|
|
10505
|
+
theme: "styles/tokens.css"
|
|
10506
|
+
};
|
|
10507
|
+
var DEFAULT_DESCRIPTION = {
|
|
10508
|
+
plugin: "A Gutterpress plugin.",
|
|
10509
|
+
theme: "A Gutterpress theme."
|
|
10510
|
+
};
|
|
10511
|
+
var DEFAULT_AUTHOR2 = "Anonymous";
|
|
10512
|
+
|
|
10513
|
+
class ExtensionScaffoldError extends Error {
|
|
10514
|
+
code;
|
|
10515
|
+
constructor(code, message) {
|
|
10516
|
+
super(message);
|
|
10517
|
+
this.name = "CreateProjectError";
|
|
10518
|
+
this.code = code;
|
|
10431
10519
|
}
|
|
10432
|
-
throw new SecurityInstallError("The npm registry provided no usable integrity hash, so the package was not installed.");
|
|
10433
10520
|
}
|
|
10434
|
-
function
|
|
10435
|
-
|
|
10521
|
+
function resolveExtensionPrefix(slug, requested) {
|
|
10522
|
+
const raw = (requested ?? `${slug}-`).trim();
|
|
10523
|
+
const prefix = raw.endsWith("-") ? raw : `${raw}-`;
|
|
10524
|
+
if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
|
|
10525
|
+
throw new ExtensionScaffoldError("invalid-name", `"${raw}" is not a usable prefix. Use lower-case letters, digits and hyphens, ` + `starting with a letter — for example "${slug}-" or "fn-".`);
|
|
10526
|
+
}
|
|
10527
|
+
if (prefix === RESERVED_PREFIX) {
|
|
10528
|
+
throw new ExtensionScaffoldError("invalid-name", `"${RESERVED_PREFIX}" is reserved for Gutterpress core's own classes and custom ` + `properties. Taking it would silently override them — choose your own prefix.`);
|
|
10529
|
+
}
|
|
10530
|
+
return prefix;
|
|
10436
10531
|
}
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10532
|
+
function identifierFor(slug) {
|
|
10533
|
+
const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
10534
|
+
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
10535
|
+
}
|
|
10536
|
+
function escapeJsonScalar(value) {
|
|
10537
|
+
return JSON.stringify(value).slice(1, -1);
|
|
10538
|
+
}
|
|
10539
|
+
async function scaffoldExtension(options) {
|
|
10540
|
+
const kind = options.kind;
|
|
10541
|
+
if (kind !== "plugin" && kind !== "theme") {
|
|
10542
|
+
throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
|
|
10440
10543
|
}
|
|
10441
|
-
const
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
if (!versions || !tags)
|
|
10445
|
-
throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
|
|
10446
|
-
let version;
|
|
10447
|
-
if (!selector) {
|
|
10448
|
-
if (typeof tags.latest === "string")
|
|
10449
|
-
version = tags.latest;
|
|
10450
|
-
} else if (object3(versions[selector])) {
|
|
10451
|
-
version = selector;
|
|
10452
|
-
} else if (typeof tags[selector] === "string") {
|
|
10453
|
-
version = tags[selector];
|
|
10454
|
-
} else if (validRange2(selector)) {
|
|
10455
|
-
version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
|
|
10544
|
+
const name = (options.name ?? "").trim();
|
|
10545
|
+
if (!name) {
|
|
10546
|
+
throw new ExtensionScaffoldError("invalid-name", "A name is required.");
|
|
10456
10547
|
}
|
|
10457
|
-
|
|
10458
|
-
|
|
10548
|
+
const slug = (options.folderName ?? slugify(name)).trim();
|
|
10549
|
+
if (!slug || slug !== slugify(slug)) {
|
|
10550
|
+
throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
10459
10551
|
}
|
|
10460
|
-
const
|
|
10461
|
-
|
|
10462
|
-
|
|
10552
|
+
const prefix = resolveExtensionPrefix(slug, options.prefix);
|
|
10553
|
+
const parentDir = options.parentDir;
|
|
10554
|
+
if (!parentDir || !path16.isAbsolute(parentDir)) {
|
|
10555
|
+
throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
|
|
10463
10556
|
}
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10557
|
+
try {
|
|
10558
|
+
await access2(parentDir, FS2.W_OK);
|
|
10559
|
+
} catch {
|
|
10560
|
+
throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
10467
10561
|
}
|
|
10468
|
-
|
|
10562
|
+
const extensionDir = path16.join(parentDir, slug);
|
|
10563
|
+
let targetExists = true;
|
|
10469
10564
|
try {
|
|
10470
|
-
|
|
10565
|
+
await access2(extensionDir, FS2.F_OK);
|
|
10471
10566
|
} catch {
|
|
10472
|
-
|
|
10567
|
+
targetExists = false;
|
|
10473
10568
|
}
|
|
10474
|
-
if (
|
|
10475
|
-
throw new
|
|
10569
|
+
if (targetExists) {
|
|
10570
|
+
throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
|
|
10476
10571
|
}
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
if (!response.ok) {
|
|
10491
|
-
throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
|
|
10492
|
-
}
|
|
10493
|
-
await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
|
|
10494
|
-
hash.update(chunk2);
|
|
10495
|
-
try {
|
|
10496
|
-
await file.write(chunk2);
|
|
10497
|
-
} catch (cause) {
|
|
10498
|
-
throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
|
|
10499
|
-
}
|
|
10500
|
-
});
|
|
10501
|
-
});
|
|
10502
|
-
} finally {
|
|
10503
|
-
await file.close();
|
|
10504
|
-
}
|
|
10505
|
-
const actual = hash.digest();
|
|
10506
|
-
if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
|
|
10507
|
-
throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
|
|
10508
|
-
}
|
|
10509
|
-
}
|
|
10510
|
-
function safeTarRelative(entryPath, packageName) {
|
|
10511
|
-
if (!entryPath || entryPath.includes("\\") || entryPath.startsWith("/") || /^[a-zA-Z]:/.test(entryPath)) {
|
|
10512
|
-
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath || "(empty)"}`);
|
|
10513
|
-
}
|
|
10514
|
-
const parts = entryPath.split("/");
|
|
10515
|
-
if (parts.at(-1) === "")
|
|
10516
|
-
parts.pop();
|
|
10517
|
-
const legacyTypesRoot = packageName.startsWith("@types/") ? packageName.slice("@types/".length) : null;
|
|
10518
|
-
if (parts.some((part) => !part || part === "." || part === "..") || parts[0] !== "package" && parts[0] !== legacyTypesRoot) {
|
|
10519
|
-
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath}`);
|
|
10520
|
-
}
|
|
10521
|
-
const relative2 = parts.slice(1).join("/");
|
|
10522
|
-
if (!relative2)
|
|
10523
|
-
return { relative: relative2, key: "" };
|
|
10524
|
-
assertWindowsSafeRelativePath(relative2);
|
|
10525
|
-
if (relative2.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
|
|
10526
|
-
throw new SecurityInstallError(`The npm tarball contains bundled node_modules (${entryPath}); bundled dependencies are not accepted.`);
|
|
10527
|
-
}
|
|
10528
|
-
return { relative: relative2, key: windowsPathKey(relative2) };
|
|
10529
|
-
}
|
|
10530
|
-
async function extractTarball(ctx, archivePath, packageDir, packageName) {
|
|
10531
|
-
const { extract } = await import("tar");
|
|
10532
|
-
let rejected = null;
|
|
10533
|
-
const seen = new Set;
|
|
10534
|
-
const metadataTypes = new Set([
|
|
10535
|
-
"GlobalExtendedHeader",
|
|
10536
|
-
"ExtendedHeader",
|
|
10537
|
-
"NextFileHasLongLinkpath",
|
|
10538
|
-
"NextFileHasLongPath",
|
|
10539
|
-
"OldGnuLongPath",
|
|
10540
|
-
"OldExtendedHeader"
|
|
10541
|
-
]);
|
|
10542
|
-
await mkdir6(packageDir, { recursive: true });
|
|
10543
|
-
const unpack = extract({
|
|
10544
|
-
cwd: packageDir,
|
|
10545
|
-
strip: 1,
|
|
10546
|
-
strict: true,
|
|
10547
|
-
preservePaths: false,
|
|
10548
|
-
preserveOwner: false,
|
|
10549
|
-
noMtime: true,
|
|
10550
|
-
chmod: false,
|
|
10551
|
-
maxDepth: ctx.limits.dependencyDepth * 4,
|
|
10552
|
-
filter: (entryPath, rawEntry) => {
|
|
10553
|
-
if (rejected)
|
|
10554
|
-
return false;
|
|
10555
|
-
const entry = rawEntry;
|
|
10556
|
-
if (metadataTypes.has(entry.type))
|
|
10557
|
-
return true;
|
|
10558
|
-
if (entry.type === "Link" || entry.type === "SymbolicLink") {
|
|
10559
|
-
rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
|
|
10560
|
-
return false;
|
|
10561
|
-
}
|
|
10562
|
-
if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
|
|
10563
|
-
rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
|
|
10564
|
-
return false;
|
|
10565
|
-
}
|
|
10566
|
-
try {
|
|
10567
|
-
const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
|
|
10568
|
-
if (!relative2) {
|
|
10569
|
-
if (entry.type === "Directory")
|
|
10570
|
-
return true;
|
|
10571
|
-
throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
|
|
10572
|
-
}
|
|
10573
|
-
if (seen.has(key)) {
|
|
10574
|
-
throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
|
|
10575
|
-
}
|
|
10576
|
-
seen.add(key);
|
|
10577
|
-
ctx.totalFiles++;
|
|
10578
|
-
if (ctx.totalFiles > ctx.limits.totalFiles) {
|
|
10579
|
-
throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
|
|
10580
|
-
}
|
|
10581
|
-
if (entry.type !== "Directory") {
|
|
10582
|
-
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
|
|
10583
|
-
throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
|
|
10584
|
-
}
|
|
10585
|
-
}
|
|
10586
|
-
return true;
|
|
10587
|
-
} catch (error2) {
|
|
10588
|
-
rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
|
|
10589
|
-
return false;
|
|
10590
|
-
}
|
|
10591
|
-
}
|
|
10592
|
-
});
|
|
10593
|
-
const limiter = new Transform({
|
|
10594
|
-
transform(chunk2, _encoding, callback) {
|
|
10595
|
-
ctx.totalUnpackedBytes += chunk2.length;
|
|
10596
|
-
if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
|
|
10597
|
-
callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
|
|
10598
|
-
} else
|
|
10599
|
-
callback(null, chunk2);
|
|
10600
|
-
}
|
|
10601
|
-
});
|
|
10602
|
-
try {
|
|
10603
|
-
await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
|
|
10604
|
-
} catch (error2) {
|
|
10605
|
-
if (rejected)
|
|
10606
|
-
throw rejected;
|
|
10607
|
-
if (error2 instanceof SecurityInstallError)
|
|
10608
|
-
throw error2;
|
|
10609
|
-
throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
|
|
10610
|
-
}
|
|
10611
|
-
if (rejected)
|
|
10612
|
-
throw rejected;
|
|
10613
|
-
}
|
|
10614
|
-
async function readInstalledManifest(packageDir, selected) {
|
|
10615
|
-
const packageJsonPath = path15.join(packageDir, "package.json");
|
|
10616
|
-
let manifest = null;
|
|
10617
|
-
try {
|
|
10618
|
-
const info2 = await lstat2(packageJsonPath);
|
|
10619
|
-
if (!info2.isFile() || info2.isSymbolicLink())
|
|
10620
|
-
throw new Error("not a regular file");
|
|
10621
|
-
manifest = object3(JSON.parse(await readFile25(packageJsonPath, "utf8")));
|
|
10622
|
-
} catch (error2) {
|
|
10623
|
-
throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
|
|
10624
|
-
}
|
|
10625
|
-
if (manifest?.name !== selected.name || manifest.version !== selected.version) {
|
|
10626
|
-
throw new SecurityInstallError(`Downloaded ${String(manifest?.name)}@${String(manifest?.version)} instead of ${selected.name}@${selected.version}.`);
|
|
10627
|
-
}
|
|
10628
|
-
return manifest;
|
|
10629
|
-
}
|
|
10630
|
-
async function optionalPackageEntry(packageDir, manifest, condition) {
|
|
10631
|
-
try {
|
|
10632
|
-
return await resolvePackageEntry(packageDir, manifest, condition);
|
|
10633
|
-
} catch (error2) {
|
|
10634
|
-
if (error2 instanceof Error && error2.message.includes("no contained JavaScript entry point")) {
|
|
10635
|
-
return;
|
|
10636
|
-
}
|
|
10637
|
-
throw error2;
|
|
10638
|
-
}
|
|
10639
|
-
}
|
|
10640
|
-
function platformAllowed(rule, current) {
|
|
10641
|
-
let values;
|
|
10642
|
-
if (typeof rule === "string") {
|
|
10643
|
-
values = [rule];
|
|
10644
|
-
} else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
|
|
10645
|
-
values = rule;
|
|
10646
|
-
} else {
|
|
10647
|
-
return true;
|
|
10648
|
-
}
|
|
10649
|
-
if (values.length === 0)
|
|
10650
|
-
return true;
|
|
10651
|
-
if (values.length === 1 && values[0] === "any")
|
|
10652
|
-
return true;
|
|
10653
|
-
if (values.includes(`!${current}`))
|
|
10654
|
-
return false;
|
|
10655
|
-
const positives = values.filter((value) => !value.startsWith("!"));
|
|
10656
|
-
return positives.length === 0 || positives.includes(current);
|
|
10657
|
-
}
|
|
10658
|
-
function relativePackagePath(parentPackagePath, name) {
|
|
10659
|
-
const segments = name.split("/");
|
|
10660
|
-
return parentPackagePath ? `${parentPackagePath}/node_modules/${segments.join("/")}` : `node_modules/${segments.join("/")}`;
|
|
10661
|
-
}
|
|
10662
|
-
async function installPackage(ctx, name, selector, parentPackagePath, ancestors, depth, optional) {
|
|
10663
|
-
const recordStart = ctx.records.length;
|
|
10664
|
-
const skippedStart = ctx.skipped.length;
|
|
10665
|
-
const warningsBefore = new Set(ctx.warnings);
|
|
10666
|
-
const discardOptionalSubtree = () => {
|
|
10667
|
-
ctx.records.splice(recordStart);
|
|
10668
|
-
ctx.skipped.splice(skippedStart);
|
|
10669
|
-
ctx.warnings = warningsBefore;
|
|
10670
|
-
};
|
|
10671
|
-
if (depth > ctx.limits.dependencyDepth) {
|
|
10672
|
-
throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
|
|
10673
|
-
}
|
|
10674
|
-
let selected;
|
|
10675
|
-
try {
|
|
10676
|
-
selected = await selectPackage(ctx, name, selector);
|
|
10677
|
-
} catch (error2) {
|
|
10678
|
-
if (optional && isOptionalAvailabilityError(error2)) {
|
|
10679
|
-
discardOptionalSubtree();
|
|
10680
|
-
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
10681
|
-
}
|
|
10682
|
-
throw error2;
|
|
10683
|
-
}
|
|
10684
|
-
const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
|
|
10685
|
-
if (ancestor)
|
|
10686
|
-
return { path: ancestor.path };
|
|
10687
|
-
if (ctx.records.length >= ctx.limits.totalPackages) {
|
|
10688
|
-
throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
|
|
10689
|
-
}
|
|
10690
|
-
const relativePath = relativePackagePath(parentPackagePath, name);
|
|
10691
|
-
const packageDir = path15.join(ctx.stageRoot, ...relativePath.split("/"));
|
|
10692
|
-
const archivePath = path15.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
|
|
10693
|
-
try {
|
|
10694
|
-
await mkdir6(path15.dirname(packageDir), { recursive: true });
|
|
10695
|
-
await downloadTarball(ctx, selected, archivePath);
|
|
10696
|
-
await extractTarball(ctx, archivePath, packageDir, selected.name);
|
|
10697
|
-
const manifest = await readInstalledManifest(packageDir, selected);
|
|
10698
|
-
if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
|
|
10699
|
-
const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
|
|
10700
|
-
if (optional) {
|
|
10701
|
-
await rm3(packageDir, { recursive: true, force: true });
|
|
10702
|
-
return { skipped: reason };
|
|
10703
|
-
}
|
|
10704
|
-
throw new PackageUnavailableError(reason);
|
|
10705
|
-
}
|
|
10706
|
-
const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
|
|
10707
|
-
const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
|
|
10708
|
-
if (parentPackagePath === null && !importEntry && !requireEntry) {
|
|
10709
|
-
throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
|
|
10710
|
-
}
|
|
10711
|
-
const record = {
|
|
10712
|
-
path: relativePath,
|
|
10713
|
-
name,
|
|
10714
|
-
version: selected.version,
|
|
10715
|
-
tarball: selected.tarball,
|
|
10716
|
-
integrity: selected.integrity.label,
|
|
10717
|
-
legacySha1: selected.integrity.legacySha1,
|
|
10718
|
-
...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
|
|
10719
|
-
...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
|
|
10720
|
-
dependencies: {},
|
|
10721
|
-
packageDir,
|
|
10722
|
-
manifest
|
|
10723
|
-
};
|
|
10724
|
-
ctx.records.push(record);
|
|
10725
|
-
if (selected.integrity.legacySha1) {
|
|
10726
|
-
ctx.warnings.add(`${name}@${selected.version} is an old npm release with only SHA-1 registry integrity. It was accepted for compatibility and recorded as legacy SHA-1 in the vendor receipt.`);
|
|
10727
|
-
}
|
|
10728
|
-
const dependencies = stringMap2(manifest.dependencies);
|
|
10729
|
-
const optionalDependencies = stringMap2(manifest.optionalDependencies);
|
|
10730
|
-
for (const optionalName of Object.keys(optionalDependencies))
|
|
10731
|
-
delete dependencies[optionalName];
|
|
10732
|
-
const peerDependencies = stringMap2(manifest.peerDependencies);
|
|
10733
|
-
const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
|
|
10734
|
-
for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
|
|
10735
|
-
delete peerDependencies[existing];
|
|
10736
|
-
}
|
|
10737
|
-
const nextAncestors = [...ancestors, record];
|
|
10738
|
-
const requiredEntries = [
|
|
10739
|
-
...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
|
|
10740
|
-
...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
|
|
10741
|
-
].sort((a, b) => a.dependency.localeCompare(b.dependency));
|
|
10742
|
-
for (const { dependency, range } of requiredEntries) {
|
|
10743
|
-
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
|
|
10744
|
-
if (!child.path)
|
|
10745
|
-
throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
|
|
10746
|
-
record.dependencies[dependency] = child.path;
|
|
10747
|
-
}
|
|
10748
|
-
const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
|
|
10749
|
-
for (const [dependency, range] of optionalEntries) {
|
|
10750
|
-
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
|
|
10751
|
-
if (child.path)
|
|
10752
|
-
record.dependencies[dependency] = child.path;
|
|
10753
|
-
else if (child.skipped) {
|
|
10754
|
-
ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
|
|
10755
|
-
}
|
|
10756
|
-
}
|
|
10757
|
-
for (const [dependency, range] of Object.entries(peerDependencies)) {
|
|
10758
|
-
if (object3(peerMeta[dependency])?.optional === true) {
|
|
10759
|
-
ctx.skipped.push({
|
|
10760
|
-
from: relativePath,
|
|
10761
|
-
name: dependency,
|
|
10762
|
-
selector: range,
|
|
10763
|
-
kind: "optional-peer",
|
|
10764
|
-
reason: "Optional peer dependencies are not auto-installed."
|
|
10765
|
-
});
|
|
10766
|
-
}
|
|
10767
|
-
}
|
|
10768
|
-
return { path: relativePath };
|
|
10769
|
-
} catch (error2) {
|
|
10770
|
-
await rm3(packageDir, { recursive: true, force: true });
|
|
10771
|
-
if (optional && isOptionalAvailabilityError(error2)) {
|
|
10772
|
-
discardOptionalSubtree();
|
|
10773
|
-
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
10774
|
-
}
|
|
10775
|
-
throw error2;
|
|
10776
|
-
} finally {
|
|
10777
|
-
await rm3(archivePath, { force: true });
|
|
10778
|
-
}
|
|
10779
|
-
}
|
|
10780
|
-
async function ensureDirectory(pathname) {
|
|
10572
|
+
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
|
|
10573
|
+
const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
|
|
10574
|
+
const plain = {
|
|
10575
|
+
"{{NAME}}": name,
|
|
10576
|
+
"{{SLUG}}": slug,
|
|
10577
|
+
"{{PREFIX}}": prefix,
|
|
10578
|
+
"{{AUTHOR}}": author,
|
|
10579
|
+
"{{DESCRIPTION}}": description,
|
|
10580
|
+
"{{IDENT}}": identifierFor(slug)
|
|
10581
|
+
};
|
|
10582
|
+
const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
|
|
10583
|
+
const entries = TEMPLATE_FILES[kind];
|
|
10584
|
+
const written = [];
|
|
10781
10585
|
try {
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10586
|
+
await mkdir7(extensionDir, { recursive: true });
|
|
10587
|
+
for (const [assetName, relDest] of entries) {
|
|
10588
|
+
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
10589
|
+
const dest = path16.join(extensionDir, relDest);
|
|
10590
|
+
await mkdir7(path16.dirname(dest), { recursive: true });
|
|
10591
|
+
const text = await readFile26(src, "utf8");
|
|
10592
|
+
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
10593
|
+
await writeFile7(dest, substitute(text, table), "utf8");
|
|
10594
|
+
written.push(relDest);
|
|
10785
10595
|
}
|
|
10786
|
-
} catch (
|
|
10787
|
-
|
|
10788
|
-
throw error2;
|
|
10789
|
-
await mkdir6(pathname);
|
|
10596
|
+
} catch (e) {
|
|
10597
|
+
throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
|
|
10790
10598
|
}
|
|
10599
|
+
return {
|
|
10600
|
+
extensionDir,
|
|
10601
|
+
manifestPath: path16.join(extensionDir, "package.json"),
|
|
10602
|
+
kind,
|
|
10603
|
+
slug,
|
|
10604
|
+
prefix,
|
|
10605
|
+
openFile: path16.join(extensionDir, OPEN_FILE[kind]),
|
|
10606
|
+
files: written
|
|
10607
|
+
};
|
|
10791
10608
|
}
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
const
|
|
10795
|
-
|
|
10796
|
-
await mkdir6(projectDir, { recursive: true });
|
|
10797
|
-
await ensureDirectory(plugins);
|
|
10798
|
-
await ensureDirectory(npm);
|
|
10799
|
-
await ensureDirectory(packageParent);
|
|
10800
|
-
const [realNpm, realPackageParent] = await Promise.all([
|
|
10801
|
-
realpath2(npm),
|
|
10802
|
-
realpath2(packageParent)
|
|
10803
|
-
]);
|
|
10804
|
-
if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
|
|
10805
|
-
throw new Error("Plugin install path resolves outside the project.");
|
|
10609
|
+
function substitute(text, table) {
|
|
10610
|
+
let out = text;
|
|
10611
|
+
for (const [token, value] of Object.entries(table)) {
|
|
10612
|
+
out = out.split(token).join(value);
|
|
10806
10613
|
}
|
|
10807
|
-
return
|
|
10614
|
+
return out;
|
|
10808
10615
|
}
|
|
10809
|
-
|
|
10616
|
+
|
|
10617
|
+
// src/lib/project-templates.ts
|
|
10618
|
+
import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
|
|
10619
|
+
var BUILT_IN_TEMPLATE_IDS = [
|
|
10620
|
+
"book",
|
|
10621
|
+
"zine",
|
|
10622
|
+
"technical"
|
|
10623
|
+
];
|
|
10624
|
+
var SKIP_ENTRIES = new Set([
|
|
10625
|
+
".git",
|
|
10626
|
+
"node_modules",
|
|
10627
|
+
"out",
|
|
10628
|
+
"dist",
|
|
10629
|
+
"build",
|
|
10630
|
+
".gutterpress",
|
|
10631
|
+
".DS_Store"
|
|
10632
|
+
]);
|
|
10633
|
+
|
|
10634
|
+
// src/lib/extension-manager.ts
|
|
10635
|
+
import { cp as cp3, mkdir as mkdir9, readFile as readFile28, rm as rm4, stat as stat6 } from "node:fs/promises";
|
|
10636
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
10637
|
+
import path18 from "node:path";
|
|
10638
|
+
import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
|
|
10639
|
+
|
|
10640
|
+
// src/lib/npm-plugin-installer.ts
|
|
10641
|
+
import { createHash as createHash7, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
10642
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
10643
|
+
import {
|
|
10644
|
+
lstat as lstat2,
|
|
10645
|
+
mkdir as mkdir8,
|
|
10646
|
+
mkdtemp as mkdtemp2,
|
|
10647
|
+
open as open2,
|
|
10648
|
+
readFile as readFile27,
|
|
10649
|
+
realpath as realpath2,
|
|
10650
|
+
rename as rename2,
|
|
10651
|
+
rm as rm3
|
|
10652
|
+
} from "node:fs/promises";
|
|
10653
|
+
import path17 from "node:path";
|
|
10654
|
+
import { Transform } from "node:stream";
|
|
10655
|
+
import { pipeline } from "node:stream/promises";
|
|
10656
|
+
import { createGunzip } from "node:zlib";
|
|
10657
|
+
import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
|
|
10658
|
+
|
|
10659
|
+
// src/lib/fetch-timeout.ts
|
|
10660
|
+
class FetchPassthroughError extends Error {
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
class FriendlyHttpError extends FetchPassthroughError {
|
|
10664
|
+
}
|
|
10665
|
+
|
|
10666
|
+
class FetchUnavailableError extends Error {
|
|
10667
|
+
}
|
|
10668
|
+
function composeSignal(signal, timeoutMs) {
|
|
10669
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
10810
10670
|
return {
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
version: record.version,
|
|
10814
|
-
tarball: record.tarball,
|
|
10815
|
-
integrity: record.integrity,
|
|
10816
|
-
legacySha1: record.legacySha1,
|
|
10817
|
-
...record.entry ? { entry: record.entry } : {},
|
|
10818
|
-
...record.requireEntry ? { requireEntry: record.requireEntry } : {},
|
|
10819
|
-
dependencies: record.dependencies
|
|
10671
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
10672
|
+
timeout
|
|
10820
10673
|
};
|
|
10821
10674
|
}
|
|
10822
|
-
async function
|
|
10823
|
-
const
|
|
10824
|
-
await mkdir6(requestedProjectDir, { recursive: true });
|
|
10825
|
-
const projectDir = await realpath2(requestedProjectDir);
|
|
10826
|
-
const requested = parseNpmPluginSpec(packageSpec);
|
|
10827
|
-
const npmRoot = await prepareVendorParent(projectDir, requested.name);
|
|
10828
|
-
const container = await mkdtemp2(path15.join(npmRoot, ".install-"));
|
|
10829
|
-
const stageRoot = path15.join(container, "root");
|
|
10830
|
-
const downloadsDir = path15.join(container, "downloads");
|
|
10831
|
-
await Promise.all([mkdir6(stageRoot), mkdir6(downloadsDir)]);
|
|
10832
|
-
const ctx = {
|
|
10833
|
-
fetch: options.fetch ?? globalThis.fetch,
|
|
10834
|
-
signal: options.signal,
|
|
10835
|
-
limits: mergeLimits(options.limits),
|
|
10836
|
-
stageRoot,
|
|
10837
|
-
downloadsDir,
|
|
10838
|
-
packuments: new Map,
|
|
10839
|
-
records: [],
|
|
10840
|
-
skipped: [],
|
|
10841
|
-
warnings: new Set,
|
|
10842
|
-
totalNetworkBytes: 0,
|
|
10843
|
-
totalUnpackedBytes: 0,
|
|
10844
|
-
totalFiles: 0,
|
|
10845
|
-
archiveCounter: 0
|
|
10846
|
-
};
|
|
10847
|
-
let finalRoot = "";
|
|
10848
|
-
let backupRoot = null;
|
|
10675
|
+
async function withFetchTimeout(options, run) {
|
|
10676
|
+
const composed = composeSignal(options.signal, options.timeoutMs);
|
|
10849
10677
|
try {
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
if (
|
|
10855
|
-
throw
|
|
10856
|
-
const
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10678
|
+
return await run(composed.signal);
|
|
10679
|
+
} catch (cause) {
|
|
10680
|
+
if (cause instanceof FetchPassthroughError)
|
|
10681
|
+
throw cause;
|
|
10682
|
+
if (options.signal?.aborted)
|
|
10683
|
+
throw cause;
|
|
10684
|
+
const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
|
|
10685
|
+
const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
|
|
10686
|
+
if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
|
|
10687
|
+
throw cause;
|
|
10688
|
+
throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
|
|
10689
|
+
}
|
|
10690
|
+
}
|
|
10691
|
+
|
|
10692
|
+
// src/lib/npm-plugin-installer.ts
|
|
10693
|
+
var DEFAULT_LIMITS = {
|
|
10694
|
+
metadataBytes: 15 * 1024 * 1024,
|
|
10695
|
+
packageTarballBytes: 50 * 1024 * 1024,
|
|
10696
|
+
totalNetworkBytes: 250 * 1024 * 1024,
|
|
10697
|
+
packageFileBytes: 50 * 1024 * 1024,
|
|
10698
|
+
totalUnpackedBytes: 512 * 1024 * 1024,
|
|
10699
|
+
totalFiles: 50000,
|
|
10700
|
+
totalPackages: 512,
|
|
10701
|
+
dependencyDepth: 64
|
|
10702
|
+
};
|
|
10703
|
+
var METADATA_TIMEOUT_MS = 30000;
|
|
10704
|
+
var TARBALL_TIMEOUT_MS = 120000;
|
|
10705
|
+
|
|
10706
|
+
class SecurityInstallError extends FetchPassthroughError {
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
class PackageUnavailableError extends Error {
|
|
10710
|
+
}
|
|
10711
|
+
function isOptionalAvailabilityError(error2) {
|
|
10712
|
+
return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
|
|
10713
|
+
}
|
|
10714
|
+
function object3(value) {
|
|
10715
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10716
|
+
}
|
|
10717
|
+
function stringMap2(value) {
|
|
10718
|
+
const map = object3(value);
|
|
10719
|
+
if (!map)
|
|
10720
|
+
return {};
|
|
10721
|
+
const result = {};
|
|
10722
|
+
for (const [name, selector] of Object.entries(map)) {
|
|
10723
|
+
if (typeof selector === "string" && selector.trim())
|
|
10724
|
+
result[name] = selector.trim();
|
|
10725
|
+
}
|
|
10726
|
+
return result;
|
|
10727
|
+
}
|
|
10728
|
+
function mb(bytes) {
|
|
10729
|
+
return `${Math.ceil(bytes / 1024 / 1024)}MB`;
|
|
10730
|
+
}
|
|
10731
|
+
function isContained2(root, candidate) {
|
|
10732
|
+
const relative2 = path17.relative(root, candidate);
|
|
10733
|
+
return relative2 === "" || !relative2.startsWith("..") && !path17.isAbsolute(relative2);
|
|
10734
|
+
}
|
|
10735
|
+
function mergeLimits(overrides) {
|
|
10736
|
+
const limits = { ...DEFAULT_LIMITS, ...overrides };
|
|
10737
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
10738
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
10739
|
+
throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
|
|
10740
|
+
}
|
|
10741
|
+
}
|
|
10742
|
+
return limits;
|
|
10743
|
+
}
|
|
10744
|
+
async function streamResponse(ctx, response, maxBytes, totalLimitMessage, onChunk) {
|
|
10745
|
+
if (!response.body)
|
|
10746
|
+
throw new Error("Registry response had no body.");
|
|
10747
|
+
const declared = Number(response.headers.get("content-length"));
|
|
10748
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
10749
|
+
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
10750
|
+
}
|
|
10751
|
+
if (Number.isFinite(declared) && declared > ctx.limits.totalNetworkBytes - ctx.totalNetworkBytes) {
|
|
10752
|
+
throw new SecurityInstallError(totalLimitMessage);
|
|
10753
|
+
}
|
|
10754
|
+
const reader = response.body.getReader();
|
|
10755
|
+
let total = 0;
|
|
10756
|
+
try {
|
|
10757
|
+
while (true) {
|
|
10758
|
+
const { done, value } = await reader.read();
|
|
10759
|
+
if (done)
|
|
10760
|
+
break;
|
|
10761
|
+
total += value.length;
|
|
10762
|
+
if (total > maxBytes) {
|
|
10763
|
+
await reader.cancel("size limit exceeded").catch(() => {});
|
|
10764
|
+
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
10765
|
+
}
|
|
10766
|
+
if (ctx.totalNetworkBytes + value.length > ctx.limits.totalNetworkBytes) {
|
|
10767
|
+
await reader.cancel("total size limit exceeded").catch(() => {});
|
|
10768
|
+
throw new SecurityInstallError(totalLimitMessage);
|
|
10769
|
+
}
|
|
10770
|
+
ctx.totalNetworkBytes += value.length;
|
|
10771
|
+
await onChunk(value);
|
|
10883
10772
|
}
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10773
|
+
} finally {
|
|
10774
|
+
reader.releaseLock();
|
|
10775
|
+
}
|
|
10776
|
+
return total;
|
|
10777
|
+
}
|
|
10778
|
+
async function fetchPackument(ctx, name) {
|
|
10779
|
+
let pending = ctx.packuments.get(name);
|
|
10780
|
+
if (pending)
|
|
10781
|
+
return pending;
|
|
10782
|
+
pending = withFetchTimeout({
|
|
10783
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
10784
|
+
signal: ctx.signal,
|
|
10785
|
+
timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
|
|
10786
|
+
offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
10787
|
+
}, async (signal) => {
|
|
10788
|
+
const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
|
|
10789
|
+
signal,
|
|
10790
|
+
redirect: "error",
|
|
10791
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
10792
|
+
});
|
|
10793
|
+
if (response.status === 404) {
|
|
10794
|
+
throw new FriendlyHttpError(`npm package "${name}" was not found.`);
|
|
10888
10795
|
}
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
}
|
|
10894
|
-
|
|
10895
|
-
|
|
10796
|
+
if (!response.ok) {
|
|
10797
|
+
throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
|
|
10798
|
+
}
|
|
10799
|
+
const chunks = [];
|
|
10800
|
+
const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
|
|
10801
|
+
chunks.push(chunk2);
|
|
10802
|
+
});
|
|
10803
|
+
const bytes = new Uint8Array(size);
|
|
10804
|
+
let offset = 0;
|
|
10805
|
+
for (const chunk2 of chunks) {
|
|
10806
|
+
bytes.set(chunk2, offset);
|
|
10807
|
+
offset += chunk2.length;
|
|
10896
10808
|
}
|
|
10809
|
+
let metadata = null;
|
|
10897
10810
|
try {
|
|
10898
|
-
|
|
10899
|
-
} catch
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10811
|
+
metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
|
|
10812
|
+
} catch {}
|
|
10813
|
+
if (!metadata || metadata.name !== name) {
|
|
10814
|
+
throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
|
|
10815
|
+
}
|
|
10816
|
+
return metadata;
|
|
10817
|
+
}).catch((error2) => {
|
|
10818
|
+
ctx.packuments.delete(name);
|
|
10819
|
+
throw error2;
|
|
10820
|
+
});
|
|
10821
|
+
ctx.packuments.set(name, pending);
|
|
10822
|
+
return pending;
|
|
10823
|
+
}
|
|
10824
|
+
function parseIntegrity(dist) {
|
|
10825
|
+
const integrity = typeof dist.integrity === "string" ? dist.integrity.trim() : "";
|
|
10826
|
+
const parsed = integrity.split(/\s+/).map((token) => token.match(/^(sha512|sha384|sha256|sha1)-([A-Za-z0-9+/]+={0,2})(?:\?.*)?$/)).filter((match) => Boolean(match?.[1] && match[2]));
|
|
10827
|
+
for (const algorithm of ["sha512", "sha384", "sha256"]) {
|
|
10828
|
+
const match = parsed.find((candidate) => candidate[1] === algorithm);
|
|
10829
|
+
if (!match)
|
|
10830
|
+
continue;
|
|
10831
|
+
const digest = Buffer.from(match[2], "base64");
|
|
10832
|
+
if (digest.length > 0) {
|
|
10833
|
+
return { algorithm, digest, label: `${algorithm}-${match[2]}`, legacySha1: false };
|
|
10908
10834
|
}
|
|
10835
|
+
}
|
|
10836
|
+
const sha1 = parsed.find((candidate) => candidate[1] === "sha1");
|
|
10837
|
+
if (sha1) {
|
|
10909
10838
|
return {
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
warnings: [...ctx.warnings]
|
|
10839
|
+
algorithm: "sha1",
|
|
10840
|
+
digest: Buffer.from(sha1[2], "base64"),
|
|
10841
|
+
label: `sha1-${sha1[2]}`,
|
|
10842
|
+
legacySha1: true
|
|
10915
10843
|
};
|
|
10916
|
-
} finally {
|
|
10917
|
-
await rm3(container, { recursive: true, force: true }).catch(() => {});
|
|
10918
10844
|
}
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10845
|
+
if (integrity) {
|
|
10846
|
+
throw new SecurityInstallError("The npm registry supplied integrity data, but no supported SHA-256-or-stronger hash.");
|
|
10847
|
+
}
|
|
10848
|
+
const shasum = typeof dist.shasum === "string" ? dist.shasum.trim().toLowerCase() : "";
|
|
10849
|
+
if (/^[a-f0-9]{40}$/.test(shasum)) {
|
|
10850
|
+
return {
|
|
10851
|
+
algorithm: "sha1",
|
|
10852
|
+
digest: Buffer.from(shasum, "hex"),
|
|
10853
|
+
label: `sha1-hex-${shasum}`,
|
|
10854
|
+
legacySha1: true
|
|
10855
|
+
};
|
|
10923
10856
|
}
|
|
10857
|
+
throw new SecurityInstallError("The npm registry provided no usable integrity hash, so the package was not installed.");
|
|
10924
10858
|
}
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
if (installed.backupRoot)
|
|
10928
|
-
await rename2(installed.backupRoot, installed.installRoot);
|
|
10859
|
+
function unsupportedSelector(selector) {
|
|
10860
|
+
return /^(?:file:|git\+|https?:|ssh:|github:|workspace:|link:|npm:)/i.test(selector);
|
|
10929
10861
|
}
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
var RECOMMENDED_EXTENSIONS = [
|
|
10934
|
-
{
|
|
10935
|
-
use: "markdown-it-mark",
|
|
10936
|
-
label: "Highlight",
|
|
10937
|
-
description: "Highlighted text with `==marked==` -> `<mark>`."
|
|
10938
|
-
},
|
|
10939
|
-
{
|
|
10940
|
-
use: "markdown-it-sub",
|
|
10941
|
-
label: "Subscript",
|
|
10942
|
-
description: "Subscript text with `H~2~O`."
|
|
10943
|
-
},
|
|
10944
|
-
{
|
|
10945
|
-
use: "markdown-it-sup",
|
|
10946
|
-
label: "Superscript",
|
|
10947
|
-
description: "Superscript text with `29^th^`."
|
|
10948
|
-
},
|
|
10949
|
-
{
|
|
10950
|
-
use: "markdown-it-abbr",
|
|
10951
|
-
label: "Abbreviations",
|
|
10952
|
-
description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
|
|
10953
|
-
},
|
|
10954
|
-
{
|
|
10955
|
-
use: "gutterpress-gfm-alerts",
|
|
10956
|
-
label: "Callouts",
|
|
10957
|
-
description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
|
|
10862
|
+
async function selectPackage(ctx, name, selector) {
|
|
10863
|
+
if (selector && unsupportedSelector(selector)) {
|
|
10864
|
+
throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
|
|
10958
10865
|
}
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
if (mutationQueues.get(key) === tail)
|
|
10975
|
-
mutationQueues.delete(key);
|
|
10976
|
-
});
|
|
10977
|
-
return run;
|
|
10978
|
-
}
|
|
10979
|
-
function entryUse(item) {
|
|
10980
|
-
if (isScalar(item))
|
|
10981
|
-
return typeof item.value === "string" ? item.value.trim() : null;
|
|
10982
|
-
if (isMap(item)) {
|
|
10983
|
-
const u = item.get("use");
|
|
10984
|
-
return typeof u === "string" ? u.trim() : null;
|
|
10866
|
+
const metadata = await fetchPackument(ctx, name);
|
|
10867
|
+
const versions = object3(metadata.versions);
|
|
10868
|
+
const tags = object3(metadata["dist-tags"]);
|
|
10869
|
+
if (!versions || !tags)
|
|
10870
|
+
throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
|
|
10871
|
+
let version;
|
|
10872
|
+
if (!selector) {
|
|
10873
|
+
if (typeof tags.latest === "string")
|
|
10874
|
+
version = tags.latest;
|
|
10875
|
+
} else if (object3(versions[selector])) {
|
|
10876
|
+
version = selector;
|
|
10877
|
+
} else if (typeof tags[selector] === "string") {
|
|
10878
|
+
version = tags[selector];
|
|
10879
|
+
} else if (validRange2(selector)) {
|
|
10880
|
+
version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
|
|
10985
10881
|
}
|
|
10986
|
-
|
|
10987
|
-
}
|
|
10988
|
-
|
|
10989
|
-
const
|
|
10990
|
-
if (!
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
enabled: item.get("enabled") !== false
|
|
10999
|
-
};
|
|
11000
|
-
}
|
|
11001
|
-
function sameExtension(a, b) {
|
|
11002
|
-
if (a.trim() === b.trim())
|
|
11003
|
-
return true;
|
|
11004
|
-
let pa;
|
|
11005
|
-
let pb;
|
|
10882
|
+
if (!version || !isExactNpmVersion(version)) {
|
|
10883
|
+
throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
|
|
10884
|
+
}
|
|
10885
|
+
const selected = object3(versions[version]);
|
|
10886
|
+
if (!selected || selected.name !== name || selected.version !== version) {
|
|
10887
|
+
throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
|
|
10888
|
+
}
|
|
10889
|
+
const dist = object3(selected.dist);
|
|
10890
|
+
if (!dist || typeof dist.tarball !== "string") {
|
|
10891
|
+
throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
|
|
10892
|
+
}
|
|
10893
|
+
let tarball;
|
|
11006
10894
|
try {
|
|
11007
|
-
|
|
11008
|
-
pb = parseExtensionSpecifier(b);
|
|
10895
|
+
tarball = new URL(dist.tarball);
|
|
11009
10896
|
} catch {
|
|
11010
|
-
|
|
10897
|
+
throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
|
|
11011
10898
|
}
|
|
11012
|
-
|
|
11013
|
-
}
|
|
11014
|
-
function indexOfUse(seq, use) {
|
|
11015
|
-
const items = seq.items;
|
|
11016
|
-
const exact = items.findIndex((item) => entryUse(item) === use.trim());
|
|
11017
|
-
if (exact >= 0)
|
|
11018
|
-
return exact;
|
|
11019
|
-
return items.findIndex((item) => {
|
|
11020
|
-
const u = entryUse(item);
|
|
11021
|
-
return !!u && sameExtension(u, use);
|
|
11022
|
-
});
|
|
11023
|
-
}
|
|
11024
|
-
function manifestPathFor(projectDir, abs) {
|
|
11025
|
-
const rel = path16.relative(path16.resolve(projectDir), abs).split(path16.sep).join("/");
|
|
11026
|
-
if (rel === "") {
|
|
11027
|
-
throw new Error("An extension cannot be the project folder itself.");
|
|
10899
|
+
if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
|
|
10900
|
+
throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
|
|
11028
10901
|
}
|
|
11029
|
-
|
|
11030
|
-
return rel;
|
|
11031
|
-
return `./${rel}`;
|
|
11032
|
-
}
|
|
11033
|
-
var NO_CARRIES = {
|
|
11034
|
-
markdown: false,
|
|
11035
|
-
styles: false,
|
|
11036
|
-
snippets: false,
|
|
11037
|
-
components: false
|
|
11038
|
-
};
|
|
11039
|
-
async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
|
|
11040
|
-
const meta = await readExtensionMeta(dir);
|
|
11041
|
-
const styles = extensionStyleList(meta);
|
|
11042
|
-
return {
|
|
11043
|
-
...base,
|
|
11044
|
-
label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
|
|
11045
|
-
...meta.description?.trim() ? { description: meta.description.trim() } : {},
|
|
11046
|
-
...meta.author?.trim() ? { author: meta.author.trim() } : {},
|
|
11047
|
-
...meta.preview !== undefined ? { preview: meta.preview } : {},
|
|
11048
|
-
...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
|
|
11049
|
-
...styles.length > 0 ? { styles } : {},
|
|
11050
|
-
carries: extensionCarries(meta),
|
|
11051
|
-
dir
|
|
11052
|
-
};
|
|
10902
|
+
return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
|
|
11053
10903
|
}
|
|
11054
|
-
async function
|
|
11055
|
-
const
|
|
11056
|
-
|
|
10904
|
+
async function downloadTarball(ctx, pkg, archivePath) {
|
|
10905
|
+
const file = await open2(archivePath, "wx");
|
|
10906
|
+
const hash = createHash7(pkg.integrity.algorithm);
|
|
11057
10907
|
try {
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
10908
|
+
await withFetchTimeout({
|
|
10909
|
+
timeoutMs: TARBALL_TIMEOUT_MS,
|
|
10910
|
+
signal: ctx.signal,
|
|
10911
|
+
timeoutMessage: `Downloading ${pkg.name}@${pkg.version} timed out.`,
|
|
10912
|
+
offlineMessage: (cause) => `Downloading ${pkg.name}@${pkg.version} failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
10913
|
+
}, async (signal) => {
|
|
10914
|
+
const response = await ctx.fetch(pkg.tarball, { signal, redirect: "error" });
|
|
10915
|
+
if (!response.ok) {
|
|
10916
|
+
throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
|
|
10917
|
+
}
|
|
10918
|
+
await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
|
|
10919
|
+
hash.update(chunk2);
|
|
10920
|
+
try {
|
|
10921
|
+
await file.write(chunk2);
|
|
10922
|
+
} catch (cause) {
|
|
10923
|
+
throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
|
|
10924
|
+
}
|
|
10925
|
+
});
|
|
10926
|
+
});
|
|
10927
|
+
} finally {
|
|
10928
|
+
await file.close();
|
|
11061
10929
|
}
|
|
11062
|
-
const
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
...raw.export ? { export: raw.export } : {}
|
|
11066
|
-
};
|
|
11067
|
-
const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
11068
|
-
if (!parsed) {
|
|
11069
|
-
return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
|
|
10930
|
+
const actual = hash.digest();
|
|
10931
|
+
if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
|
|
10932
|
+
throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
|
|
11070
10933
|
}
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
...common,
|
|
11076
|
-
kind: "bundled",
|
|
11077
|
-
name: bundledName,
|
|
11078
|
-
label: rec?.label ?? bundledName,
|
|
11079
|
-
...rec ? { description: rec.description } : {},
|
|
11080
|
-
carries: { ...NO_CARRIES, markdown: true }
|
|
11081
|
-
});
|
|
10934
|
+
}
|
|
10935
|
+
function safeTarRelative(entryPath, packageName) {
|
|
10936
|
+
if (!entryPath || entryPath.includes("\\") || entryPath.startsWith("/") || /^[a-zA-Z]:/.test(entryPath)) {
|
|
10937
|
+
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath || "(empty)"}`);
|
|
11082
10938
|
}
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
10939
|
+
const parts = entryPath.split("/");
|
|
10940
|
+
if (parts.at(-1) === "")
|
|
10941
|
+
parts.pop();
|
|
10942
|
+
const legacyTypesRoot = packageName.startsWith("@types/") ? packageName.slice("@types/".length) : null;
|
|
10943
|
+
if (parts.some((part) => !part || part === "." || part === "..") || parts[0] !== "package" && parts[0] !== legacyTypesRoot) {
|
|
10944
|
+
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath}`);
|
|
10945
|
+
}
|
|
10946
|
+
const relative2 = parts.slice(1).join("/");
|
|
10947
|
+
if (!relative2)
|
|
10948
|
+
return { relative: relative2, key: "" };
|
|
10949
|
+
assertWindowsSafeRelativePath(relative2);
|
|
10950
|
+
if (relative2.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
|
|
10951
|
+
throw new SecurityInstallError(`The npm tarball contains bundled node_modules (${entryPath}); bundled dependencies are not accepted.`);
|
|
10952
|
+
}
|
|
10953
|
+
return { relative: relative2, key: windowsPathKey(relative2) };
|
|
10954
|
+
}
|
|
10955
|
+
async function extractTarball(ctx, archivePath, packageDir, packageName) {
|
|
10956
|
+
const { extract } = await import("tar");
|
|
10957
|
+
let rejected = null;
|
|
10958
|
+
const seen = new Set;
|
|
10959
|
+
const metadataTypes = new Set([
|
|
10960
|
+
"GlobalExtendedHeader",
|
|
10961
|
+
"ExtendedHeader",
|
|
10962
|
+
"NextFileHasLongLinkpath",
|
|
10963
|
+
"NextFileHasLongPath",
|
|
10964
|
+
"OldGnuLongPath",
|
|
10965
|
+
"OldExtendedHeader"
|
|
10966
|
+
]);
|
|
10967
|
+
await mkdir8(packageDir, { recursive: true });
|
|
10968
|
+
const unpack = extract({
|
|
10969
|
+
cwd: packageDir,
|
|
10970
|
+
strip: 1,
|
|
10971
|
+
strict: true,
|
|
10972
|
+
preservePaths: false,
|
|
10973
|
+
preserveOwner: false,
|
|
10974
|
+
noMtime: true,
|
|
10975
|
+
chmod: false,
|
|
10976
|
+
maxDepth: ctx.limits.dependencyDepth * 4,
|
|
10977
|
+
filter: (entryPath, rawEntry) => {
|
|
10978
|
+
if (rejected)
|
|
10979
|
+
return false;
|
|
10980
|
+
const entry = rawEntry;
|
|
10981
|
+
if (metadataTypes.has(entry.type))
|
|
10982
|
+
return true;
|
|
10983
|
+
if (entry.type === "Link" || entry.type === "SymbolicLink") {
|
|
10984
|
+
rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
|
|
10985
|
+
return false;
|
|
10986
|
+
}
|
|
10987
|
+
if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
|
|
10988
|
+
rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
|
|
10989
|
+
return false;
|
|
10990
|
+
}
|
|
10991
|
+
try {
|
|
10992
|
+
const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
|
|
10993
|
+
if (!relative2) {
|
|
10994
|
+
if (entry.type === "Directory")
|
|
10995
|
+
return true;
|
|
10996
|
+
throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
|
|
10997
|
+
}
|
|
10998
|
+
if (seen.has(key)) {
|
|
10999
|
+
throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
|
|
11000
|
+
}
|
|
11001
|
+
seen.add(key);
|
|
11002
|
+
ctx.totalFiles++;
|
|
11003
|
+
if (ctx.totalFiles > ctx.limits.totalFiles) {
|
|
11004
|
+
throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
|
|
11005
|
+
}
|
|
11006
|
+
if (entry.type !== "Directory") {
|
|
11007
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
|
|
11008
|
+
throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
11011
|
+
return true;
|
|
11012
|
+
} catch (error2) {
|
|
11013
|
+
rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
|
|
11014
|
+
return false;
|
|
11015
|
+
}
|
|
11092
11016
|
}
|
|
11093
|
-
|
|
11094
|
-
|
|
11017
|
+
});
|
|
11018
|
+
const limiter = new Transform({
|
|
11019
|
+
transform(chunk2, _encoding, callback) {
|
|
11020
|
+
ctx.totalUnpackedBytes += chunk2.length;
|
|
11021
|
+
if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
|
|
11022
|
+
callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
|
|
11023
|
+
} else
|
|
11024
|
+
callback(null, chunk2);
|
|
11095
11025
|
}
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
name: parsed.name,
|
|
11106
|
-
...parsed.version ? { version: parsed.version } : {}
|
|
11107
|
-
};
|
|
11108
|
-
if (!parsed.version) {
|
|
11109
|
-
warnings.push(`Not pinned — run \`gutterpress ext add ${parsed.name}\` to install it and pin an exact version.`);
|
|
11110
|
-
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11026
|
+
});
|
|
11027
|
+
try {
|
|
11028
|
+
await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
|
|
11029
|
+
} catch (error2) {
|
|
11030
|
+
if (rejected)
|
|
11031
|
+
throw rejected;
|
|
11032
|
+
if (error2 instanceof SecurityInstallError)
|
|
11033
|
+
throw error2;
|
|
11034
|
+
throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
|
|
11111
11035
|
}
|
|
11112
|
-
|
|
11036
|
+
if (rejected)
|
|
11037
|
+
throw rejected;
|
|
11038
|
+
}
|
|
11039
|
+
async function readInstalledManifest(packageDir, selected) {
|
|
11040
|
+
const packageJsonPath = path17.join(packageDir, "package.json");
|
|
11041
|
+
let manifest = null;
|
|
11113
11042
|
try {
|
|
11114
|
-
|
|
11043
|
+
const info2 = await lstat2(packageJsonPath);
|
|
11044
|
+
if (!info2.isFile() || info2.isSymbolicLink())
|
|
11045
|
+
throw new Error("not a regular file");
|
|
11046
|
+
manifest = object3(JSON.parse(await readFile27(packageJsonPath, "utf8")));
|
|
11115
11047
|
} catch (error2) {
|
|
11116
|
-
|
|
11048
|
+
throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
|
|
11117
11049
|
}
|
|
11118
|
-
if (
|
|
11119
|
-
|
|
11120
|
-
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11050
|
+
if (manifest?.name !== selected.name || manifest.version !== selected.version) {
|
|
11051
|
+
throw new SecurityInstallError(`Downloaded ${String(manifest?.name)}@${String(manifest?.version)} instead of ${selected.name}@${selected.version}.`);
|
|
11121
11052
|
}
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11053
|
+
return manifest;
|
|
11054
|
+
}
|
|
11055
|
+
async function optionalPackageEntry(packageDir, manifest, condition) {
|
|
11056
|
+
try {
|
|
11057
|
+
return await resolvePackageEntry(packageDir, manifest, condition);
|
|
11058
|
+
} catch (error2) {
|
|
11059
|
+
if (error2 instanceof Error && error2.message.includes("no contained JavaScript entry point")) {
|
|
11060
|
+
return;
|
|
11061
|
+
}
|
|
11062
|
+
throw error2;
|
|
11125
11063
|
}
|
|
11126
|
-
const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
|
|
11127
|
-
return withWarnings({
|
|
11128
|
-
...described,
|
|
11129
|
-
carries: { ...described.carries, markdown: true }
|
|
11130
|
-
});
|
|
11131
11064
|
}
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
if (!raw)
|
|
11141
|
-
continue;
|
|
11142
|
-
out.push(await describeEntry(projectDir, raw));
|
|
11065
|
+
function platformAllowed(rule, current) {
|
|
11066
|
+
let values;
|
|
11067
|
+
if (typeof rule === "string") {
|
|
11068
|
+
values = [rule];
|
|
11069
|
+
} else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
|
|
11070
|
+
values = rule;
|
|
11071
|
+
} else {
|
|
11072
|
+
return true;
|
|
11143
11073
|
}
|
|
11144
|
-
|
|
11074
|
+
if (values.length === 0)
|
|
11075
|
+
return true;
|
|
11076
|
+
if (values.length === 1 && values[0] === "any")
|
|
11077
|
+
return true;
|
|
11078
|
+
if (values.includes(`!${current}`))
|
|
11079
|
+
return false;
|
|
11080
|
+
const positives = values.filter((value) => !value.startsWith("!"));
|
|
11081
|
+
return positives.length === 0 || positives.includes(current);
|
|
11145
11082
|
}
|
|
11146
|
-
|
|
11147
|
-
|
|
11083
|
+
function relativePackagePath(parentPackagePath, name) {
|
|
11084
|
+
const segments = name.split("/");
|
|
11085
|
+
return parentPackagePath ? `${parentPackagePath}/node_modules/${segments.join("/")}` : `node_modules/${segments.join("/")}`;
|
|
11148
11086
|
}
|
|
11149
|
-
function
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11087
|
+
async function installPackage(ctx, name, selector, parentPackagePath, ancestors, depth, optional) {
|
|
11088
|
+
const recordStart = ctx.records.length;
|
|
11089
|
+
const skippedStart = ctx.skipped.length;
|
|
11090
|
+
const warningsBefore = new Set(ctx.warnings);
|
|
11091
|
+
const discardOptionalSubtree = () => {
|
|
11092
|
+
ctx.records.splice(recordStart);
|
|
11093
|
+
ctx.skipped.splice(skippedStart);
|
|
11094
|
+
ctx.warnings = warningsBefore;
|
|
11155
11095
|
};
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11096
|
+
if (depth > ctx.limits.dependencyDepth) {
|
|
11097
|
+
throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
|
|
11098
|
+
}
|
|
11099
|
+
let selected;
|
|
11100
|
+
try {
|
|
11101
|
+
selected = await selectPackage(ctx, name, selector);
|
|
11102
|
+
} catch (error2) {
|
|
11103
|
+
if (optional && isOptionalAvailabilityError(error2)) {
|
|
11104
|
+
discardOptionalSubtree();
|
|
11105
|
+
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
11106
|
+
}
|
|
11107
|
+
throw error2;
|
|
11108
|
+
}
|
|
11109
|
+
const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
|
|
11110
|
+
if (ancestor)
|
|
11111
|
+
return { path: ancestor.path };
|
|
11112
|
+
if (ctx.records.length >= ctx.limits.totalPackages) {
|
|
11113
|
+
throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
|
|
11114
|
+
}
|
|
11115
|
+
const relativePath = relativePackagePath(parentPackagePath, name);
|
|
11116
|
+
const packageDir = path17.join(ctx.stageRoot, ...relativePath.split("/"));
|
|
11117
|
+
const archivePath = path17.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
|
|
11118
|
+
try {
|
|
11119
|
+
await mkdir8(path17.dirname(packageDir), { recursive: true });
|
|
11120
|
+
await downloadTarball(ctx, selected, archivePath);
|
|
11121
|
+
await extractTarball(ctx, archivePath, packageDir, selected.name);
|
|
11122
|
+
const manifest = await readInstalledManifest(packageDir, selected);
|
|
11123
|
+
if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
|
|
11124
|
+
const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
|
|
11125
|
+
if (optional) {
|
|
11126
|
+
await rm3(packageDir, { recursive: true, force: true });
|
|
11127
|
+
return { skipped: reason };
|
|
11171
11128
|
}
|
|
11172
|
-
|
|
11173
|
-
item.set("enabled", false);
|
|
11174
|
-
} else {
|
|
11175
|
-
const map = new YAMLMap(doc.schema);
|
|
11176
|
-
map.set("use", entryUse(item));
|
|
11177
|
-
map.set("enabled", false);
|
|
11178
|
-
seq.items[idx] = map;
|
|
11129
|
+
throw new PackageUnavailableError(reason);
|
|
11179
11130
|
}
|
|
11180
|
-
await
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
return withMutationLock(projectDir, async () => {
|
|
11185
|
-
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11186
|
-
const seq = doc.get("extensions", true);
|
|
11187
|
-
const idx = isSeq2(seq) ? indexOfUse(seq, use) : -1;
|
|
11188
|
-
if (!isSeq2(seq) || idx < 0)
|
|
11189
|
-
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
11190
|
-
const written = entryUse(seq.items[idx]);
|
|
11191
|
-
seq.items.splice(idx, 1);
|
|
11192
|
-
if (seq.items.length === 0)
|
|
11193
|
-
doc.delete("extensions");
|
|
11194
|
-
await writeManifestDoc(file, doc);
|
|
11195
|
-
let parsed = null;
|
|
11196
|
-
try {
|
|
11197
|
-
parsed = parseExtensionSpecifier(written);
|
|
11198
|
-
} catch {
|
|
11199
|
-
return;
|
|
11131
|
+
const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
|
|
11132
|
+
const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
|
|
11133
|
+
if (parentPackagePath === null && !importEntry && !requireEntry) {
|
|
11134
|
+
throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
|
|
11200
11135
|
}
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11136
|
+
const record = {
|
|
11137
|
+
path: relativePath,
|
|
11138
|
+
name,
|
|
11139
|
+
version: selected.version,
|
|
11140
|
+
tarball: selected.tarball,
|
|
11141
|
+
integrity: selected.integrity.label,
|
|
11142
|
+
legacySha1: selected.integrity.legacySha1,
|
|
11143
|
+
...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
|
|
11144
|
+
...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
|
|
11145
|
+
dependencies: {},
|
|
11146
|
+
packageDir,
|
|
11147
|
+
manifest
|
|
11148
|
+
};
|
|
11149
|
+
ctx.records.push(record);
|
|
11150
|
+
if (selected.integrity.legacySha1) {
|
|
11151
|
+
ctx.warnings.add(`${name}@${selected.version} is an old npm release with only SHA-1 registry integrity. It was accepted for compatibility and recorded as legacy SHA-1 in the vendor receipt.`);
|
|
11207
11152
|
}
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
if (isMap(item)) {
|
|
11217
|
-
item.set("use", use);
|
|
11218
|
-
if (exportName)
|
|
11219
|
-
item.set("export", exportName);
|
|
11220
|
-
else
|
|
11221
|
-
item.delete("export");
|
|
11222
|
-
if (item.items.length === 1)
|
|
11223
|
-
seq.items[idx] = new Scalar(use);
|
|
11224
|
-
} else if (exportName) {
|
|
11225
|
-
const map = new YAMLMap(doc.schema);
|
|
11226
|
-
map.set("use", use);
|
|
11227
|
-
map.set("export", exportName);
|
|
11228
|
-
seq.items[idx] = map;
|
|
11229
|
-
} else {
|
|
11230
|
-
seq.items[idx] = new Scalar(use);
|
|
11153
|
+
const dependencies = stringMap2(manifest.dependencies);
|
|
11154
|
+
const optionalDependencies = stringMap2(manifest.optionalDependencies);
|
|
11155
|
+
for (const optionalName of Object.keys(optionalDependencies))
|
|
11156
|
+
delete dependencies[optionalName];
|
|
11157
|
+
const peerDependencies = stringMap2(manifest.peerDependencies);
|
|
11158
|
+
const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
|
|
11159
|
+
for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
|
|
11160
|
+
delete peerDependencies[existing];
|
|
11231
11161
|
}
|
|
11232
|
-
|
|
11233
|
-
const
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11162
|
+
const nextAncestors = [...ancestors, record];
|
|
11163
|
+
const requiredEntries = [
|
|
11164
|
+
...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
|
|
11165
|
+
...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
|
|
11166
|
+
].sort((a, b) => a.dependency.localeCompare(b.dependency));
|
|
11167
|
+
for (const { dependency, range } of requiredEntries) {
|
|
11168
|
+
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
|
|
11169
|
+
if (!child.path)
|
|
11170
|
+
throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
|
|
11171
|
+
record.dependencies[dependency] = child.path;
|
|
11172
|
+
}
|
|
11173
|
+
const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
|
|
11174
|
+
for (const [dependency, range] of optionalEntries) {
|
|
11175
|
+
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
|
|
11176
|
+
if (child.path)
|
|
11177
|
+
record.dependencies[dependency] = child.path;
|
|
11178
|
+
else if (child.skipped) {
|
|
11179
|
+
ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
|
|
11180
|
+
}
|
|
11181
|
+
}
|
|
11182
|
+
for (const [dependency, range] of Object.entries(peerDependencies)) {
|
|
11183
|
+
if (object3(peerMeta[dependency])?.optional === true) {
|
|
11184
|
+
ctx.skipped.push({
|
|
11185
|
+
from: relativePath,
|
|
11186
|
+
name: dependency,
|
|
11187
|
+
selector: range,
|
|
11188
|
+
kind: "optional-peer",
|
|
11189
|
+
reason: "Optional peer dependencies are not auto-installed."
|
|
11190
|
+
});
|
|
11191
|
+
}
|
|
11192
|
+
}
|
|
11193
|
+
return { path: relativePath };
|
|
11194
|
+
} catch (error2) {
|
|
11195
|
+
await rm3(packageDir, { recursive: true, force: true });
|
|
11196
|
+
if (optional && isOptionalAvailabilityError(error2)) {
|
|
11197
|
+
discardOptionalSubtree();
|
|
11198
|
+
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
11199
|
+
}
|
|
11200
|
+
throw error2;
|
|
11201
|
+
} finally {
|
|
11202
|
+
await rm3(archivePath, { force: true });
|
|
11239
11203
|
}
|
|
11240
|
-
await writeManifestDoc(file, doc);
|
|
11241
11204
|
}
|
|
11242
|
-
async function
|
|
11205
|
+
async function ensureDirectory(pathname) {
|
|
11243
11206
|
try {
|
|
11244
|
-
await
|
|
11245
|
-
|
|
11246
|
-
|
|
11207
|
+
const info2 = await lstat2(pathname);
|
|
11208
|
+
if (info2.isSymbolicLink() || !info2.isDirectory()) {
|
|
11209
|
+
throw new Error(`Plugin install path is not a normal directory: ${pathname}`);
|
|
11210
|
+
}
|
|
11211
|
+
} catch (error2) {
|
|
11212
|
+
if (error2.code !== "ENOENT")
|
|
11213
|
+
throw error2;
|
|
11214
|
+
await mkdir8(pathname);
|
|
11247
11215
|
}
|
|
11248
|
-
const use = manifestPathFor(projectDir, abs);
|
|
11249
|
-
await loadPlugin(extensionConfigFor({ use, kind: "path", name: use, export: exportName }), projectDir);
|
|
11250
|
-
await upsertEntry(projectDir, use, exportName);
|
|
11251
|
-
return await describeExtension(projectDir, use);
|
|
11252
|
-
}
|
|
11253
|
-
async function addExtension(projectDir, specifier, options = {}) {
|
|
11254
|
-
return withMutationLock(projectDir, () => addExtensionUnlocked(projectDir, specifier, options));
|
|
11255
11216
|
}
|
|
11256
|
-
async function
|
|
11257
|
-
const
|
|
11258
|
-
const
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11217
|
+
async function prepareVendorParent(projectDir, name) {
|
|
11218
|
+
const plugins = path17.join(projectDir, PLUGINS_DIR);
|
|
11219
|
+
const npm = path17.join(plugins, VENDORED_NPM_DIR);
|
|
11220
|
+
const packageParent = path17.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
|
|
11221
|
+
await mkdir8(projectDir, { recursive: true });
|
|
11222
|
+
await ensureDirectory(plugins);
|
|
11223
|
+
await ensureDirectory(npm);
|
|
11224
|
+
await ensureDirectory(packageParent);
|
|
11225
|
+
const [realNpm, realPackageParent] = await Promise.all([
|
|
11226
|
+
realpath2(npm),
|
|
11227
|
+
realpath2(packageParent)
|
|
11228
|
+
]);
|
|
11229
|
+
if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
|
|
11230
|
+
throw new Error("Plugin install path resolves outside the project.");
|
|
11270
11231
|
}
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11232
|
+
return realNpm;
|
|
11233
|
+
}
|
|
11234
|
+
function publicReceipt(record) {
|
|
11235
|
+
return {
|
|
11236
|
+
path: record.path,
|
|
11237
|
+
name: record.name,
|
|
11238
|
+
version: record.version,
|
|
11239
|
+
tarball: record.tarball,
|
|
11240
|
+
integrity: record.integrity,
|
|
11241
|
+
legacySha1: record.legacySha1,
|
|
11242
|
+
...record.entry ? { entry: record.entry } : {},
|
|
11243
|
+
...record.requireEntry ? { requireEntry: record.requireEntry } : {},
|
|
11244
|
+
dependencies: record.dependencies
|
|
11245
|
+
};
|
|
11246
|
+
}
|
|
11247
|
+
async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
|
|
11248
|
+
const requestedProjectDir = path17.resolve(projectDirInput);
|
|
11249
|
+
await mkdir8(requestedProjectDir, { recursive: true });
|
|
11250
|
+
const projectDir = await realpath2(requestedProjectDir);
|
|
11251
|
+
const requested = parseNpmPluginSpec(packageSpec);
|
|
11252
|
+
const npmRoot = await prepareVendorParent(projectDir, requested.name);
|
|
11253
|
+
const container = await mkdtemp2(path17.join(npmRoot, ".install-"));
|
|
11254
|
+
const stageRoot = path17.join(container, "root");
|
|
11255
|
+
const downloadsDir = path17.join(container, "downloads");
|
|
11256
|
+
await Promise.all([mkdir8(stageRoot), mkdir8(downloadsDir)]);
|
|
11257
|
+
const ctx = {
|
|
11258
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
11259
|
+
signal: options.signal,
|
|
11260
|
+
limits: mergeLimits(options.limits),
|
|
11261
|
+
stageRoot,
|
|
11262
|
+
downloadsDir,
|
|
11263
|
+
packuments: new Map,
|
|
11264
|
+
records: [],
|
|
11265
|
+
skipped: [],
|
|
11266
|
+
warnings: new Set,
|
|
11267
|
+
totalNetworkBytes: 0,
|
|
11268
|
+
totalUnpackedBytes: 0,
|
|
11269
|
+
totalFiles: 0,
|
|
11270
|
+
archiveCounter: 0
|
|
11271
|
+
};
|
|
11272
|
+
let finalRoot = "";
|
|
11273
|
+
let backupRoot = null;
|
|
11275
11274
|
try {
|
|
11276
|
-
await
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11275
|
+
const rootResult = await installPackage(ctx, requested.name, requested.selector, null, [], 0, false);
|
|
11276
|
+
if (!rootResult.path)
|
|
11277
|
+
throw new Error("Root plugin package was unexpectedly skipped.");
|
|
11278
|
+
const rootRecord = ctx.records.find((record) => record.path === rootResult.path);
|
|
11279
|
+
if (!rootRecord)
|
|
11280
|
+
throw new Error("Root plugin package is missing from the dependency graph.");
|
|
11281
|
+
const importEntry = rootRecord.entry ?? rootRecord.requireEntry;
|
|
11282
|
+
if (!importEntry)
|
|
11283
|
+
throw new Error("Root plugin package has no recorded JavaScript entry.");
|
|
11284
|
+
const importFormat = packageEntryFormat(rootRecord.manifest, importEntry);
|
|
11285
|
+
const entry = importFormat === "commonjs" ? rootRecord.requireEntry ?? importEntry : importEntry;
|
|
11286
|
+
const format = packageEntryFormat(rootRecord.manifest, entry);
|
|
11287
|
+
const tree = await computeVendorTreeDigest(stageRoot);
|
|
11288
|
+
const receipt = {
|
|
11289
|
+
schemaVersion: VENDOR_RECEIPT_VERSION,
|
|
11290
|
+
root: {
|
|
11291
|
+
name: rootRecord.name,
|
|
11292
|
+
version: rootRecord.version,
|
|
11293
|
+
packagePath: rootRecord.path,
|
|
11294
|
+
entry,
|
|
11295
|
+
format
|
|
11296
|
+
},
|
|
11297
|
+
packages: ctx.records.map(publicReceipt).sort((a, b) => a.path.localeCompare(b.path)),
|
|
11298
|
+
skipped: ctx.skipped,
|
|
11299
|
+
tree: { algorithm: "sha256", ...tree }
|
|
11300
|
+
};
|
|
11301
|
+
const receiptFile = await open2(path17.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
|
|
11286
11302
|
try {
|
|
11287
|
-
await
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11303
|
+
await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
|
|
11304
|
+
`, "utf8");
|
|
11305
|
+
await receiptFile.sync();
|
|
11306
|
+
} finally {
|
|
11307
|
+
await receiptFile.close();
|
|
11291
11308
|
}
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11309
|
+
finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
|
|
11310
|
+
const realFinalParent = await realpath2(path17.dirname(finalRoot));
|
|
11311
|
+
if (!isContained2(projectDir, realFinalParent)) {
|
|
11312
|
+
throw new Error("Plugin install destination resolves outside the project.");
|
|
11313
|
+
}
|
|
11314
|
+
try {
|
|
11315
|
+
await lstat2(finalRoot);
|
|
11316
|
+
backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
|
|
11317
|
+
await rename2(finalRoot, backupRoot);
|
|
11318
|
+
} catch (error2) {
|
|
11319
|
+
if (error2.code !== "ENOENT")
|
|
11320
|
+
throw error2;
|
|
11321
|
+
}
|
|
11322
|
+
try {
|
|
11323
|
+
await rename2(stageRoot, finalRoot);
|
|
11324
|
+
} catch (error2) {
|
|
11325
|
+
if (backupRoot) {
|
|
11326
|
+
try {
|
|
11327
|
+
await rename2(backupRoot, finalRoot);
|
|
11328
|
+
} catch (restoreError) {
|
|
11329
|
+
throw new Error(`Could not publish the new plugin tree or restore the previous tree: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`, { cause: error2 });
|
|
11330
|
+
}
|
|
11331
|
+
}
|
|
11332
|
+
throw error2;
|
|
11333
|
+
}
|
|
11334
|
+
return {
|
|
11335
|
+
name: rootRecord.name,
|
|
11336
|
+
version: rootRecord.version,
|
|
11337
|
+
installRoot: finalRoot,
|
|
11338
|
+
backupRoot,
|
|
11339
|
+
warnings: [...ctx.warnings]
|
|
11340
|
+
};
|
|
11341
|
+
} finally {
|
|
11342
|
+
await rm3(container, { recursive: true, force: true }).catch(() => {});
|
|
11306
11343
|
}
|
|
11307
|
-
return candidate;
|
|
11308
11344
|
}
|
|
11309
|
-
async function
|
|
11310
|
-
if (
|
|
11311
|
-
|
|
11345
|
+
async function finalizeNpmPluginInstall(installed) {
|
|
11346
|
+
if (installed.backupRoot) {
|
|
11347
|
+
await rm3(installed.backupRoot, { recursive: true, force: true });
|
|
11312
11348
|
}
|
|
11313
|
-
return withMutationLock(projectDir, async () => {
|
|
11314
|
-
const dest = path16.join(projectDir, EXTENSIONS_DIR, id);
|
|
11315
|
-
if (!existsSync11(dest)) {
|
|
11316
|
-
const src = path16.join(await getAssetsDir(), "themes", id);
|
|
11317
|
-
await mkdir7(path16.dirname(dest), { recursive: true });
|
|
11318
|
-
await cp2(src, dest, { recursive: true });
|
|
11319
|
-
}
|
|
11320
|
-
return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
|
|
11321
|
-
});
|
|
11322
11349
|
}
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
import path17 from "node:path";
|
|
11328
|
-
var STARTER_THEME_FOR_TEMPLATE = {
|
|
11329
|
-
book: "clean-book",
|
|
11330
|
-
zine: "zine",
|
|
11331
|
-
technical: "technical-doc"
|
|
11332
|
-
};
|
|
11333
|
-
function starterOverridesCss(themeId) {
|
|
11334
|
-
return `/**
|
|
11335
|
-
* Your book's own styles.
|
|
11336
|
-
*
|
|
11337
|
-
* Your book's look lives in ${EXTENSIONS_DIR}/${themeId}/theme.css — an
|
|
11338
|
-
* extension listed under \`extensions:\` in manifest.yaml. Add or swap looks
|
|
11339
|
-
* with \`gutterpress ext add\` (or the desktop's Look view) without losing
|
|
11340
|
-
* anything you write here. This file loads AFTER every extension, so any
|
|
11341
|
-
* rule you add below overrides the look's defaults.
|
|
11342
|
-
*
|
|
11343
|
-
* Most everyday adjustments (colors, fonts, margins, spacing) are already
|
|
11344
|
-
* exposed as CSS custom properties on the theme's :root — try those first.
|
|
11345
|
-
*/
|
|
11346
|
-
`;
|
|
11350
|
+
async function rollbackNpmPluginInstall(installed) {
|
|
11351
|
+
await rm3(installed.installRoot, { recursive: true, force: true });
|
|
11352
|
+
if (installed.backupRoot)
|
|
11353
|
+
await rename2(installed.backupRoot, installed.installRoot);
|
|
11347
11354
|
}
|
|
11348
11355
|
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11356
|
+
// src/lib/extension-manager.ts
|
|
11357
|
+
var EXTENSIONS_DIR = "extensions";
|
|
11358
|
+
var RECOMMENDED_EXTENSIONS = [
|
|
11359
|
+
{
|
|
11360
|
+
use: "markdown-it-mark",
|
|
11361
|
+
label: "Highlight",
|
|
11362
|
+
description: "Highlighted text with `==marked==` -> `<mark>`."
|
|
11363
|
+
},
|
|
11364
|
+
{
|
|
11365
|
+
use: "markdown-it-sub",
|
|
11366
|
+
label: "Subscript",
|
|
11367
|
+
description: "Subscript text with `H~2~O`."
|
|
11368
|
+
},
|
|
11369
|
+
{
|
|
11370
|
+
use: "markdown-it-sup",
|
|
11371
|
+
label: "Superscript",
|
|
11372
|
+
description: "Superscript text with `29^th^`."
|
|
11373
|
+
},
|
|
11374
|
+
{
|
|
11375
|
+
use: "markdown-it-abbr",
|
|
11376
|
+
label: "Abbreviations",
|
|
11377
|
+
description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
|
|
11378
|
+
},
|
|
11379
|
+
{
|
|
11380
|
+
use: "gutterpress-gfm-alerts",
|
|
11381
|
+
label: "Callouts",
|
|
11382
|
+
description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
|
|
11355
11383
|
}
|
|
11384
|
+
];
|
|
11385
|
+
var BUILT_IN_STYLE_SET_IDS = ["clean-book", "zine", "technical-doc"];
|
|
11386
|
+
var mutationQueues = new Map;
|
|
11387
|
+
function withMutationLock(projectDir, mutation) {
|
|
11388
|
+
const resolved = path18.resolve(projectDir);
|
|
11389
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
11390
|
+
const previous = mutationQueues.get(key) ?? Promise.resolve();
|
|
11391
|
+
const run = previous.then(mutation, mutation);
|
|
11392
|
+
const tail = run.then(() => {
|
|
11393
|
+
return;
|
|
11394
|
+
}, () => {
|
|
11395
|
+
return;
|
|
11396
|
+
});
|
|
11397
|
+
mutationQueues.set(key, tail);
|
|
11398
|
+
tail.then(() => {
|
|
11399
|
+
if (mutationQueues.get(key) === tail)
|
|
11400
|
+
mutationQueues.delete(key);
|
|
11401
|
+
});
|
|
11402
|
+
return run;
|
|
11356
11403
|
}
|
|
11357
|
-
function
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
var DEFAULT_AUTHOR = "Anonymous";
|
|
11364
|
-
function isPositivePoints(value) {
|
|
11365
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11366
|
-
}
|
|
11367
|
-
function requirePreset(preset, customPage) {
|
|
11368
|
-
if (!preset || !PRESET_IDS.includes(preset)) {
|
|
11369
|
-
const got = preset ? ` (got "${preset}")` : "";
|
|
11370
|
-
throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
|
|
11371
|
-
}
|
|
11372
|
-
if (preset === "custom") {
|
|
11373
|
-
const missing = [];
|
|
11374
|
-
if (!customPage || !isPositivePoints(customPage.width))
|
|
11375
|
-
missing.push("page width");
|
|
11376
|
-
if (!customPage || !isPositivePoints(customPage.height))
|
|
11377
|
-
missing.push("page height");
|
|
11378
|
-
if (missing.length > 0) {
|
|
11379
|
-
throw new CreateProjectErrorImpl("custom-page-required", `The custom preset needs a trim size: ${missing.join(" and ")} in points ` + `(72pt = 1in — e.g. US Letter is 612 x 792).`);
|
|
11380
|
-
}
|
|
11381
|
-
} else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
|
|
11382
|
-
throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
|
|
11383
|
-
}
|
|
11384
|
-
if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
|
|
11385
|
-
throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
|
|
11404
|
+
function entryUse(item) {
|
|
11405
|
+
if (isScalar2(item))
|
|
11406
|
+
return typeof item.value === "string" ? item.value.trim() : null;
|
|
11407
|
+
if (isMap2(item)) {
|
|
11408
|
+
const u = item.get("use");
|
|
11409
|
+
return typeof u === "string" ? u.trim() : null;
|
|
11386
11410
|
}
|
|
11387
|
-
return
|
|
11411
|
+
return null;
|
|
11388
11412
|
}
|
|
11389
|
-
function
|
|
11390
|
-
const
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11413
|
+
function rawEntry(item) {
|
|
11414
|
+
const use = entryUse(item);
|
|
11415
|
+
if (!use)
|
|
11416
|
+
return null;
|
|
11417
|
+
if (!isMap2(item))
|
|
11418
|
+
return { use, enabled: true };
|
|
11419
|
+
const x = item.get("export");
|
|
11420
|
+
return {
|
|
11421
|
+
use,
|
|
11422
|
+
...typeof x === "string" && x.trim() ? { export: x.trim() } : {},
|
|
11423
|
+
enabled: item.get("enabled") !== false
|
|
11424
|
+
};
|
|
11396
11425
|
}
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
const slug = slugifyProjectName(name);
|
|
11403
|
-
const folderName = (options.folderName ?? slug).trim();
|
|
11404
|
-
if (!folderName || folderName !== slugifyProjectName(folderName)) {
|
|
11405
|
-
throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
11406
|
-
}
|
|
11407
|
-
const parentDir = options.parentDir;
|
|
11408
|
-
if (!parentDir || !path17.isAbsolute(parentDir)) {
|
|
11409
|
-
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
11410
|
-
}
|
|
11411
|
-
try {
|
|
11412
|
-
await access(parentDir, FS.W_OK);
|
|
11413
|
-
} catch {
|
|
11414
|
-
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
11415
|
-
}
|
|
11416
|
-
const projectDir = path17.join(parentDir, folderName);
|
|
11417
|
-
let targetExists = true;
|
|
11426
|
+
function sameExtension(a, b) {
|
|
11427
|
+
if (a.trim() === b.trim())
|
|
11428
|
+
return true;
|
|
11429
|
+
let pa;
|
|
11430
|
+
let pb;
|
|
11418
11431
|
try {
|
|
11419
|
-
|
|
11432
|
+
pa = parseExtensionSpecifier(a);
|
|
11433
|
+
pb = parseExtensionSpecifier(b);
|
|
11420
11434
|
} catch {
|
|
11421
|
-
|
|
11422
|
-
}
|
|
11423
|
-
if (targetExists) {
|
|
11424
|
-
throw new CreateProjectErrorImpl("target-exists", `A folder named "${folderName}" already exists here. Choose a different name or location.`);
|
|
11435
|
+
return false;
|
|
11425
11436
|
}
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
const
|
|
11430
|
-
const
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
|
|
11443
|
-
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11444
|
-
await mkdir8(path17.join(projectDir, "assets"), { recursive: true });
|
|
11445
|
-
await copyFile(tplManifest, path17.join(projectDir, MANIFEST_FILENAMES[0]));
|
|
11446
|
-
await copyFile(tplChapter, path17.join(projectDir, "chapter-01.md"));
|
|
11447
|
-
await mkdir8(path17.join(projectDir, "styles"), { recursive: true });
|
|
11448
|
-
await writeFile6(path17.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
|
|
11449
|
-
}
|
|
11450
|
-
} catch (e) {
|
|
11451
|
-
throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11437
|
+
return pa.kind === "npm" && pb.kind === "npm" && pa.name === pb.name;
|
|
11438
|
+
}
|
|
11439
|
+
function indexOfUse(seq, use) {
|
|
11440
|
+
const items = seq.items;
|
|
11441
|
+
const exact = items.findIndex((item) => entryUse(item) === use.trim());
|
|
11442
|
+
if (exact >= 0)
|
|
11443
|
+
return exact;
|
|
11444
|
+
return items.findIndex((item) => {
|
|
11445
|
+
const u = entryUse(item);
|
|
11446
|
+
return !!u && sameExtension(u, use);
|
|
11447
|
+
});
|
|
11448
|
+
}
|
|
11449
|
+
function manifestPathFor(projectDir, abs) {
|
|
11450
|
+
const rel = path18.relative(path18.resolve(projectDir), abs).split(path18.sep).join("/");
|
|
11451
|
+
if (rel === "") {
|
|
11452
|
+
throw new Error("An extension cannot be the project folder itself.");
|
|
11452
11453
|
}
|
|
11454
|
+
if (isPathSpecifier(rel))
|
|
11455
|
+
return rel;
|
|
11456
|
+
return `./${rel}`;
|
|
11457
|
+
}
|
|
11458
|
+
var NO_CARRIES = {
|
|
11459
|
+
markdown: false,
|
|
11460
|
+
styles: false,
|
|
11461
|
+
snippets: false,
|
|
11462
|
+
components: false
|
|
11463
|
+
};
|
|
11464
|
+
async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
|
|
11465
|
+
const meta = await readExtensionMeta(dir);
|
|
11466
|
+
const styles = extensionStyleList(meta);
|
|
11467
|
+
return {
|
|
11468
|
+
...base,
|
|
11469
|
+
label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
|
|
11470
|
+
...meta.description?.trim() ? { description: meta.description.trim() } : {},
|
|
11471
|
+
...meta.author?.trim() ? { author: meta.author.trim() } : {},
|
|
11472
|
+
...meta.preview !== undefined ? { preview: meta.preview } : {},
|
|
11473
|
+
...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
|
|
11474
|
+
...styles.length > 0 ? { styles } : {},
|
|
11475
|
+
carries: extensionCarries(meta),
|
|
11476
|
+
dir
|
|
11477
|
+
};
|
|
11478
|
+
}
|
|
11479
|
+
async function describeEntry(projectDir, raw) {
|
|
11480
|
+
const warnings = [];
|
|
11481
|
+
let parsed = null;
|
|
11453
11482
|
try {
|
|
11454
|
-
|
|
11455
|
-
} catch (
|
|
11456
|
-
|
|
11483
|
+
parsed = parseExtensionSpecifier(raw.use);
|
|
11484
|
+
} catch (error2) {
|
|
11485
|
+
warnings.push(error2 instanceof Error ? error2.message : String(error2));
|
|
11457
11486
|
}
|
|
11458
|
-
const
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
11487
|
+
const common = {
|
|
11488
|
+
use: raw.use,
|
|
11489
|
+
enabled: raw.enabled,
|
|
11490
|
+
...raw.export ? { export: raw.export } : {}
|
|
11463
11491
|
};
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
if (!customTemplateDir) {
|
|
11468
|
-
await addBuiltInStyleSet(projectDir, starterThemeId);
|
|
11469
|
-
}
|
|
11470
|
-
if (preset) {
|
|
11471
|
-
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11472
|
-
doc.set("preset", preset);
|
|
11473
|
-
doc.set("targets", doc.createNode(targets ?? []));
|
|
11474
|
-
if (options.customPage) {
|
|
11475
|
-
const page = {
|
|
11476
|
-
width: options.customPage.width,
|
|
11477
|
-
height: options.customPage.height
|
|
11478
|
-
};
|
|
11479
|
-
if (options.customPage.tolerance !== undefined) {
|
|
11480
|
-
page.tolerance = options.customPage.tolerance;
|
|
11481
|
-
}
|
|
11482
|
-
doc.set("page", doc.createNode(page));
|
|
11483
|
-
}
|
|
11484
|
-
await writeManifestDoc(file, doc);
|
|
11485
|
-
}
|
|
11486
|
-
const firstSource = await firstSourceFile(manifestPath);
|
|
11487
|
-
if (firstSource)
|
|
11488
|
-
openFile = path17.join(projectDir, firstSource);
|
|
11489
|
-
await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
|
|
11490
|
-
} catch (e) {
|
|
11491
|
-
throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11492
|
+
const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
11493
|
+
if (!parsed) {
|
|
11494
|
+
return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
|
|
11492
11495
|
}
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11496
|
+
if (parsed.kind === "bundled") {
|
|
11497
|
+
const bundledName = parsed.name;
|
|
11498
|
+
const rec = RECOMMENDED_EXTENSIONS.find((r) => r.use === bundledName);
|
|
11499
|
+
return withWarnings({
|
|
11500
|
+
...common,
|
|
11501
|
+
kind: "bundled",
|
|
11502
|
+
name: bundledName,
|
|
11503
|
+
label: rec?.label ?? bundledName,
|
|
11504
|
+
...rec ? { description: rec.description } : {},
|
|
11505
|
+
carries: { ...NO_CARRIES, markdown: true }
|
|
11506
|
+
});
|
|
11507
|
+
}
|
|
11508
|
+
if (parsed.kind === "path") {
|
|
11509
|
+
const abs = path18.resolve(projectDir, parsed.path);
|
|
11510
|
+
const base2 = { ...common, kind: "path", name: parsed.path };
|
|
11511
|
+
let info2;
|
|
11497
11512
|
try {
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
versionHistory = "local-git";
|
|
11506
|
-
} catch (e) {
|
|
11507
|
-
versionHistory = "none";
|
|
11508
|
-
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
11509
|
-
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
11510
|
-
versionHistoryError = enclosingRepoDir ? `This book was added to ${path17.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
|
|
11513
|
+
info2 = await stat6(abs);
|
|
11514
|
+
} catch {
|
|
11515
|
+
warnings.push(`Not found: ${abs}`);
|
|
11516
|
+
return withWarnings({ ...base2, label: path18.basename(abs), carries: NO_CARRIES });
|
|
11517
|
+
}
|
|
11518
|
+
if (info2.isDirectory()) {
|
|
11519
|
+
return withWarnings(await describeFolder(abs, base2, prettify(path18.basename(abs))));
|
|
11511
11520
|
}
|
|
11521
|
+
return withWarnings({
|
|
11522
|
+
...base2,
|
|
11523
|
+
label: path18.basename(abs),
|
|
11524
|
+
carries: { ...NO_CARRIES, markdown: true }
|
|
11525
|
+
});
|
|
11512
11526
|
}
|
|
11513
|
-
const
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11527
|
+
const base = {
|
|
11528
|
+
...common,
|
|
11529
|
+
kind: "npm",
|
|
11530
|
+
name: parsed.name,
|
|
11531
|
+
...parsed.version ? { version: parsed.version } : {}
|
|
11518
11532
|
};
|
|
11519
|
-
if (
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
return result;
|
|
11523
|
-
}
|
|
11524
|
-
async function ensureGitignoreHasDist(projectDir) {
|
|
11525
|
-
const gitignorePath = path17.join(projectDir, ".gitignore");
|
|
11526
|
-
let existing = null;
|
|
11527
|
-
try {
|
|
11528
|
-
existing = await readFile27(gitignorePath, "utf8");
|
|
11529
|
-
} catch {}
|
|
11530
|
-
if (existing === null) {
|
|
11531
|
-
await writeFile6(gitignorePath, `dist/
|
|
11532
|
-
`, "utf8");
|
|
11533
|
-
return;
|
|
11533
|
+
if (!parsed.version) {
|
|
11534
|
+
warnings.push(`Not pinned — run \`gutterpress ext add ${parsed.name}\` to install it and pin an exact version.`);
|
|
11535
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11534
11536
|
}
|
|
11535
|
-
|
|
11536
|
-
`).some((line) => /^\/?dist\/?$/.test(line.trim()));
|
|
11537
|
-
if (alreadyIgnoresDist)
|
|
11538
|
-
return;
|
|
11539
|
-
const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
|
|
11540
|
-
`);
|
|
11541
|
-
await writeFile6(gitignorePath, existing + (needsLeadingNewline ? `
|
|
11542
|
-
` : "") + `dist/
|
|
11543
|
-
`, "utf8");
|
|
11544
|
-
}
|
|
11545
|
-
async function firstSourceFile(manifestPath) {
|
|
11546
|
-
let text;
|
|
11537
|
+
let installRoot = null;
|
|
11547
11538
|
try {
|
|
11548
|
-
|
|
11549
|
-
} catch {
|
|
11550
|
-
|
|
11551
|
-
}
|
|
11552
|
-
const filesIdx = text.search(/^\s*files:\s*$/m);
|
|
11553
|
-
if (filesIdx === -1)
|
|
11554
|
-
return;
|
|
11555
|
-
const rest = text.slice(filesIdx);
|
|
11556
|
-
const m = rest.match(/^\s*-\s*"?([^"\n]+?)"?\s*$/m);
|
|
11557
|
-
return m ? m[1].trim() : undefined;
|
|
11558
|
-
}
|
|
11559
|
-
async function fillTemplateFile(filePath, substitutions) {
|
|
11560
|
-
let text = await readFile27(filePath, "utf8");
|
|
11561
|
-
for (const [token, value] of Object.entries(substitutions)) {
|
|
11562
|
-
text = text.split(token).join(value);
|
|
11539
|
+
installRoot = await resolveVendoredPluginInstallRoot(projectDir, parsed.name, parsed.version);
|
|
11540
|
+
} catch (error2) {
|
|
11541
|
+
warnings.push(error2 instanceof Error ? error2.message : String(error2));
|
|
11563
11542
|
}
|
|
11564
|
-
|
|
11565
|
-
}
|
|
11566
|
-
|
|
11567
|
-
// src/lib/extension-scaffold.ts
|
|
11568
|
-
import { access as access2, mkdir as mkdir9, readFile as readFile28, writeFile as writeFile7 } from "node:fs/promises";
|
|
11569
|
-
import { constants as FS2 } from "node:fs";
|
|
11570
|
-
import path18 from "node:path";
|
|
11571
|
-
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
11572
|
-
var RESERVED_PREFIX = "gp-";
|
|
11573
|
-
var TEMPLATE_FILES = {
|
|
11574
|
-
plugin: [
|
|
11575
|
-
["package.json", "package.json"],
|
|
11576
|
-
["README.md", "README.md"],
|
|
11577
|
-
["plugin.js.tpl", "plugin.js"],
|
|
11578
|
-
["styles/plugin.css", "styles/plugin.css"],
|
|
11579
|
-
["snippets/term-box.md", "snippets/term-box.md"],
|
|
11580
|
-
["test/fixture.md", "test/fixture.md"],
|
|
11581
|
-
["test/expected.html", "test/expected.html"],
|
|
11582
|
-
["test/plugin.test.js.tpl", "test/plugin.test.js"]
|
|
11583
|
-
],
|
|
11584
|
-
theme: [
|
|
11585
|
-
["package.json", "package.json"],
|
|
11586
|
-
["README.md", "README.md"],
|
|
11587
|
-
["components.yaml", "components.yaml"],
|
|
11588
|
-
["snippets/callout.md", "snippets/callout.md"],
|
|
11589
|
-
["styles/tokens.css", "styles/tokens.css"],
|
|
11590
|
-
["styles/base.css", "styles/base.css"],
|
|
11591
|
-
["styles/components.css", "styles/components.css"],
|
|
11592
|
-
["styles/page-templates.css", "styles/page-templates.css"],
|
|
11593
|
-
["styles/page-rules.css", "styles/page-rules.css"],
|
|
11594
|
-
["styles/book.css", "styles/book.css"]
|
|
11595
|
-
]
|
|
11596
|
-
};
|
|
11597
|
-
var OPEN_FILE = {
|
|
11598
|
-
plugin: "plugin.js",
|
|
11599
|
-
theme: "styles/tokens.css"
|
|
11600
|
-
};
|
|
11601
|
-
var DEFAULT_DESCRIPTION = {
|
|
11602
|
-
plugin: "A Gutterpress plugin.",
|
|
11603
|
-
theme: "A Gutterpress theme."
|
|
11604
|
-
};
|
|
11605
|
-
var DEFAULT_AUTHOR2 = "Anonymous";
|
|
11606
|
-
|
|
11607
|
-
class ExtensionScaffoldError extends Error {
|
|
11608
|
-
code;
|
|
11609
|
-
constructor(code, message) {
|
|
11610
|
-
super(message);
|
|
11611
|
-
this.name = "CreateProjectError";
|
|
11612
|
-
this.code = code;
|
|
11543
|
+
if (!installRoot) {
|
|
11544
|
+
warnings.push(`Not installed — run \`gutterpress ext add ${raw.use}\`.`);
|
|
11545
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11613
11546
|
}
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
const prefix = raw.endsWith("-") ? raw : `${raw}-`;
|
|
11618
|
-
if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
|
|
11619
|
-
throw new ExtensionScaffoldError("invalid-name", `"${raw}" is not a usable prefix. Use lower-case letters, digits and hyphens, ` + `starting with a letter — for example "${slug}-" or "fn-".`);
|
|
11547
|
+
const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
|
|
11548
|
+
if (!existsSync12(path18.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
|
|
11549
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11620
11550
|
}
|
|
11621
|
-
|
|
11622
|
-
|
|
11551
|
+
const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
|
|
11552
|
+
return withWarnings({
|
|
11553
|
+
...described,
|
|
11554
|
+
carries: { ...described.carries, markdown: true }
|
|
11555
|
+
});
|
|
11556
|
+
}
|
|
11557
|
+
async function listProjectExtensions(projectDir) {
|
|
11558
|
+
const { doc } = await loadManifestDoc(projectDir);
|
|
11559
|
+
const seq = doc.get("extensions", true);
|
|
11560
|
+
if (!isSeq3(seq))
|
|
11561
|
+
return [];
|
|
11562
|
+
const out = [];
|
|
11563
|
+
for (const item of seq.items) {
|
|
11564
|
+
const raw = rawEntry(item);
|
|
11565
|
+
if (!raw)
|
|
11566
|
+
continue;
|
|
11567
|
+
out.push(await describeEntry(projectDir, raw));
|
|
11623
11568
|
}
|
|
11624
|
-
return
|
|
11569
|
+
return out;
|
|
11625
11570
|
}
|
|
11626
|
-
function
|
|
11627
|
-
|
|
11628
|
-
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
11571
|
+
async function describeExtension(projectDir, use) {
|
|
11572
|
+
return (await listProjectExtensions(projectDir)).find((e) => sameExtension(e.use, use)) ?? null;
|
|
11629
11573
|
}
|
|
11630
|
-
function
|
|
11631
|
-
return
|
|
11574
|
+
function extensionConfigFor(entry) {
|
|
11575
|
+
return {
|
|
11576
|
+
use: entry.use,
|
|
11577
|
+
...entry.kind === "path" ? { path: entry.name } : { name: entry.name, ...entry.version ? { version: entry.version } : {} },
|
|
11578
|
+
...entry.export ? { export: entry.export } : {},
|
|
11579
|
+
options: {}
|
|
11580
|
+
};
|
|
11632
11581
|
}
|
|
11633
|
-
async function
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11582
|
+
async function setExtensionEnabled(projectDir, use, enabled) {
|
|
11583
|
+
return withMutationLock(projectDir, async () => {
|
|
11584
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11585
|
+
const seq = doc.get("extensions", true);
|
|
11586
|
+
const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
|
|
11587
|
+
if (!isSeq3(seq) || idx < 0)
|
|
11588
|
+
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
11589
|
+
const item = seq.items[idx];
|
|
11590
|
+
if (enabled) {
|
|
11591
|
+
if (isMap2(item)) {
|
|
11592
|
+
item.delete("enabled");
|
|
11593
|
+
if (item.items.length === 1 && typeof item.get("use") === "string") {
|
|
11594
|
+
seq.items[idx] = new Scalar(item.get("use"));
|
|
11595
|
+
}
|
|
11596
|
+
}
|
|
11597
|
+
} else if (isMap2(item)) {
|
|
11598
|
+
item.set("enabled", false);
|
|
11599
|
+
} else {
|
|
11600
|
+
const map = new YAMLMap(doc.schema);
|
|
11601
|
+
map.set("use", entryUse(item));
|
|
11602
|
+
map.set("enabled", false);
|
|
11603
|
+
seq.items[idx] = map;
|
|
11604
|
+
}
|
|
11605
|
+
await writeManifestDoc(file, doc);
|
|
11606
|
+
});
|
|
11607
|
+
}
|
|
11608
|
+
async function removeExtension(projectDir, use) {
|
|
11609
|
+
return withMutationLock(projectDir, async () => {
|
|
11610
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11611
|
+
const seq = doc.get("extensions", true);
|
|
11612
|
+
const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
|
|
11613
|
+
if (!isSeq3(seq) || idx < 0)
|
|
11614
|
+
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
11615
|
+
const written = entryUse(seq.items[idx]);
|
|
11616
|
+
seq.items.splice(idx, 1);
|
|
11617
|
+
if (seq.items.length === 0)
|
|
11618
|
+
doc.delete("extensions");
|
|
11619
|
+
await writeManifestDoc(file, doc);
|
|
11620
|
+
let parsed = null;
|
|
11621
|
+
try {
|
|
11622
|
+
parsed = parseExtensionSpecifier(written);
|
|
11623
|
+
} catch {
|
|
11624
|
+
return;
|
|
11625
|
+
}
|
|
11626
|
+
if (parsed.kind === "npm" && parsed.version) {
|
|
11627
|
+
await rm4(vendoredNpmPluginRoot(projectDir, parsed.name, parsed.version), {
|
|
11628
|
+
recursive: true,
|
|
11629
|
+
force: true
|
|
11630
|
+
});
|
|
11631
|
+
clearVendoredPluginResolver(projectDir, parsed.name, parsed.version);
|
|
11632
|
+
}
|
|
11633
|
+
});
|
|
11634
|
+
}
|
|
11635
|
+
async function upsertEntry(projectDir, use, exportName) {
|
|
11636
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11637
|
+
const seq = ensureSeq(doc, "extensions");
|
|
11638
|
+
const idx = indexOfUse(seq, use);
|
|
11639
|
+
if (idx >= 0) {
|
|
11640
|
+
const item = seq.items[idx];
|
|
11641
|
+
if (isMap2(item)) {
|
|
11642
|
+
item.set("use", use);
|
|
11643
|
+
if (exportName)
|
|
11644
|
+
item.set("export", exportName);
|
|
11645
|
+
else
|
|
11646
|
+
item.delete("export");
|
|
11647
|
+
if (item.items.length === 1)
|
|
11648
|
+
seq.items[idx] = new Scalar(use);
|
|
11649
|
+
} else if (exportName) {
|
|
11650
|
+
const map = new YAMLMap(doc.schema);
|
|
11651
|
+
map.set("use", use);
|
|
11652
|
+
map.set("export", exportName);
|
|
11653
|
+
seq.items[idx] = map;
|
|
11654
|
+
} else {
|
|
11655
|
+
seq.items[idx] = new Scalar(use);
|
|
11656
|
+
}
|
|
11657
|
+
} else if (exportName) {
|
|
11658
|
+
const map = new YAMLMap(doc.schema);
|
|
11659
|
+
map.set("use", use);
|
|
11660
|
+
map.set("export", exportName);
|
|
11661
|
+
seq.add(map);
|
|
11662
|
+
} else {
|
|
11663
|
+
seq.add(new Scalar(use));
|
|
11650
11664
|
}
|
|
11665
|
+
await writeManifestDoc(file, doc);
|
|
11666
|
+
}
|
|
11667
|
+
async function addPathExtension(projectDir, abs, exportName) {
|
|
11651
11668
|
try {
|
|
11652
|
-
await
|
|
11669
|
+
await stat6(abs);
|
|
11653
11670
|
} catch {
|
|
11654
|
-
throw new
|
|
11671
|
+
throw new Error(`Extension not found: ${abs}`);
|
|
11655
11672
|
}
|
|
11656
|
-
const
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11673
|
+
const use = manifestPathFor(projectDir, abs);
|
|
11674
|
+
await loadPlugin(extensionConfigFor({ use, kind: "path", name: use, export: exportName }), projectDir);
|
|
11675
|
+
await upsertEntry(projectDir, use, exportName);
|
|
11676
|
+
return await describeExtension(projectDir, use);
|
|
11677
|
+
}
|
|
11678
|
+
async function addExtension(projectDir, specifier, options = {}) {
|
|
11679
|
+
return withMutationLock(projectDir, () => addExtensionUnlocked(projectDir, specifier, options));
|
|
11680
|
+
}
|
|
11681
|
+
async function addExtensionUnlocked(projectDir, specifier, options) {
|
|
11682
|
+
const input = specifier.trim();
|
|
11683
|
+
const exportName = options.exportName?.trim() || undefined;
|
|
11684
|
+
if (path18.isAbsolute(input))
|
|
11685
|
+
return addPathExtension(projectDir, input, exportName);
|
|
11686
|
+
const parsed = parseExtensionSpecifier(input);
|
|
11687
|
+
if (parsed.kind === "path") {
|
|
11688
|
+
return addPathExtension(projectDir, path18.resolve(projectDir, parsed.path), exportName);
|
|
11662
11689
|
}
|
|
11663
|
-
if (
|
|
11664
|
-
|
|
11690
|
+
if (parsed.kind === "bundled") {
|
|
11691
|
+
if (exportName)
|
|
11692
|
+
throw new Error(`Bundled extension "${parsed.name}" does not take a named export.`);
|
|
11693
|
+
await upsertEntry(projectDir, parsed.name);
|
|
11694
|
+
return await describeExtension(projectDir, parsed.name);
|
|
11665
11695
|
}
|
|
11666
|
-
const
|
|
11667
|
-
const
|
|
11668
|
-
const
|
|
11669
|
-
|
|
11670
|
-
"{{SLUG}}": slug,
|
|
11671
|
-
"{{PREFIX}}": prefix,
|
|
11672
|
-
"{{AUTHOR}}": author,
|
|
11673
|
-
"{{DESCRIPTION}}": description,
|
|
11674
|
-
"{{IDENT}}": identifierFor(slug)
|
|
11675
|
-
};
|
|
11676
|
-
const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
|
|
11677
|
-
const entries = TEMPLATE_FILES[kind];
|
|
11678
|
-
const written = [];
|
|
11696
|
+
const existing = await describeExtension(projectDir, parsed.name);
|
|
11697
|
+
const chosenExport = exportName ?? existing?.export;
|
|
11698
|
+
const installed = await installNpmPlugin(projectDir, input, options);
|
|
11699
|
+
const use = pinnedNpmSpecifier(installed.name, installed.version);
|
|
11679
11700
|
try {
|
|
11680
|
-
await
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11701
|
+
await loadPlugin(extensionConfigFor({
|
|
11702
|
+
use,
|
|
11703
|
+
kind: "npm",
|
|
11704
|
+
name: installed.name,
|
|
11705
|
+
version: installed.version,
|
|
11706
|
+
export: chosenExport
|
|
11707
|
+
}), projectDir);
|
|
11708
|
+
await options.__testFailBeforeManifestCommit?.();
|
|
11709
|
+
await upsertEntry(projectDir, use, chosenExport);
|
|
11710
|
+
} catch (cause) {
|
|
11711
|
+
try {
|
|
11712
|
+
await rollbackNpmPluginInstall(installed);
|
|
11713
|
+
clearVendoredPluginResolver(projectDir, installed.name, installed.version);
|
|
11714
|
+
} catch (rollbackError) {
|
|
11715
|
+
throw new Error(`Extension install failed and its previous vendor tree could not be restored: ` + `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause });
|
|
11689
11716
|
}
|
|
11690
|
-
|
|
11691
|
-
throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11717
|
+
throw new Error(`Downloaded ${use}, but it is not a loadable markdown-it plugin: ` + `${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
11692
11718
|
}
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
openFile: path18.join(extensionDir, OPEN_FILE[kind]),
|
|
11700
|
-
files: written
|
|
11701
|
-
};
|
|
11719
|
+
const warnings = [...installed.warnings];
|
|
11720
|
+
await finalizeNpmPluginInstall(installed).catch((error2) => {
|
|
11721
|
+
warnings.push(`The extension was installed, but an old backup could not be removed: ` + `${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
11722
|
+
});
|
|
11723
|
+
const entry = await describeExtension(projectDir, use);
|
|
11724
|
+
return warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
11702
11725
|
}
|
|
11703
|
-
function
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11726
|
+
async function uniqueExtensionId(projectDir, base) {
|
|
11727
|
+
const slug = slugify(base) || "extension";
|
|
11728
|
+
let candidate = slug;
|
|
11729
|
+
for (let n = 2;existsSync12(path18.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
|
|
11730
|
+
candidate = `${slug}-${n}`;
|
|
11707
11731
|
}
|
|
11708
|
-
return
|
|
11732
|
+
return candidate;
|
|
11733
|
+
}
|
|
11734
|
+
async function addBuiltInStyleSet(projectDir, id) {
|
|
11735
|
+
if (!BUILT_IN_STYLE_SET_IDS.includes(id)) {
|
|
11736
|
+
throw new Error(`Unknown built-in look "${id}" (one of: ${BUILT_IN_STYLE_SET_IDS.join(", ")}).`);
|
|
11737
|
+
}
|
|
11738
|
+
return withMutationLock(projectDir, async () => {
|
|
11739
|
+
const dest = path18.join(projectDir, EXTENSIONS_DIR, id);
|
|
11740
|
+
if (!existsSync12(dest)) {
|
|
11741
|
+
const src = path18.join(await getAssetsDir(), "themes", id);
|
|
11742
|
+
await mkdir9(path18.dirname(dest), { recursive: true });
|
|
11743
|
+
await cp3(src, dest, { recursive: true });
|
|
11744
|
+
}
|
|
11745
|
+
return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
|
|
11746
|
+
});
|
|
11709
11747
|
}
|
|
11710
|
-
|
|
11711
|
-
// src/lib/project-templates.ts
|
|
11712
|
-
import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
|
|
11713
|
-
var BUILT_IN_TEMPLATE_IDS = [
|
|
11714
|
-
"book",
|
|
11715
|
-
"zine",
|
|
11716
|
-
"technical"
|
|
11717
|
-
];
|
|
11718
|
-
var SKIP_ENTRIES = new Set([
|
|
11719
|
-
".git",
|
|
11720
|
-
"node_modules",
|
|
11721
|
-
"out",
|
|
11722
|
-
"dist",
|
|
11723
|
-
"build",
|
|
11724
|
-
".gutterpress",
|
|
11725
|
-
".DS_Store"
|
|
11726
|
-
]);
|
|
11727
11748
|
|
|
11728
11749
|
// src/lib/extension-import.ts
|
|
11729
11750
|
import { existsSync as existsSync13 } from "node:fs";
|
|
@@ -14242,4 +14263,4 @@ async function runLint(opts = {}) {
|
|
|
14242
14263
|
}
|
|
14243
14264
|
return { ok: true, riskyCount, containmentCount, filesLinted: linted };
|
|
14244
14265
|
}
|
|
14245
|
-
export { isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, loadManifestWithPath, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer,
|
|
14266
|
+
export { isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, loadManifestWithPath, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, scaffoldProject, EXTENSION_KINDS, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, BUILT_IN_STYLE_SET_IDS, listProjectExtensions, setExtensionEnabled, removeExtension, addExtension, addBuiltInStyleSet, importExtensionFromFile, importExtensionFromUrl, searchNpmExtensions, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listPublishProviders, publishProviderFor, resolvePublishFormat, runPublish, connectPublishProvider, disconnectPublishCredential, runLint };
|