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
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
resolveChromiumExecutable,
|
|
19
19
|
run,
|
|
20
20
|
spawnCapture
|
|
21
|
-
} from "./index-
|
|
21
|
+
} from "./index-6n15e1ze.js";
|
|
22
22
|
import {
|
|
23
23
|
gitFs,
|
|
24
24
|
gitScopeFor,
|
|
@@ -3115,14 +3115,30 @@ function applyPlugins(md, plugins) {
|
|
|
3115
3115
|
}
|
|
3116
3116
|
}
|
|
3117
3117
|
}
|
|
3118
|
-
function collectPluginCss(plugins) {
|
|
3119
|
-
return plugins.map((p) => p.css).filter((css) => typeof css === "string" && css.length > 0).join(`
|
|
3120
|
-
|
|
3121
|
-
`);
|
|
3122
|
-
}
|
|
3123
3118
|
function collectPluginStylePaths(plugins) {
|
|
3124
3119
|
return plugins.flatMap((p) => p.styles ?? []);
|
|
3125
3120
|
}
|
|
3121
|
+
function collectPluginStyleGroups(plugins) {
|
|
3122
|
+
const taken = new Set;
|
|
3123
|
+
const groups = [];
|
|
3124
|
+
for (const p of plugins) {
|
|
3125
|
+
const paths = p.styles ?? [];
|
|
3126
|
+
const css = typeof p.css === "string" && p.css.length > 0 ? p.css : undefined;
|
|
3127
|
+
if (paths.length === 0 && !css)
|
|
3128
|
+
continue;
|
|
3129
|
+
const base = `ext.${layerSlug(p.name)}`;
|
|
3130
|
+
let layer = base;
|
|
3131
|
+
for (let n = 2;taken.has(layer); n++)
|
|
3132
|
+
layer = `${base}-${n}`;
|
|
3133
|
+
taken.add(layer);
|
|
3134
|
+
groups.push({ name: p.name, layer, paths, ...css ? { css } : {} });
|
|
3135
|
+
}
|
|
3136
|
+
return groups;
|
|
3137
|
+
}
|
|
3138
|
+
function layerSlug(name) {
|
|
3139
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3140
|
+
return /^[a-z]/.test(slug) ? slug : `x${slug}`;
|
|
3141
|
+
}
|
|
3126
3142
|
|
|
3127
3143
|
// src/lib/markdown/assemble.ts
|
|
3128
3144
|
async function assembleBookHtml(opts) {
|
|
@@ -3180,7 +3196,7 @@ ${MARKER_CSS.trim()}
|
|
|
3180
3196
|
@layer gp.vocab {
|
|
3181
3197
|
${GUTTERPRESS_CSS.trim()}
|
|
3182
3198
|
}`,
|
|
3183
|
-
pluginCss ? `/*
|
|
3199
|
+
pluginCss ? `/* extension css */
|
|
3184
3200
|
${pluginCss.trim()}` : null,
|
|
3185
3201
|
projectCss ? `/* project css */
|
|
3186
3202
|
${projectCss.trim()}` : null
|
|
@@ -3611,16 +3627,34 @@ async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
|
|
|
3611
3627
|
async function renderChapters(inputDir, opts = {}) {
|
|
3612
3628
|
const styles = await resolveActiveStyles(inputDir, opts.styles);
|
|
3613
3629
|
const inlined = await inlineStyles(inputDir, styles);
|
|
3614
|
-
const
|
|
3615
|
-
const
|
|
3630
|
+
const blocks = [];
|
|
3631
|
+
const layers = [];
|
|
3632
|
+
const styleWarnings = [];
|
|
3633
|
+
const cssAssetCopies = [];
|
|
3634
|
+
for (const group of opts.pluginStyles ?? []) {
|
|
3635
|
+
const groupInlined = await inlineStyles(inputDir, group.paths);
|
|
3636
|
+
styleWarnings.push(...groupInlined.warnings);
|
|
3637
|
+
cssAssetCopies.push(...groupInlined.copies);
|
|
3638
|
+
const css = [groupInlined.css, group.css].filter((s) => !!s && s.trim().length > 0).join(`
|
|
3639
|
+
|
|
3640
|
+
`);
|
|
3641
|
+
if (!css)
|
|
3642
|
+
continue;
|
|
3643
|
+
layers.push(group.layer);
|
|
3644
|
+
blocks.push(`/* ${group.name} */
|
|
3645
|
+
@layer ${group.layer} {
|
|
3646
|
+
${css.trim()}
|
|
3647
|
+
}`);
|
|
3648
|
+
}
|
|
3649
|
+
styleWarnings.push(...inlined.warnings);
|
|
3616
3650
|
if (styleWarnings.length > 0)
|
|
3617
3651
|
opts.onStyleWarnings?.(styleWarnings);
|
|
3618
|
-
|
|
3652
|
+
cssAssetCopies.push(...inlined.copies);
|
|
3619
3653
|
if (cssAssetCopies.length > 0)
|
|
3620
3654
|
opts.onCssAssets?.(cssAssetCopies);
|
|
3621
|
-
const pluginCss =
|
|
3655
|
+
const pluginCss = blocks.length > 0 ? [`@layer ${layers.join(", ")};`, ...blocks].join(`
|
|
3622
3656
|
|
|
3623
|
-
`);
|
|
3657
|
+
`) : "";
|
|
3624
3658
|
const files = await resolveActiveMarkdownFiles(inputDir, opts.files);
|
|
3625
3659
|
if (files.length === 0) {
|
|
3626
3660
|
throw new Error(`No markdown files found in ${inputDir}`);
|
|
@@ -3629,7 +3663,7 @@ async function renderChapters(inputDir, opts = {}) {
|
|
|
3629
3663
|
files,
|
|
3630
3664
|
readText: (relPath) => readFile4(join(inputDir, canonicalChapterId(relPath)), "utf-8"),
|
|
3631
3665
|
projectCss: inlined.css,
|
|
3632
|
-
preloadImages:
|
|
3666
|
+
preloadImages: cssAssetCopies.map((c) => c.to),
|
|
3633
3667
|
title: opts.title,
|
|
3634
3668
|
plugins: opts.plugins,
|
|
3635
3669
|
pluginCss,
|
|
@@ -3646,8 +3680,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
|
|
|
3646
3680
|
styles: opts.styles,
|
|
3647
3681
|
files: opts.files,
|
|
3648
3682
|
plugins: opts.plugins,
|
|
3649
|
-
|
|
3650
|
-
pluginStylePaths: opts.pluginStylePaths,
|
|
3683
|
+
pluginStyles: opts.pluginStyles,
|
|
3651
3684
|
onChapterWarnings: opts.onChapterWarnings,
|
|
3652
3685
|
onImageRefs: opts.onImageRefs,
|
|
3653
3686
|
onCssAssets: opts.onCssAssets,
|
|
@@ -4569,12 +4602,12 @@ async function loadPlugins(configs, baseDir, onError) {
|
|
|
4569
4602
|
}
|
|
4570
4603
|
async function loadPluginsWithCss(configs, baseDir, onError) {
|
|
4571
4604
|
if (!configs || configs.length === 0) {
|
|
4572
|
-
return { plugins: undefined,
|
|
4605
|
+
return { plugins: undefined, pluginStyles: [], pluginStylePaths: [] };
|
|
4573
4606
|
}
|
|
4574
4607
|
const plugins = await loadPlugins(configs, baseDir, onError);
|
|
4575
4608
|
return {
|
|
4576
4609
|
plugins,
|
|
4577
|
-
|
|
4610
|
+
pluginStyles: collectPluginStyleGroups(plugins),
|
|
4578
4611
|
pluginStylePaths: collectPluginStylePaths(plugins)
|
|
4579
4612
|
};
|
|
4580
4613
|
}
|
|
@@ -4841,7 +4874,7 @@ import git from "isomorphic-git";
|
|
|
4841
4874
|
// package.json
|
|
4842
4875
|
var package_default = {
|
|
4843
4876
|
name: "gutterpress",
|
|
4844
|
-
version: "0.
|
|
4877
|
+
version: "0.11.1",
|
|
4845
4878
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
4846
4879
|
author: "itlackey",
|
|
4847
4880
|
license: "MPL-2.0",
|
|
@@ -9070,7 +9103,7 @@ async function renderBook(ctx) {
|
|
|
9070
9103
|
} else {
|
|
9071
9104
|
log.info("Using all .md files in alphabetical order");
|
|
9072
9105
|
}
|
|
9073
|
-
const { plugins,
|
|
9106
|
+
const { plugins, pluginStyles } = await loadBuildPlugins(ctx);
|
|
9074
9107
|
const imageRefs = [];
|
|
9075
9108
|
const cssAssets = [];
|
|
9076
9109
|
const htmlFile = await renderChaptersToFile(renderDir, workDir, {
|
|
@@ -9078,8 +9111,7 @@ async function renderBook(ctx) {
|
|
|
9078
9111
|
styles: config.styles,
|
|
9079
9112
|
files: config.source.files,
|
|
9080
9113
|
plugins,
|
|
9081
|
-
|
|
9082
|
-
pluginStylePaths,
|
|
9114
|
+
pluginStyles,
|
|
9083
9115
|
onChapterWarnings: (file, warnings) => {
|
|
9084
9116
|
for (const w of warnings) {
|
|
9085
9117
|
const key = layoutWarningKey(path9.resolve(renderDir, file), w.line, w.message);
|
|
@@ -9225,7 +9257,7 @@ class PdfOutput {
|
|
|
9225
9257
|
const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
|
|
9226
9258
|
await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
|
|
9227
9259
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
9228
|
-
const { buildNativePdf } = await import("./engine-
|
|
9260
|
+
const { buildNativePdf } = await import("./engine-w5ed5hmb.js");
|
|
9229
9261
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
9230
9262
|
title: config.title,
|
|
9231
9263
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
@@ -9362,14 +9394,13 @@ function incrementalPreviewEnabled() {
|
|
|
9362
9394
|
return process.env.GUTTERPRESS_PREVIEW_INCREMENTAL !== "0";
|
|
9363
9395
|
}
|
|
9364
9396
|
async function renderPreviewBook(inputPath, config, opts) {
|
|
9365
|
-
const { plugins,
|
|
9397
|
+
const { plugins, pluginStyles } = await loadPluginsWithCss(config.extensions, inputPath, (ref, err) => warn2(`Skipping plugin "${ref}" in preview — ${err.message}`));
|
|
9366
9398
|
return renderChapters(inputPath, {
|
|
9367
9399
|
title: config.title ?? "Document",
|
|
9368
9400
|
styles: config.styles,
|
|
9369
9401
|
files: opts.files,
|
|
9370
9402
|
plugins,
|
|
9371
|
-
|
|
9372
|
-
pluginStylePaths,
|
|
9403
|
+
pluginStyles,
|
|
9373
9404
|
wrapChapters: opts.wrapChapters,
|
|
9374
9405
|
annotateSourceChapters: true,
|
|
9375
9406
|
...opts.onCssAssets ? { onCssAssets: opts.onCssAssets } : {},
|
|
@@ -10888,9 +10919,9 @@ async function getSystemDiagnostics() {
|
|
|
10888
10919
|
};
|
|
10889
10920
|
}
|
|
10890
10921
|
// src/lib/project-scaffold.ts
|
|
10891
|
-
import { access, copyFile, cp as
|
|
10892
|
-
import { constants as FS, existsSync as
|
|
10893
|
-
import
|
|
10922
|
+
import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile27, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
|
|
10923
|
+
import { constants as FS, existsSync as existsSync12 } from "node:fs";
|
|
10924
|
+
import path16 from "node:path";
|
|
10894
10925
|
|
|
10895
10926
|
// src/lib/manifest-doc.ts
|
|
10896
10927
|
import { existsSync as existsSync11 } from "node:fs";
|
|
@@ -10952,1916 +10983,1901 @@ function scalarString(item) {
|
|
|
10952
10983
|
return typeof item === "string" ? item : null;
|
|
10953
10984
|
}
|
|
10954
10985
|
|
|
10955
|
-
// src/lib/
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
mkdtemp as mkdtemp2,
|
|
10968
|
-
open as open2,
|
|
10969
|
-
readFile as readFile27,
|
|
10970
|
-
realpath as realpath2,
|
|
10971
|
-
rename as rename4,
|
|
10972
|
-
rm as rm3
|
|
10973
|
-
} from "node:fs/promises";
|
|
10974
|
-
import path16 from "node:path";
|
|
10975
|
-
import { Transform } from "node:stream";
|
|
10976
|
-
import { pipeline } from "node:stream/promises";
|
|
10977
|
-
import { createGunzip } from "node:zlib";
|
|
10978
|
-
import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
|
|
10979
|
-
|
|
10980
|
-
// src/lib/fetch-timeout.ts
|
|
10981
|
-
class FetchPassthroughError extends Error {
|
|
10982
|
-
}
|
|
10983
|
-
|
|
10984
|
-
class FriendlyHttpError extends FetchPassthroughError {
|
|
10986
|
+
// src/lib/project-scaffold.ts
|
|
10987
|
+
function starterOverridesCss() {
|
|
10988
|
+
return `/**
|
|
10989
|
+
* Your book's own styles.
|
|
10990
|
+
*
|
|
10991
|
+
* This file loads AFTER every extension, so any rule you add here overrides
|
|
10992
|
+
* a look's defaults. A new book has no look until you add one:
|
|
10993
|
+
* gutterpress ext add clean-book <book> --look (a built-in look)
|
|
10994
|
+
* gutterpress ext add <package> <book> (a look from npm)
|
|
10995
|
+
* A book has one look at a time; adding another turns the current one off.
|
|
10996
|
+
*/
|
|
10997
|
+
`;
|
|
10985
10998
|
}
|
|
10986
10999
|
|
|
10987
|
-
class
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
timeout
|
|
10994
|
-
};
|
|
10995
|
-
}
|
|
10996
|
-
async function withFetchTimeout(options, run2) {
|
|
10997
|
-
const composed = composeSignal(options.signal, options.timeoutMs);
|
|
10998
|
-
try {
|
|
10999
|
-
return await run2(composed.signal);
|
|
11000
|
-
} catch (cause) {
|
|
11001
|
-
if (cause instanceof FetchPassthroughError)
|
|
11002
|
-
throw cause;
|
|
11003
|
-
if (options.signal?.aborted)
|
|
11004
|
-
throw cause;
|
|
11005
|
-
const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
|
|
11006
|
-
const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
|
|
11007
|
-
if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
|
|
11008
|
-
throw cause;
|
|
11009
|
-
throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
|
|
11000
|
+
class CreateProjectErrorImpl extends Error {
|
|
11001
|
+
code;
|
|
11002
|
+
constructor(code, message) {
|
|
11003
|
+
super(message);
|
|
11004
|
+
this.name = "CreateProjectError";
|
|
11005
|
+
this.code = code;
|
|
11010
11006
|
}
|
|
11011
11007
|
}
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
var DEFAULT_LIMITS = {
|
|
11015
|
-
metadataBytes: 15 * 1024 * 1024,
|
|
11016
|
-
packageTarballBytes: 50 * 1024 * 1024,
|
|
11017
|
-
totalNetworkBytes: 250 * 1024 * 1024,
|
|
11018
|
-
packageFileBytes: 50 * 1024 * 1024,
|
|
11019
|
-
totalUnpackedBytes: 512 * 1024 * 1024,
|
|
11020
|
-
totalFiles: 50000,
|
|
11021
|
-
totalPackages: 512,
|
|
11022
|
-
dependencyDepth: 64
|
|
11023
|
-
};
|
|
11024
|
-
var METADATA_TIMEOUT_MS = 30000;
|
|
11025
|
-
var TARBALL_TIMEOUT_MS = 120000;
|
|
11026
|
-
|
|
11027
|
-
class SecurityInstallError extends FetchPassthroughError {
|
|
11028
|
-
}
|
|
11029
|
-
|
|
11030
|
-
class PackageUnavailableError extends Error {
|
|
11008
|
+
function slugifyProjectName(name) {
|
|
11009
|
+
return slugify(name);
|
|
11031
11010
|
}
|
|
11032
|
-
function
|
|
11033
|
-
return
|
|
11011
|
+
function escapeYamlScalar(value) {
|
|
11012
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
11034
11013
|
}
|
|
11035
|
-
|
|
11036
|
-
|
|
11014
|
+
var DEFAULT_AUTHOR = "Anonymous";
|
|
11015
|
+
function isPositivePoints(value) {
|
|
11016
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11037
11017
|
}
|
|
11038
|
-
function
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
const result = {};
|
|
11043
|
-
for (const [name, selector] of Object.entries(map)) {
|
|
11044
|
-
if (typeof selector === "string" && selector.trim())
|
|
11045
|
-
result[name] = selector.trim();
|
|
11018
|
+
function requirePreset(preset, customPage) {
|
|
11019
|
+
if (!preset || !PRESET_IDS.includes(preset)) {
|
|
11020
|
+
const got = preset ? ` (got "${preset}")` : "";
|
|
11021
|
+
throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
|
|
11046
11022
|
}
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
}
|
|
11056
|
-
function mergeLimits(overrides) {
|
|
11057
|
-
const limits = { ...DEFAULT_LIMITS, ...overrides };
|
|
11058
|
-
for (const [name, value] of Object.entries(limits)) {
|
|
11059
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
11060
|
-
throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
|
|
11023
|
+
if (preset === "custom") {
|
|
11024
|
+
const missing = [];
|
|
11025
|
+
if (!customPage || !isPositivePoints(customPage.width))
|
|
11026
|
+
missing.push("page width");
|
|
11027
|
+
if (!customPage || !isPositivePoints(customPage.height))
|
|
11028
|
+
missing.push("page height");
|
|
11029
|
+
if (missing.length > 0) {
|
|
11030
|
+
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).`);
|
|
11061
11031
|
}
|
|
11032
|
+
} else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
|
|
11033
|
+
throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
|
|
11062
11034
|
}
|
|
11063
|
-
|
|
11035
|
+
if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
|
|
11036
|
+
throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
|
|
11037
|
+
}
|
|
11038
|
+
return preset;
|
|
11064
11039
|
}
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
11040
|
+
function resolveScaffoldTargets(requested, preset) {
|
|
11041
|
+
const ids = requested ?? [...PRESETS[preset].defaultTargets];
|
|
11042
|
+
const unknown = ids.filter((id) => !TARGETS[id]);
|
|
11043
|
+
if (unknown.length > 0) {
|
|
11044
|
+
throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
|
|
11071
11045
|
}
|
|
11072
|
-
|
|
11073
|
-
|
|
11046
|
+
return [...new Set(ids)];
|
|
11047
|
+
}
|
|
11048
|
+
async function scaffoldProject(options) {
|
|
11049
|
+
const name = (options.name ?? "").trim();
|
|
11050
|
+
if (!name) {
|
|
11051
|
+
throw new CreateProjectErrorImpl("invalid-name", "A project name is required.");
|
|
11052
|
+
}
|
|
11053
|
+
const slug = slugifyProjectName(name);
|
|
11054
|
+
const folderName = (options.folderName ?? slug).trim();
|
|
11055
|
+
if (!folderName || folderName !== slugifyProjectName(folderName)) {
|
|
11056
|
+
throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
11057
|
+
}
|
|
11058
|
+
const parentDir = options.parentDir;
|
|
11059
|
+
if (!parentDir || !path16.isAbsolute(parentDir)) {
|
|
11060
|
+
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
11074
11061
|
}
|
|
11075
|
-
const reader = response.body.getReader();
|
|
11076
|
-
let total = 0;
|
|
11077
11062
|
try {
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11063
|
+
await access(parentDir, FS.W_OK);
|
|
11064
|
+
} catch {
|
|
11065
|
+
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
11066
|
+
}
|
|
11067
|
+
const projectDir = path16.join(parentDir, folderName);
|
|
11068
|
+
let targetExists = true;
|
|
11069
|
+
try {
|
|
11070
|
+
await access(projectDir, FS.F_OK);
|
|
11071
|
+
} catch {
|
|
11072
|
+
targetExists = false;
|
|
11073
|
+
}
|
|
11074
|
+
if (targetExists) {
|
|
11075
|
+
throw new CreateProjectErrorImpl("target-exists", `A folder named "${folderName}" already exists here. Choose a different name or location.`);
|
|
11076
|
+
}
|
|
11077
|
+
const template = options.template ?? "book";
|
|
11078
|
+
const customTemplateDir = options.templateDir;
|
|
11079
|
+
const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
|
|
11080
|
+
const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
|
|
11081
|
+
try {
|
|
11082
|
+
await mkdir7(projectDir, { recursive: true });
|
|
11083
|
+
if (customTemplateDir) {
|
|
11084
|
+
const entries = await readdir6(customTemplateDir, { withFileTypes: true });
|
|
11085
|
+
for (const entry of entries) {
|
|
11086
|
+
if (entry.name === ".gutterpress-template.json")
|
|
11087
|
+
continue;
|
|
11088
|
+
await cp2(path16.join(customTemplateDir, entry.name), path16.join(projectDir, entry.name), { recursive: true });
|
|
11086
11089
|
}
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
+
await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
|
|
11091
|
+
} else {
|
|
11092
|
+
const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
|
|
11093
|
+
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11094
|
+
await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
|
|
11095
|
+
await copyFile(tplManifest, path16.join(projectDir, MANIFEST_FILENAMES[0]));
|
|
11096
|
+
await copyFile(tplChapter, path16.join(projectDir, "chapter-01.md"));
|
|
11097
|
+
await mkdir7(path16.join(projectDir, "styles"), { recursive: true });
|
|
11098
|
+
await writeFile8(path16.join(projectDir, "styles", "book.css"), starterOverridesCss(), "utf8");
|
|
11099
|
+
}
|
|
11100
|
+
} catch (e) {
|
|
11101
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11102
|
+
}
|
|
11103
|
+
try {
|
|
11104
|
+
await ensureGitignoreHasDist(projectDir);
|
|
11105
|
+
} catch (e) {
|
|
11106
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
|
|
11107
|
+
}
|
|
11108
|
+
const manifestPath = MANIFEST_FILENAMES.map((name2) => path16.join(projectDir, name2)).find(existsSync12) ?? path16.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
11109
|
+
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11110
|
+
const substitutions = {
|
|
11111
|
+
"{{TITLE}}": escapeYamlScalar(name),
|
|
11112
|
+
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
11113
|
+
};
|
|
11114
|
+
let openFile = path16.join(projectDir, "chapter-01.md");
|
|
11115
|
+
try {
|
|
11116
|
+
await fillTemplateFile(manifestPath, substitutions);
|
|
11117
|
+
if (preset) {
|
|
11118
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11119
|
+
doc.set("preset", preset);
|
|
11120
|
+
doc.set("targets", doc.createNode(targets ?? []));
|
|
11121
|
+
if (options.customPage) {
|
|
11122
|
+
const page = {
|
|
11123
|
+
width: options.customPage.width,
|
|
11124
|
+
height: options.customPage.height
|
|
11125
|
+
};
|
|
11126
|
+
if (options.customPage.tolerance !== undefined) {
|
|
11127
|
+
page.tolerance = options.customPage.tolerance;
|
|
11128
|
+
}
|
|
11129
|
+
doc.set("page", doc.createNode(page));
|
|
11090
11130
|
}
|
|
11091
|
-
|
|
11092
|
-
await onChunk(value);
|
|
11131
|
+
await writeManifestDoc(file, doc);
|
|
11093
11132
|
}
|
|
11094
|
-
|
|
11095
|
-
|
|
11133
|
+
const firstSource = await firstSourceFile(manifestPath);
|
|
11134
|
+
if (firstSource)
|
|
11135
|
+
openFile = path16.join(projectDir, firstSource);
|
|
11136
|
+
await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
|
|
11137
|
+
} catch (e) {
|
|
11138
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11096
11139
|
}
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
if (pending)
|
|
11102
|
-
return pending;
|
|
11103
|
-
pending = withFetchTimeout({
|
|
11104
|
-
timeoutMs: METADATA_TIMEOUT_MS,
|
|
11105
|
-
signal: ctx.signal,
|
|
11106
|
-
timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
|
|
11107
|
-
offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
11108
|
-
}, async (signal) => {
|
|
11109
|
-
const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
|
|
11110
|
-
signal,
|
|
11111
|
-
redirect: "error",
|
|
11112
|
-
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
11113
|
-
});
|
|
11114
|
-
if (response.status === 404) {
|
|
11115
|
-
throw new FriendlyHttpError(`npm package "${name}" was not found.`);
|
|
11116
|
-
}
|
|
11117
|
-
if (!response.ok) {
|
|
11118
|
-
throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
|
|
11119
|
-
}
|
|
11120
|
-
const chunks = [];
|
|
11121
|
-
const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
|
|
11122
|
-
chunks.push(chunk2);
|
|
11123
|
-
});
|
|
11124
|
-
const bytes = new Uint8Array(size);
|
|
11125
|
-
let offset = 0;
|
|
11126
|
-
for (const chunk2 of chunks) {
|
|
11127
|
-
bytes.set(chunk2, offset);
|
|
11128
|
-
offset += chunk2.length;
|
|
11129
|
-
}
|
|
11130
|
-
let metadata = null;
|
|
11140
|
+
const requested = options.versionHistory ?? "local-git";
|
|
11141
|
+
let versionHistory = "none";
|
|
11142
|
+
let versionHistoryError;
|
|
11143
|
+
if (requested === "local-git") {
|
|
11131
11144
|
try {
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
}
|
|
11145
|
-
function parseIntegrity(dist) {
|
|
11146
|
-
const integrity = typeof dist.integrity === "string" ? dist.integrity.trim() : "";
|
|
11147
|
-
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]));
|
|
11148
|
-
for (const algorithm of ["sha512", "sha384", "sha256"]) {
|
|
11149
|
-
const match = parsed.find((candidate) => candidate[1] === algorithm);
|
|
11150
|
-
if (!match)
|
|
11151
|
-
continue;
|
|
11152
|
-
const digest = Buffer.from(match[2], "base64");
|
|
11153
|
-
if (digest.length > 0) {
|
|
11154
|
-
return { algorithm, digest, label: `${algorithm}-${match[2]}`, legacySha1: false };
|
|
11145
|
+
const { providerFor } = await import("./source-provider-jkppq9fc.js");
|
|
11146
|
+
const provider = providerFor({ type: "local-folder", path: projectDir });
|
|
11147
|
+
await provider.initVersionHistory({
|
|
11148
|
+
projectDir,
|
|
11149
|
+
authorName: options.author?.trim() || undefined,
|
|
11150
|
+
initialMessage: "Created project"
|
|
11151
|
+
});
|
|
11152
|
+
versionHistory = "local-git";
|
|
11153
|
+
} catch (e) {
|
|
11154
|
+
versionHistory = "none";
|
|
11155
|
+
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
11156
|
+
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
11157
|
+
versionHistoryError = enclosingRepoDir ? `This book was added to ${path16.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
|
|
11155
11158
|
}
|
|
11156
11159
|
}
|
|
11157
|
-
const
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
}
|
|
11166
|
-
if (integrity) {
|
|
11167
|
-
throw new SecurityInstallError("The npm registry supplied integrity data, but no supported SHA-256-or-stronger hash.");
|
|
11168
|
-
}
|
|
11169
|
-
const shasum = typeof dist.shasum === "string" ? dist.shasum.trim().toLowerCase() : "";
|
|
11170
|
-
if (/^[a-f0-9]{40}$/.test(shasum)) {
|
|
11171
|
-
return {
|
|
11172
|
-
algorithm: "sha1",
|
|
11173
|
-
digest: Buffer.from(shasum, "hex"),
|
|
11174
|
-
label: `sha1-hex-${shasum}`,
|
|
11175
|
-
legacySha1: true
|
|
11176
|
-
};
|
|
11160
|
+
const result = {
|
|
11161
|
+
projectDir,
|
|
11162
|
+
manifestPath,
|
|
11163
|
+
openFile,
|
|
11164
|
+
versionHistory
|
|
11165
|
+
};
|
|
11166
|
+
if (versionHistoryError !== undefined) {
|
|
11167
|
+
result.versionHistoryError = versionHistoryError;
|
|
11177
11168
|
}
|
|
11178
|
-
|
|
11169
|
+
return result;
|
|
11179
11170
|
}
|
|
11180
|
-
function
|
|
11181
|
-
|
|
11171
|
+
function prettifyFolderName(base) {
|
|
11172
|
+
const words = base.replace(/[-_]+/g, " ").trim();
|
|
11173
|
+
return words.replace(/\b\w/g, (c) => c.toUpperCase()) || "Untitled Book";
|
|
11182
11174
|
}
|
|
11183
|
-
async function
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
const metadata = await fetchPackument(ctx, name);
|
|
11188
|
-
const versions = object3(metadata.versions);
|
|
11189
|
-
const tags = object3(metadata["dist-tags"]);
|
|
11190
|
-
if (!versions || !tags)
|
|
11191
|
-
throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
|
|
11192
|
-
let version;
|
|
11193
|
-
if (!selector) {
|
|
11194
|
-
if (typeof tags.latest === "string")
|
|
11195
|
-
version = tags.latest;
|
|
11196
|
-
} else if (object3(versions[selector])) {
|
|
11197
|
-
version = selector;
|
|
11198
|
-
} else if (typeof tags[selector] === "string") {
|
|
11199
|
-
version = tags[selector];
|
|
11200
|
-
} else if (validRange2(selector)) {
|
|
11201
|
-
version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
|
|
11175
|
+
async function adoptFolder(options) {
|
|
11176
|
+
const dir = options.dir;
|
|
11177
|
+
if (typeof dir !== "string" || !path16.isAbsolute(dir)) {
|
|
11178
|
+
throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
|
|
11202
11179
|
}
|
|
11203
|
-
|
|
11204
|
-
|
|
11180
|
+
let st;
|
|
11181
|
+
try {
|
|
11182
|
+
st = await stat5(dir);
|
|
11183
|
+
} catch {
|
|
11184
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Folder not found: ${dir}`);
|
|
11205
11185
|
}
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
|
|
11186
|
+
if (!st.isDirectory()) {
|
|
11187
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
|
|
11209
11188
|
}
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
|
|
11189
|
+
if (MANIFEST_FILENAMES.some((name) => existsSync12(path16.join(dir, name)))) {
|
|
11190
|
+
throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
|
|
11213
11191
|
}
|
|
11214
|
-
|
|
11192
|
+
const template = options.template ?? "book";
|
|
11193
|
+
const title = (options.title ?? "").trim() || prettifyFolderName(path16.basename(dir));
|
|
11194
|
+
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11195
|
+
let mdFiles;
|
|
11215
11196
|
try {
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11197
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
11198
|
+
mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
11199
|
+
if (mdFiles.length === 0) {
|
|
11200
|
+
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11201
|
+
await copyFile(tplChapter, path16.join(dir, "chapter-01.md"));
|
|
11202
|
+
await fillTemplateFile(path16.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
|
|
11203
|
+
mdFiles = ["chapter-01.md"];
|
|
11204
|
+
}
|
|
11205
|
+
await mkdir7(path16.join(dir, "styles"), { recursive: true });
|
|
11206
|
+
const bookCssPath = path16.join(dir, "styles", "book.css");
|
|
11207
|
+
if (!existsSync12(bookCssPath)) {
|
|
11208
|
+
await writeFile8(bookCssPath, starterOverridesCss(), "utf8");
|
|
11209
|
+
}
|
|
11210
|
+
const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
|
|
11211
|
+
`);
|
|
11212
|
+
const manifest = `title: "${escapeYamlScalar(title)}"
|
|
11213
|
+
authors:
|
|
11214
|
+
- "${escapeYamlScalar(author)}"
|
|
11215
|
+
preset: dtrpg
|
|
11216
|
+
targets:
|
|
11217
|
+
- dtrpg
|
|
11218
|
+
source:
|
|
11219
|
+
files:
|
|
11220
|
+
${filesYaml}
|
|
11221
|
+
styles:
|
|
11222
|
+
- styles/book.css
|
|
11223
|
+
`;
|
|
11224
|
+
await writeFile8(path16.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
|
|
11225
|
+
await mkdir7(path16.join(dir, "assets"), { recursive: true });
|
|
11226
|
+
await ensureGitignoreHasDist(dir);
|
|
11227
|
+
} catch (e) {
|
|
11228
|
+
if (e instanceof CreateProjectErrorImpl)
|
|
11229
|
+
throw e;
|
|
11230
|
+
throw new CreateProjectErrorImpl("scaffold-io", `Could not set up the folder: ${e instanceof Error ? e.message : String(e)}`);
|
|
11219
11231
|
}
|
|
11220
|
-
|
|
11221
|
-
|
|
11232
|
+
const requested = options.versionHistory ?? "local-git";
|
|
11233
|
+
let versionHistory = "none";
|
|
11234
|
+
let versionHistoryError;
|
|
11235
|
+
if (requested === "local-git") {
|
|
11236
|
+
try {
|
|
11237
|
+
const { providerFor } = await import("./source-provider-jkppq9fc.js");
|
|
11238
|
+
const provider = providerFor({ type: "local-folder", path: dir });
|
|
11239
|
+
await provider.initVersionHistory({
|
|
11240
|
+
projectDir: dir,
|
|
11241
|
+
authorName: options.author?.trim() || undefined,
|
|
11242
|
+
initialMessage: "Set up as a gutterpress book"
|
|
11243
|
+
});
|
|
11244
|
+
versionHistory = "local-git";
|
|
11245
|
+
} catch (e) {
|
|
11246
|
+
versionHistory = "none";
|
|
11247
|
+
versionHistoryError = e instanceof Error ? e.message : String(e);
|
|
11248
|
+
}
|
|
11222
11249
|
}
|
|
11223
|
-
|
|
11250
|
+
const result = {
|
|
11251
|
+
projectDir: dir,
|
|
11252
|
+
manifestPath: path16.join(dir, MANIFEST_FILENAMES[0]),
|
|
11253
|
+
openFile: path16.join(dir, mdFiles[0]),
|
|
11254
|
+
versionHistory
|
|
11255
|
+
};
|
|
11256
|
+
if (versionHistoryError !== undefined)
|
|
11257
|
+
result.versionHistoryError = versionHistoryError;
|
|
11258
|
+
return result;
|
|
11224
11259
|
}
|
|
11225
|
-
async function
|
|
11226
|
-
const
|
|
11227
|
-
|
|
11260
|
+
async function ensureGitignoreHasDist(projectDir) {
|
|
11261
|
+
const gitignorePath = path16.join(projectDir, ".gitignore");
|
|
11262
|
+
let existing = null;
|
|
11228
11263
|
try {
|
|
11229
|
-
await
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
const response = await ctx.fetch(pkg.tarball, { signal, redirect: "error" });
|
|
11236
|
-
if (!response.ok) {
|
|
11237
|
-
throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
|
|
11238
|
-
}
|
|
11239
|
-
await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
|
|
11240
|
-
hash.update(chunk2);
|
|
11241
|
-
try {
|
|
11242
|
-
await file.write(chunk2);
|
|
11243
|
-
} catch (cause) {
|
|
11244
|
-
throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
|
|
11245
|
-
}
|
|
11246
|
-
});
|
|
11247
|
-
});
|
|
11248
|
-
} finally {
|
|
11249
|
-
await file.close();
|
|
11250
|
-
}
|
|
11251
|
-
const actual = hash.digest();
|
|
11252
|
-
if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
|
|
11253
|
-
throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
|
|
11264
|
+
existing = await readFile27(gitignorePath, "utf8");
|
|
11265
|
+
} catch {}
|
|
11266
|
+
if (existing === null) {
|
|
11267
|
+
await writeFile8(gitignorePath, `dist/
|
|
11268
|
+
`, "utf8");
|
|
11269
|
+
return;
|
|
11254
11270
|
}
|
|
11271
|
+
const alreadyIgnoresDist = existing.split(`
|
|
11272
|
+
`).some((line) => /^\/?dist\/?$/.test(line.trim()));
|
|
11273
|
+
if (alreadyIgnoresDist)
|
|
11274
|
+
return;
|
|
11275
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
|
|
11276
|
+
`);
|
|
11277
|
+
await writeFile8(gitignorePath, existing + (needsLeadingNewline ? `
|
|
11278
|
+
` : "") + `dist/
|
|
11279
|
+
`, "utf8");
|
|
11255
11280
|
}
|
|
11256
|
-
function
|
|
11257
|
-
|
|
11258
|
-
|
|
11281
|
+
async function firstSourceFile(manifestPath) {
|
|
11282
|
+
let text;
|
|
11283
|
+
try {
|
|
11284
|
+
text = await readFile27(manifestPath, "utf8");
|
|
11285
|
+
} catch {
|
|
11286
|
+
return;
|
|
11259
11287
|
}
|
|
11260
|
-
const
|
|
11261
|
-
if (
|
|
11262
|
-
|
|
11263
|
-
const
|
|
11264
|
-
|
|
11265
|
-
|
|
11288
|
+
const filesIdx = text.search(/^\s*files:\s*$/m);
|
|
11289
|
+
if (filesIdx === -1)
|
|
11290
|
+
return;
|
|
11291
|
+
const rest = text.slice(filesIdx);
|
|
11292
|
+
const m = rest.match(/^\s*-\s*"?([^"\n]+?)"?\s*$/m);
|
|
11293
|
+
return m ? m[1].trim() : undefined;
|
|
11294
|
+
}
|
|
11295
|
+
async function fillTemplateFile(filePath, substitutions) {
|
|
11296
|
+
let text = await readFile27(filePath, "utf8");
|
|
11297
|
+
for (const [token, value] of Object.entries(substitutions)) {
|
|
11298
|
+
text = text.split(token).join(value);
|
|
11266
11299
|
}
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11300
|
+
await writeFile8(filePath, text, "utf8");
|
|
11301
|
+
}
|
|
11302
|
+
// src/lib/extension-scaffold.ts
|
|
11303
|
+
import { access as access2, mkdir as mkdir8, readFile as readFile28, writeFile as writeFile9 } from "node:fs/promises";
|
|
11304
|
+
import { constants as FS2 } from "node:fs";
|
|
11305
|
+
import path17 from "node:path";
|
|
11306
|
+
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
11307
|
+
var RESERVED_PREFIX = "gp-";
|
|
11308
|
+
var TEMPLATE_FILES = {
|
|
11309
|
+
plugin: [
|
|
11310
|
+
["package.json", "package.json"],
|
|
11311
|
+
["README.md", "README.md"],
|
|
11312
|
+
["plugin.js.tpl", "plugin.js"],
|
|
11313
|
+
["styles/plugin.css", "styles/plugin.css"],
|
|
11314
|
+
["snippets/term-box.md", "snippets/term-box.md"],
|
|
11315
|
+
["test/fixture.md", "test/fixture.md"],
|
|
11316
|
+
["test/expected.html", "test/expected.html"],
|
|
11317
|
+
["test/plugin.test.js.tpl", "test/plugin.test.js"]
|
|
11318
|
+
],
|
|
11319
|
+
theme: [
|
|
11320
|
+
["package.json", "package.json"],
|
|
11321
|
+
["README.md", "README.md"],
|
|
11322
|
+
["components.yaml", "components.yaml"],
|
|
11323
|
+
["snippets/callout.md", "snippets/callout.md"],
|
|
11324
|
+
["styles/tokens.css", "styles/tokens.css"],
|
|
11325
|
+
["styles/base.css", "styles/base.css"],
|
|
11326
|
+
["styles/components.css", "styles/components.css"],
|
|
11327
|
+
["styles/page-templates.css", "styles/page-templates.css"],
|
|
11328
|
+
["styles/page-rules.css", "styles/page-rules.css"],
|
|
11329
|
+
["styles/book.css", "styles/book.css"]
|
|
11330
|
+
]
|
|
11331
|
+
};
|
|
11332
|
+
var OPEN_FILE = {
|
|
11333
|
+
plugin: "plugin.js",
|
|
11334
|
+
theme: "styles/tokens.css"
|
|
11335
|
+
};
|
|
11336
|
+
var DEFAULT_DESCRIPTION = {
|
|
11337
|
+
plugin: "A Gutterpress plugin.",
|
|
11338
|
+
theme: "A Gutterpress theme."
|
|
11339
|
+
};
|
|
11340
|
+
var DEFAULT_AUTHOR2 = "Anonymous";
|
|
11341
|
+
|
|
11342
|
+
class ExtensionScaffoldError extends Error {
|
|
11343
|
+
code;
|
|
11344
|
+
constructor(code, message) {
|
|
11345
|
+
super(message);
|
|
11346
|
+
this.name = "CreateProjectError";
|
|
11347
|
+
this.code = code;
|
|
11273
11348
|
}
|
|
11274
|
-
return { relative: relative2, key: windowsPathKey(relative2) };
|
|
11275
11349
|
}
|
|
11276
|
-
|
|
11277
|
-
const
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
"GlobalExtendedHeader",
|
|
11282
|
-
"ExtendedHeader",
|
|
11283
|
-
"NextFileHasLongLinkpath",
|
|
11284
|
-
"NextFileHasLongPath",
|
|
11285
|
-
"OldGnuLongPath",
|
|
11286
|
-
"OldExtendedHeader"
|
|
11287
|
-
]);
|
|
11288
|
-
await mkdir7(packageDir, { recursive: true });
|
|
11289
|
-
const unpack = extract({
|
|
11290
|
-
cwd: packageDir,
|
|
11291
|
-
strip: 1,
|
|
11292
|
-
strict: true,
|
|
11293
|
-
preservePaths: false,
|
|
11294
|
-
preserveOwner: false,
|
|
11295
|
-
noMtime: true,
|
|
11296
|
-
chmod: false,
|
|
11297
|
-
maxDepth: ctx.limits.dependencyDepth * 4,
|
|
11298
|
-
filter: (entryPath, rawEntry) => {
|
|
11299
|
-
if (rejected)
|
|
11300
|
-
return false;
|
|
11301
|
-
const entry = rawEntry;
|
|
11302
|
-
if (metadataTypes.has(entry.type))
|
|
11303
|
-
return true;
|
|
11304
|
-
if (entry.type === "Link" || entry.type === "SymbolicLink") {
|
|
11305
|
-
rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
|
|
11306
|
-
return false;
|
|
11307
|
-
}
|
|
11308
|
-
if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
|
|
11309
|
-
rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
|
|
11310
|
-
return false;
|
|
11311
|
-
}
|
|
11312
|
-
try {
|
|
11313
|
-
const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
|
|
11314
|
-
if (!relative2) {
|
|
11315
|
-
if (entry.type === "Directory")
|
|
11316
|
-
return true;
|
|
11317
|
-
throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
|
|
11318
|
-
}
|
|
11319
|
-
if (seen.has(key)) {
|
|
11320
|
-
throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
|
|
11321
|
-
}
|
|
11322
|
-
seen.add(key);
|
|
11323
|
-
ctx.totalFiles++;
|
|
11324
|
-
if (ctx.totalFiles > ctx.limits.totalFiles) {
|
|
11325
|
-
throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
|
|
11326
|
-
}
|
|
11327
|
-
if (entry.type !== "Directory") {
|
|
11328
|
-
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
|
|
11329
|
-
throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
|
|
11330
|
-
}
|
|
11331
|
-
}
|
|
11332
|
-
return true;
|
|
11333
|
-
} catch (error2) {
|
|
11334
|
-
rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
|
|
11335
|
-
return false;
|
|
11336
|
-
}
|
|
11337
|
-
}
|
|
11338
|
-
});
|
|
11339
|
-
const limiter = new Transform({
|
|
11340
|
-
transform(chunk2, _encoding, callback) {
|
|
11341
|
-
ctx.totalUnpackedBytes += chunk2.length;
|
|
11342
|
-
if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
|
|
11343
|
-
callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
|
|
11344
|
-
} else
|
|
11345
|
-
callback(null, chunk2);
|
|
11346
|
-
}
|
|
11347
|
-
});
|
|
11348
|
-
try {
|
|
11349
|
-
await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
|
|
11350
|
-
} catch (error2) {
|
|
11351
|
-
if (rejected)
|
|
11352
|
-
throw rejected;
|
|
11353
|
-
if (error2 instanceof SecurityInstallError)
|
|
11354
|
-
throw error2;
|
|
11355
|
-
throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
|
|
11350
|
+
function resolveExtensionPrefix(slug, requested) {
|
|
11351
|
+
const raw = (requested ?? `${slug}-`).trim();
|
|
11352
|
+
const prefix = raw.endsWith("-") ? raw : `${raw}-`;
|
|
11353
|
+
if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
|
|
11354
|
+
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-".`);
|
|
11356
11355
|
}
|
|
11357
|
-
if (
|
|
11358
|
-
throw
|
|
11356
|
+
if (prefix === RESERVED_PREFIX) {
|
|
11357
|
+
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.`);
|
|
11358
|
+
}
|
|
11359
|
+
return prefix;
|
|
11359
11360
|
}
|
|
11360
|
-
|
|
11361
|
-
const
|
|
11362
|
-
|
|
11361
|
+
function identifierFor(slug) {
|
|
11362
|
+
const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
11363
|
+
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
11364
|
+
}
|
|
11365
|
+
function escapeJsonScalar(value) {
|
|
11366
|
+
return JSON.stringify(value).slice(1, -1);
|
|
11367
|
+
}
|
|
11368
|
+
async function scaffoldExtension(options) {
|
|
11369
|
+
const kind = options.kind;
|
|
11370
|
+
if (kind !== "plugin" && kind !== "theme") {
|
|
11371
|
+
throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
|
|
11372
|
+
}
|
|
11373
|
+
const name = (options.name ?? "").trim();
|
|
11374
|
+
if (!name) {
|
|
11375
|
+
throw new ExtensionScaffoldError("invalid-name", "A name is required.");
|
|
11376
|
+
}
|
|
11377
|
+
const slug = (options.folderName ?? slugify(name)).trim();
|
|
11378
|
+
if (!slug || slug !== slugify(slug)) {
|
|
11379
|
+
throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
11380
|
+
}
|
|
11381
|
+
const prefix = resolveExtensionPrefix(slug, options.prefix);
|
|
11382
|
+
const parentDir = options.parentDir;
|
|
11383
|
+
if (!parentDir || !path17.isAbsolute(parentDir)) {
|
|
11384
|
+
throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
|
|
11385
|
+
}
|
|
11363
11386
|
try {
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
manifest = object3(JSON.parse(await readFile27(packageJsonPath, "utf8")));
|
|
11368
|
-
} catch (error2) {
|
|
11369
|
-
throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
|
|
11387
|
+
await access2(parentDir, FS2.W_OK);
|
|
11388
|
+
} catch {
|
|
11389
|
+
throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
11370
11390
|
}
|
|
11371
|
-
|
|
11372
|
-
|
|
11391
|
+
const extensionDir = path17.join(parentDir, slug);
|
|
11392
|
+
let targetExists = true;
|
|
11393
|
+
try {
|
|
11394
|
+
await access2(extensionDir, FS2.F_OK);
|
|
11395
|
+
} catch {
|
|
11396
|
+
targetExists = false;
|
|
11373
11397
|
}
|
|
11374
|
-
|
|
11375
|
-
}
|
|
11376
|
-
|
|
11398
|
+
if (targetExists) {
|
|
11399
|
+
throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
|
|
11400
|
+
}
|
|
11401
|
+
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
|
|
11402
|
+
const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
|
|
11403
|
+
const plain = {
|
|
11404
|
+
"{{NAME}}": name,
|
|
11405
|
+
"{{SLUG}}": slug,
|
|
11406
|
+
"{{PREFIX}}": prefix,
|
|
11407
|
+
"{{AUTHOR}}": author,
|
|
11408
|
+
"{{DESCRIPTION}}": description,
|
|
11409
|
+
"{{IDENT}}": identifierFor(slug)
|
|
11410
|
+
};
|
|
11411
|
+
const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
|
|
11412
|
+
const entries = TEMPLATE_FILES[kind];
|
|
11413
|
+
const written = [];
|
|
11377
11414
|
try {
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11415
|
+
await mkdir8(extensionDir, { recursive: true });
|
|
11416
|
+
for (const [assetName, relDest] of entries) {
|
|
11417
|
+
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
11418
|
+
const dest = path17.join(extensionDir, relDest);
|
|
11419
|
+
await mkdir8(path17.dirname(dest), { recursive: true });
|
|
11420
|
+
const text = await readFile28(src, "utf8");
|
|
11421
|
+
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
11422
|
+
await writeFile9(dest, substitute(text, table), "utf8");
|
|
11423
|
+
written.push(relDest);
|
|
11382
11424
|
}
|
|
11383
|
-
|
|
11425
|
+
} catch (e) {
|
|
11426
|
+
throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
|
|
11384
11427
|
}
|
|
11428
|
+
return {
|
|
11429
|
+
extensionDir,
|
|
11430
|
+
manifestPath: path17.join(extensionDir, "package.json"),
|
|
11431
|
+
kind,
|
|
11432
|
+
slug,
|
|
11433
|
+
prefix,
|
|
11434
|
+
openFile: path17.join(extensionDir, OPEN_FILE[kind]),
|
|
11435
|
+
files: written
|
|
11436
|
+
};
|
|
11385
11437
|
}
|
|
11386
|
-
function
|
|
11387
|
-
let
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
} else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
|
|
11391
|
-
values = rule;
|
|
11392
|
-
} else {
|
|
11393
|
-
return true;
|
|
11438
|
+
function substitute(text, table) {
|
|
11439
|
+
let out = text;
|
|
11440
|
+
for (const [token, value] of Object.entries(table)) {
|
|
11441
|
+
out = out.split(token).join(value);
|
|
11394
11442
|
}
|
|
11395
|
-
|
|
11396
|
-
return true;
|
|
11397
|
-
if (values.length === 1 && values[0] === "any")
|
|
11398
|
-
return true;
|
|
11399
|
-
if (values.includes(`!${current}`))
|
|
11400
|
-
return false;
|
|
11401
|
-
const positives = values.filter((value) => !value.startsWith("!"));
|
|
11402
|
-
return positives.length === 0 || positives.includes(current);
|
|
11443
|
+
return out;
|
|
11403
11444
|
}
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
}
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
};
|
|
11417
|
-
if (depth > ctx.limits.dependencyDepth) {
|
|
11418
|
-
throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
|
|
11419
|
-
}
|
|
11420
|
-
let selected;
|
|
11421
|
-
try {
|
|
11422
|
-
selected = await selectPackage(ctx, name, selector);
|
|
11423
|
-
} catch (error2) {
|
|
11424
|
-
if (optional && isOptionalAvailabilityError(error2)) {
|
|
11425
|
-
discardOptionalSubtree();
|
|
11426
|
-
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
11427
|
-
}
|
|
11428
|
-
throw error2;
|
|
11429
|
-
}
|
|
11430
|
-
const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
|
|
11431
|
-
if (ancestor)
|
|
11432
|
-
return { path: ancestor.path };
|
|
11433
|
-
if (ctx.records.length >= ctx.limits.totalPackages) {
|
|
11434
|
-
throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
|
|
11435
|
-
}
|
|
11436
|
-
const relativePath = relativePackagePath(parentPackagePath, name);
|
|
11437
|
-
const packageDir = path16.join(ctx.stageRoot, ...relativePath.split("/"));
|
|
11438
|
-
const archivePath = path16.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
|
|
11439
|
-
try {
|
|
11440
|
-
await mkdir7(path16.dirname(packageDir), { recursive: true });
|
|
11441
|
-
await downloadTarball(ctx, selected, archivePath);
|
|
11442
|
-
await extractTarball(ctx, archivePath, packageDir, selected.name);
|
|
11443
|
-
const manifest = await readInstalledManifest(packageDir, selected);
|
|
11444
|
-
if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
|
|
11445
|
-
const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
|
|
11446
|
-
if (optional) {
|
|
11447
|
-
await rm3(packageDir, { recursive: true, force: true });
|
|
11448
|
-
return { skipped: reason };
|
|
11449
|
-
}
|
|
11450
|
-
throw new PackageUnavailableError(reason);
|
|
11451
|
-
}
|
|
11452
|
-
const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
|
|
11453
|
-
const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
|
|
11454
|
-
if (parentPackagePath === null && !importEntry && !requireEntry) {
|
|
11455
|
-
throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
|
|
11456
|
-
}
|
|
11457
|
-
const record = {
|
|
11458
|
-
path: relativePath,
|
|
11459
|
-
name,
|
|
11460
|
-
version: selected.version,
|
|
11461
|
-
tarball: selected.tarball,
|
|
11462
|
-
integrity: selected.integrity.label,
|
|
11463
|
-
legacySha1: selected.integrity.legacySha1,
|
|
11464
|
-
...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
|
|
11465
|
-
...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
|
|
11466
|
-
dependencies: {},
|
|
11467
|
-
packageDir,
|
|
11468
|
-
manifest
|
|
11469
|
-
};
|
|
11470
|
-
ctx.records.push(record);
|
|
11471
|
-
if (selected.integrity.legacySha1) {
|
|
11472
|
-
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.`);
|
|
11473
|
-
}
|
|
11474
|
-
const dependencies = stringMap2(manifest.dependencies);
|
|
11475
|
-
const optionalDependencies = stringMap2(manifest.optionalDependencies);
|
|
11476
|
-
for (const optionalName of Object.keys(optionalDependencies))
|
|
11477
|
-
delete dependencies[optionalName];
|
|
11478
|
-
const peerDependencies = stringMap2(manifest.peerDependencies);
|
|
11479
|
-
const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
|
|
11480
|
-
for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
|
|
11481
|
-
delete peerDependencies[existing];
|
|
11482
|
-
}
|
|
11483
|
-
const nextAncestors = [...ancestors, record];
|
|
11484
|
-
const requiredEntries = [
|
|
11485
|
-
...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
|
|
11486
|
-
...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
|
|
11487
|
-
].sort((a, b) => a.dependency.localeCompare(b.dependency));
|
|
11488
|
-
for (const { dependency, range } of requiredEntries) {
|
|
11489
|
-
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
|
|
11490
|
-
if (!child.path)
|
|
11491
|
-
throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
|
|
11492
|
-
record.dependencies[dependency] = child.path;
|
|
11493
|
-
}
|
|
11494
|
-
const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
|
|
11495
|
-
for (const [dependency, range] of optionalEntries) {
|
|
11496
|
-
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
|
|
11497
|
-
if (child.path)
|
|
11498
|
-
record.dependencies[dependency] = child.path;
|
|
11499
|
-
else if (child.skipped) {
|
|
11500
|
-
ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
|
|
11501
|
-
}
|
|
11502
|
-
}
|
|
11503
|
-
for (const [dependency, range] of Object.entries(peerDependencies)) {
|
|
11504
|
-
if (object3(peerMeta[dependency])?.optional === true) {
|
|
11505
|
-
ctx.skipped.push({
|
|
11506
|
-
from: relativePath,
|
|
11507
|
-
name: dependency,
|
|
11508
|
-
selector: range,
|
|
11509
|
-
kind: "optional-peer",
|
|
11510
|
-
reason: "Optional peer dependencies are not auto-installed."
|
|
11511
|
-
});
|
|
11512
|
-
}
|
|
11513
|
-
}
|
|
11514
|
-
return { path: relativePath };
|
|
11515
|
-
} catch (error2) {
|
|
11516
|
-
await rm3(packageDir, { recursive: true, force: true });
|
|
11517
|
-
if (optional && isOptionalAvailabilityError(error2)) {
|
|
11518
|
-
discardOptionalSubtree();
|
|
11519
|
-
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
11520
|
-
}
|
|
11521
|
-
throw error2;
|
|
11522
|
-
} finally {
|
|
11523
|
-
await rm3(archivePath, { force: true });
|
|
11524
|
-
}
|
|
11525
|
-
}
|
|
11526
|
-
async function ensureDirectory(pathname) {
|
|
11527
|
-
try {
|
|
11528
|
-
const info2 = await lstat2(pathname);
|
|
11529
|
-
if (info2.isSymbolicLink() || !info2.isDirectory()) {
|
|
11530
|
-
throw new Error(`Plugin install path is not a normal directory: ${pathname}`);
|
|
11531
|
-
}
|
|
11532
|
-
} catch (error2) {
|
|
11533
|
-
if (error2.code !== "ENOENT")
|
|
11534
|
-
throw error2;
|
|
11535
|
-
await mkdir7(pathname);
|
|
11536
|
-
}
|
|
11537
|
-
}
|
|
11538
|
-
async function prepareVendorParent(projectDir, name) {
|
|
11539
|
-
const plugins = path16.join(projectDir, PLUGINS_DIR);
|
|
11540
|
-
const npm = path16.join(plugins, VENDORED_NPM_DIR);
|
|
11541
|
-
const packageParent = path16.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
|
|
11542
|
-
await mkdir7(projectDir, { recursive: true });
|
|
11543
|
-
await ensureDirectory(plugins);
|
|
11544
|
-
await ensureDirectory(npm);
|
|
11545
|
-
await ensureDirectory(packageParent);
|
|
11546
|
-
const [realNpm, realPackageParent] = await Promise.all([
|
|
11547
|
-
realpath2(npm),
|
|
11548
|
-
realpath2(packageParent)
|
|
11549
|
-
]);
|
|
11550
|
-
if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
|
|
11551
|
-
throw new Error("Plugin install path resolves outside the project.");
|
|
11552
|
-
}
|
|
11553
|
-
return realNpm;
|
|
11554
|
-
}
|
|
11555
|
-
function publicReceipt(record) {
|
|
11556
|
-
return {
|
|
11557
|
-
path: record.path,
|
|
11558
|
-
name: record.name,
|
|
11559
|
-
version: record.version,
|
|
11560
|
-
tarball: record.tarball,
|
|
11561
|
-
integrity: record.integrity,
|
|
11562
|
-
legacySha1: record.legacySha1,
|
|
11563
|
-
...record.entry ? { entry: record.entry } : {},
|
|
11564
|
-
...record.requireEntry ? { requireEntry: record.requireEntry } : {},
|
|
11565
|
-
dependencies: record.dependencies
|
|
11566
|
-
};
|
|
11567
|
-
}
|
|
11568
|
-
async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
|
|
11569
|
-
const requestedProjectDir = path16.resolve(projectDirInput);
|
|
11570
|
-
await mkdir7(requestedProjectDir, { recursive: true });
|
|
11571
|
-
const projectDir = await realpath2(requestedProjectDir);
|
|
11572
|
-
const requested = parseNpmPluginSpec(packageSpec);
|
|
11573
|
-
const npmRoot = await prepareVendorParent(projectDir, requested.name);
|
|
11574
|
-
const container = await mkdtemp2(path16.join(npmRoot, ".install-"));
|
|
11575
|
-
const stageRoot = path16.join(container, "root");
|
|
11576
|
-
const downloadsDir = path16.join(container, "downloads");
|
|
11577
|
-
await Promise.all([mkdir7(stageRoot), mkdir7(downloadsDir)]);
|
|
11578
|
-
const ctx = {
|
|
11579
|
-
fetch: options.fetch ?? globalThis.fetch,
|
|
11580
|
-
signal: options.signal,
|
|
11581
|
-
limits: mergeLimits(options.limits),
|
|
11582
|
-
stageRoot,
|
|
11583
|
-
downloadsDir,
|
|
11584
|
-
packuments: new Map,
|
|
11585
|
-
records: [],
|
|
11586
|
-
skipped: [],
|
|
11587
|
-
warnings: new Set,
|
|
11588
|
-
totalNetworkBytes: 0,
|
|
11589
|
-
totalUnpackedBytes: 0,
|
|
11590
|
-
totalFiles: 0,
|
|
11591
|
-
archiveCounter: 0
|
|
11592
|
-
};
|
|
11593
|
-
let finalRoot = "";
|
|
11594
|
-
let backupRoot = null;
|
|
11445
|
+
// src/lib/project-templates.ts
|
|
11446
|
+
import { cp as cp3, mkdir as mkdir9, readFile as readFile29, readdir as readdir7, stat as stat6, writeFile as writeFile10 } from "node:fs/promises";
|
|
11447
|
+
import { constants as FS3, existsSync as existsSync13 } from "node:fs";
|
|
11448
|
+
import { access as access3 } from "node:fs/promises";
|
|
11449
|
+
import path18 from "node:path";
|
|
11450
|
+
import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
|
|
11451
|
+
var BUILT_IN_TEMPLATE_IDS = [
|
|
11452
|
+
"book",
|
|
11453
|
+
"zine",
|
|
11454
|
+
"technical"
|
|
11455
|
+
];
|
|
11456
|
+
async function readTemplateChoices(manifestPath) {
|
|
11595
11457
|
try {
|
|
11596
|
-
const
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
const
|
|
11600
|
-
if (
|
|
11601
|
-
|
|
11602
|
-
const
|
|
11603
|
-
if (
|
|
11604
|
-
|
|
11605
|
-
const importFormat = packageEntryFormat(rootRecord.manifest, importEntry);
|
|
11606
|
-
const entry = importFormat === "commonjs" ? rootRecord.requireEntry ?? importEntry : importEntry;
|
|
11607
|
-
const format = packageEntryFormat(rootRecord.manifest, entry);
|
|
11608
|
-
const tree = await computeVendorTreeDigest(stageRoot);
|
|
11609
|
-
const receipt = {
|
|
11610
|
-
schemaVersion: VENDOR_RECEIPT_VERSION,
|
|
11611
|
-
root: {
|
|
11612
|
-
name: rootRecord.name,
|
|
11613
|
-
version: rootRecord.version,
|
|
11614
|
-
packagePath: rootRecord.path,
|
|
11615
|
-
entry,
|
|
11616
|
-
format
|
|
11617
|
-
},
|
|
11618
|
-
packages: ctx.records.map(publicReceipt).sort((a, b) => a.path.localeCompare(b.path)),
|
|
11619
|
-
skipped: ctx.skipped,
|
|
11620
|
-
tree: { algorithm: "sha256", ...tree }
|
|
11621
|
-
};
|
|
11622
|
-
const receiptFile = await open2(path16.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
|
|
11623
|
-
try {
|
|
11624
|
-
await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
|
|
11625
|
-
`, "utf8");
|
|
11626
|
-
await receiptFile.sync();
|
|
11627
|
-
} finally {
|
|
11628
|
-
await receiptFile.close();
|
|
11629
|
-
}
|
|
11630
|
-
finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
|
|
11631
|
-
const realFinalParent = await realpath2(path16.dirname(finalRoot));
|
|
11632
|
-
if (!isContained2(projectDir, realFinalParent)) {
|
|
11633
|
-
throw new Error("Plugin install destination resolves outside the project.");
|
|
11634
|
-
}
|
|
11635
|
-
try {
|
|
11636
|
-
await lstat2(finalRoot);
|
|
11637
|
-
backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
|
|
11638
|
-
await rename4(finalRoot, backupRoot);
|
|
11639
|
-
} catch (error2) {
|
|
11640
|
-
if (error2.code !== "ENOENT")
|
|
11641
|
-
throw error2;
|
|
11642
|
-
}
|
|
11643
|
-
try {
|
|
11644
|
-
await rename4(stageRoot, finalRoot);
|
|
11645
|
-
} catch (error2) {
|
|
11646
|
-
if (backupRoot) {
|
|
11647
|
-
try {
|
|
11648
|
-
await rename4(backupRoot, finalRoot);
|
|
11649
|
-
} catch (restoreError) {
|
|
11650
|
-
throw new Error(`Could not publish the new plugin tree or restore the previous tree: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`, { cause: error2 });
|
|
11651
|
-
}
|
|
11652
|
-
}
|
|
11653
|
-
throw error2;
|
|
11458
|
+
const text = await readFile29(manifestPath, "utf8");
|
|
11459
|
+
const doc = parseDocument2(text);
|
|
11460
|
+
const out = {};
|
|
11461
|
+
const preset = doc.get("preset");
|
|
11462
|
+
if (typeof preset === "string" && preset)
|
|
11463
|
+
out.preset = preset;
|
|
11464
|
+
const targets = doc.get("targets", true);
|
|
11465
|
+
if (isSeq2(targets)) {
|
|
11466
|
+
out.targets = targets.items.map((i) => scalarString(i)).filter((s) => s !== null);
|
|
11654
11467
|
}
|
|
11655
|
-
return
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
installRoot: finalRoot,
|
|
11659
|
-
backupRoot,
|
|
11660
|
-
warnings: [...ctx.warnings]
|
|
11661
|
-
};
|
|
11662
|
-
} finally {
|
|
11663
|
-
await rm3(container, { recursive: true, force: true }).catch(() => {});
|
|
11664
|
-
}
|
|
11665
|
-
}
|
|
11666
|
-
async function finalizeNpmPluginInstall(installed) {
|
|
11667
|
-
if (installed.backupRoot) {
|
|
11668
|
-
await rm3(installed.backupRoot, { recursive: true, force: true });
|
|
11468
|
+
return out;
|
|
11469
|
+
} catch {
|
|
11470
|
+
return {};
|
|
11669
11471
|
}
|
|
11670
11472
|
}
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
}
|
|
11676
|
-
|
|
11677
|
-
// src/lib/extension-manager.ts
|
|
11678
|
-
var EXTENSIONS_DIR = "extensions";
|
|
11679
|
-
var RECOMMENDED_EXTENSIONS = [
|
|
11680
|
-
{
|
|
11681
|
-
use: "markdown-it-mark",
|
|
11682
|
-
label: "Highlight",
|
|
11683
|
-
description: "Highlighted text with `==marked==` -> `<mark>`."
|
|
11684
|
-
},
|
|
11685
|
-
{
|
|
11686
|
-
use: "markdown-it-sub",
|
|
11687
|
-
label: "Subscript",
|
|
11688
|
-
description: "Subscript text with `H~2~O`."
|
|
11689
|
-
},
|
|
11690
|
-
{
|
|
11691
|
-
use: "markdown-it-sup",
|
|
11692
|
-
label: "Superscript",
|
|
11693
|
-
description: "Superscript text with `29^th^`."
|
|
11473
|
+
var BUILT_IN_META = {
|
|
11474
|
+
book: {
|
|
11475
|
+
label: "Book",
|
|
11476
|
+
description: "A clean starting point for a novel, memoir, or any long-form book."
|
|
11694
11477
|
},
|
|
11695
|
-
{
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
|
|
11478
|
+
zine: {
|
|
11479
|
+
label: "Zine",
|
|
11480
|
+
description: "A short, personal zine made to be printed, folded, and shared."
|
|
11699
11481
|
},
|
|
11700
|
-
{
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
|
|
11482
|
+
technical: {
|
|
11483
|
+
label: "Technical document",
|
|
11484
|
+
description: "A manual or guide with steps, reference tables, and code examples."
|
|
11704
11485
|
}
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11486
|
+
};
|
|
11487
|
+
async function listBuiltInTemplates() {
|
|
11488
|
+
return Promise.all(BUILT_IN_TEMPLATE_IDS.map(async (id) => ({
|
|
11489
|
+
id,
|
|
11490
|
+
label: BUILT_IN_META[id].label,
|
|
11491
|
+
description: BUILT_IN_META[id].description,
|
|
11492
|
+
kind: "builtin",
|
|
11493
|
+
...await readTemplateChoices(await getAssetPath(`templates/${id}/manifest.yaml`))
|
|
11494
|
+
})));
|
|
11495
|
+
}
|
|
11496
|
+
var SKIP_ENTRIES = new Set([
|
|
11497
|
+
".git",
|
|
11498
|
+
"node_modules",
|
|
11499
|
+
"out",
|
|
11500
|
+
"dist",
|
|
11501
|
+
"build",
|
|
11502
|
+
".gutterpress",
|
|
11503
|
+
".DS_Store"
|
|
11504
|
+
]);
|
|
11505
|
+
var VENDOR_DIR = "shared";
|
|
11506
|
+
function collectEscaping(nodes, getValue, escapes) {
|
|
11507
|
+
const out = [];
|
|
11508
|
+
for (const node of nodes) {
|
|
11509
|
+
const value = getValue(node);
|
|
11510
|
+
if (value !== null && escapes(value))
|
|
11511
|
+
out.push({ node, value });
|
|
11512
|
+
}
|
|
11513
|
+
return out;
|
|
11514
|
+
}
|
|
11515
|
+
function extensionEntryPath(node) {
|
|
11516
|
+
const use = isMap(node) ? scalarString(node.get("use", true)) : scalarString(node);
|
|
11517
|
+
return use !== null && isPathSpecifier(use) ? use : null;
|
|
11518
|
+
}
|
|
11519
|
+
function dropEscaping(doc, seq, key, escaping) {
|
|
11520
|
+
if (!isSeq2(seq))
|
|
11716
11521
|
return;
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
mutationQueues.delete(key);
|
|
11722
|
-
});
|
|
11723
|
-
return run2;
|
|
11522
|
+
const drop = new Set(escaping.map((e) => e.node));
|
|
11523
|
+
seq.items = seq.items.filter((node) => !drop.has(node));
|
|
11524
|
+
if (seq.items.length === 0)
|
|
11525
|
+
doc.delete(key);
|
|
11724
11526
|
}
|
|
11725
|
-
function
|
|
11726
|
-
if (
|
|
11727
|
-
|
|
11728
|
-
if (
|
|
11729
|
-
|
|
11730
|
-
|
|
11527
|
+
function rewriteEntry(node, newPath) {
|
|
11528
|
+
if (isMap(node))
|
|
11529
|
+
node.set("use", newPath);
|
|
11530
|
+
else if (isScalar(node))
|
|
11531
|
+
node.value = newPath;
|
|
11532
|
+
}
|
|
11533
|
+
function commonAncestorDir(absFiles) {
|
|
11534
|
+
const dirSegs = absFiles.map((f) => path18.resolve(f).split(path18.sep).slice(0, -1));
|
|
11535
|
+
if (dirSegs.length === 0)
|
|
11536
|
+
return "";
|
|
11537
|
+
let common = dirSegs[0];
|
|
11538
|
+
for (const segs of dirSegs.slice(1)) {
|
|
11539
|
+
let i = 0;
|
|
11540
|
+
while (i < common.length && i < segs.length && common[i] === segs[i])
|
|
11541
|
+
i++;
|
|
11542
|
+
common = common.slice(0, i);
|
|
11731
11543
|
}
|
|
11732
|
-
return
|
|
11544
|
+
return common.join(path18.sep) || path18.sep;
|
|
11733
11545
|
}
|
|
11734
|
-
function
|
|
11735
|
-
const
|
|
11736
|
-
if (!
|
|
11737
|
-
return
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
...typeof x === "string" && x.trim() ? { export: x.trim() } : {},
|
|
11744
|
-
enabled: item.get("enabled") !== false
|
|
11745
|
-
};
|
|
11546
|
+
function pickVendorDir(templateDir, base) {
|
|
11547
|
+
const preferred = path18.basename(base) || VENDOR_DIR;
|
|
11548
|
+
if (!existsSync13(path18.join(templateDir, preferred)))
|
|
11549
|
+
return preferred;
|
|
11550
|
+
for (let n = 1;; n++) {
|
|
11551
|
+
const candidate = `${preferred}-${n}`;
|
|
11552
|
+
if (!existsSync13(path18.join(templateDir, candidate)))
|
|
11553
|
+
return candidate;
|
|
11554
|
+
}
|
|
11746
11555
|
}
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11556
|
+
var TEMPLATE_META_FILE = ".gutterpress-template.json";
|
|
11557
|
+
async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
11558
|
+
const { doc, file } = await loadManifestDoc(templateDir);
|
|
11559
|
+
const escapes = (p) => escapesProjectRoot(sourceProjectDir, path18.resolve(sourceProjectDir, p));
|
|
11560
|
+
const stylesSeq = doc.get("styles", true);
|
|
11561
|
+
const extensionsSeq = doc.get("extensions", true);
|
|
11562
|
+
const escapingStyles = collectEscaping(isSeq2(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
|
|
11563
|
+
const escapingExtensions = collectEscaping(isSeq2(extensionsSeq) ? extensionsSeq.items : [], extensionEntryPath, escapes);
|
|
11564
|
+
if (escapingStyles.length === 0 && escapingExtensions.length === 0) {
|
|
11565
|
+
return { vendoredRefs: [], excludedRefs: [] };
|
|
11566
|
+
}
|
|
11567
|
+
if (mode === "exclude") {
|
|
11568
|
+
const excludedRefs = [...escapingStyles, ...escapingExtensions].map((e) => e.value);
|
|
11569
|
+
dropEscaping(doc, stylesSeq, "styles", escapingStyles);
|
|
11570
|
+
dropEscaping(doc, extensionsSeq, "extensions", escapingExtensions);
|
|
11571
|
+
await writeManifestDoc(file, doc);
|
|
11572
|
+
return { vendoredRefs: [], excludedRefs };
|
|
11573
|
+
}
|
|
11574
|
+
const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
|
|
11575
|
+
const extensionFiles = escapingExtensions.map((p) => path18.resolve(sourceProjectDir, p.value));
|
|
11576
|
+
const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
|
|
11577
|
+
const base = commonAncestorDir(externalFiles);
|
|
11578
|
+
const vendorDir = pickVendorDir(templateDir, base);
|
|
11579
|
+
await Promise.all(externalFiles.filter((abs) => existsSync13(abs)).map(async (abs) => {
|
|
11580
|
+
const dest = path18.join(templateDir, vendorDir, path18.relative(base, abs));
|
|
11581
|
+
await mkdir9(path18.dirname(dest), { recursive: true });
|
|
11582
|
+
await cp3(abs, dest, { recursive: true });
|
|
11583
|
+
}));
|
|
11584
|
+
const bookLocal = (value) => toPosixPath(path18.join(vendorDir, path18.relative(base, path18.resolve(sourceProjectDir, value))));
|
|
11585
|
+
const vendoredRefs = [];
|
|
11586
|
+
for (const { node, value } of escapingStyles) {
|
|
11587
|
+
const rewritten = bookLocal(value);
|
|
11588
|
+
rewriteEntry(node, rewritten);
|
|
11589
|
+
vendoredRefs.push(rewritten);
|
|
11590
|
+
}
|
|
11591
|
+
for (const { node, value } of escapingExtensions) {
|
|
11592
|
+
const rewritten = bookLocal(value);
|
|
11593
|
+
rewriteEntry(node, `./${rewritten}`);
|
|
11594
|
+
vendoredRefs.push(rewritten);
|
|
11595
|
+
}
|
|
11596
|
+
await writeManifestDoc(file, doc);
|
|
11597
|
+
return { vendoredRefs, excludedRefs: [] };
|
|
11598
|
+
}
|
|
11599
|
+
async function saveProjectAsTemplate(options) {
|
|
11600
|
+
const { projectDir, name, templatesRoot } = options;
|
|
11601
|
+
const id = slugify(name);
|
|
11602
|
+
if (!id)
|
|
11603
|
+
throw new Error(`Could not derive a template id from "${name}".`);
|
|
11604
|
+
const dir = path18.join(templatesRoot, id);
|
|
11605
|
+
let exists = true;
|
|
11752
11606
|
try {
|
|
11753
|
-
|
|
11754
|
-
pb = parseExtensionSpecifier(b);
|
|
11607
|
+
await access3(dir, FS3.F_OK);
|
|
11755
11608
|
} catch {
|
|
11756
|
-
|
|
11609
|
+
exists = false;
|
|
11757
11610
|
}
|
|
11758
|
-
|
|
11759
|
-
}
|
|
11760
|
-
function indexOfUse(seq, use) {
|
|
11761
|
-
const items = seq.items;
|
|
11762
|
-
const exact = items.findIndex((item) => entryUse(item) === use.trim());
|
|
11763
|
-
if (exact >= 0)
|
|
11764
|
-
return exact;
|
|
11765
|
-
return items.findIndex((item) => {
|
|
11766
|
-
const u = entryUse(item);
|
|
11767
|
-
return !!u && sameExtension(u, use);
|
|
11768
|
-
});
|
|
11769
|
-
}
|
|
11770
|
-
function manifestPathFor(projectDir, abs) {
|
|
11771
|
-
const rel = path17.relative(path17.resolve(projectDir), abs).split(path17.sep).join("/");
|
|
11772
|
-
if (rel === "") {
|
|
11773
|
-
throw new Error("An extension cannot be the project folder itself.");
|
|
11611
|
+
if (exists) {
|
|
11612
|
+
throw new Error(`A template named "${name}" already exists. Choose a different name.`);
|
|
11774
11613
|
}
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11614
|
+
await mkdir9(dir, { recursive: true });
|
|
11615
|
+
const entries = await readdir7(projectDir, { withFileTypes: true });
|
|
11616
|
+
for (const entry of entries) {
|
|
11617
|
+
if (SKIP_ENTRIES.has(entry.name))
|
|
11618
|
+
continue;
|
|
11619
|
+
if (entry.name === TEMPLATE_META_FILE)
|
|
11620
|
+
continue;
|
|
11621
|
+
await cp3(path18.join(projectDir, entry.name), path18.join(dir, entry.name), { recursive: true });
|
|
11622
|
+
}
|
|
11623
|
+
const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
|
|
11624
|
+
const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
|
|
11625
|
+
if (manifestName)
|
|
11626
|
+
await retokeniseManifest(path18.join(dir, manifestName));
|
|
11627
|
+
await writeFile10(path18.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
|
|
11788
11628
|
return {
|
|
11789
|
-
|
|
11790
|
-
label:
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
...
|
|
11795
|
-
...styles.length > 0 ? { styles } : {},
|
|
11796
|
-
carries: extensionCarries(meta),
|
|
11797
|
-
dir
|
|
11629
|
+
id,
|
|
11630
|
+
label: name,
|
|
11631
|
+
description: "Your saved template.",
|
|
11632
|
+
kind: "custom",
|
|
11633
|
+
dir,
|
|
11634
|
+
...shared
|
|
11798
11635
|
};
|
|
11799
11636
|
}
|
|
11800
|
-
async function
|
|
11801
|
-
|
|
11802
|
-
let parsed = null;
|
|
11637
|
+
async function retokeniseManifest(manifestPath) {
|
|
11638
|
+
let text;
|
|
11803
11639
|
try {
|
|
11804
|
-
|
|
11805
|
-
} catch
|
|
11806
|
-
|
|
11807
|
-
}
|
|
11808
|
-
const common = {
|
|
11809
|
-
use: raw.use,
|
|
11810
|
-
enabled: raw.enabled,
|
|
11811
|
-
...raw.export ? { export: raw.export } : {}
|
|
11812
|
-
};
|
|
11813
|
-
const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
11814
|
-
if (!parsed) {
|
|
11815
|
-
return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
|
|
11640
|
+
text = await readFile29(manifestPath, "utf8");
|
|
11641
|
+
} catch {
|
|
11642
|
+
return;
|
|
11816
11643
|
}
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
carries: { ...NO_CARRIES, markdown: true }
|
|
11827
|
-
});
|
|
11644
|
+
text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
|
|
11645
|
+
await writeFile10(manifestPath, text, "utf8");
|
|
11646
|
+
}
|
|
11647
|
+
async function listCustomTemplates(templatesRoot) {
|
|
11648
|
+
let entries;
|
|
11649
|
+
try {
|
|
11650
|
+
entries = await readdir7(templatesRoot, { withFileTypes: true });
|
|
11651
|
+
} catch {
|
|
11652
|
+
return [];
|
|
11828
11653
|
}
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11654
|
+
const out = [];
|
|
11655
|
+
for (const entry of entries) {
|
|
11656
|
+
if (!entry.isDirectory())
|
|
11657
|
+
continue;
|
|
11658
|
+
const dir = path18.join(templatesRoot, entry.name);
|
|
11659
|
+
let label = prettify(entry.name);
|
|
11833
11660
|
try {
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
carries: { ...NO_CARRIES, markdown: true }
|
|
11661
|
+
const meta = JSON.parse(await readFile29(path18.join(dir, TEMPLATE_META_FILE), "utf8"));
|
|
11662
|
+
if (typeof meta.label === "string" && meta.label)
|
|
11663
|
+
label = meta.label;
|
|
11664
|
+
} catch {}
|
|
11665
|
+
out.push({
|
|
11666
|
+
id: entry.name,
|
|
11667
|
+
label,
|
|
11668
|
+
description: "Your saved template.",
|
|
11669
|
+
kind: "custom",
|
|
11670
|
+
dir,
|
|
11671
|
+
...await readTemplateChoices(path18.join(dir, MANIFEST_FILENAMES[0]))
|
|
11846
11672
|
});
|
|
11847
11673
|
}
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11674
|
+
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
11675
|
+
return out;
|
|
11676
|
+
}
|
|
11677
|
+
async function importTemplateFromFolder(options) {
|
|
11678
|
+
const { sourceDir, templatesRoot } = options;
|
|
11679
|
+
const srcStat = await stat6(sourceDir);
|
|
11680
|
+
if (!srcStat.isDirectory()) {
|
|
11681
|
+
throw new Error("The chosen path is not a folder.");
|
|
11682
|
+
}
|
|
11683
|
+
const name = options.name?.trim() || prettify(path18.basename(sourceDir));
|
|
11684
|
+
return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
|
|
11685
|
+
}
|
|
11686
|
+
// src/lib/snippets.ts
|
|
11687
|
+
import { readdir as readdir8, readFile as readFile32, writeFile as writeFile11, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
|
|
11688
|
+
import path21 from "node:path";
|
|
11689
|
+
|
|
11690
|
+
// src/lib/extension-manager.ts
|
|
11691
|
+
import { cp as cp4, mkdir as mkdir11, readFile as readFile31, rm as rm4, stat as stat7 } from "node:fs/promises";
|
|
11692
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
11693
|
+
import path20 from "node:path";
|
|
11694
|
+
import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
|
|
11695
|
+
|
|
11696
|
+
// src/lib/npm-plugin-installer.ts
|
|
11697
|
+
import { createHash as createHash7, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
11698
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
11699
|
+
import {
|
|
11700
|
+
lstat as lstat2,
|
|
11701
|
+
mkdir as mkdir10,
|
|
11702
|
+
mkdtemp as mkdtemp2,
|
|
11703
|
+
open as open2,
|
|
11704
|
+
readFile as readFile30,
|
|
11705
|
+
realpath as realpath2,
|
|
11706
|
+
rename as rename4,
|
|
11707
|
+
rm as rm3
|
|
11708
|
+
} from "node:fs/promises";
|
|
11709
|
+
import path19 from "node:path";
|
|
11710
|
+
import { Transform } from "node:stream";
|
|
11711
|
+
import { pipeline } from "node:stream/promises";
|
|
11712
|
+
import { createGunzip } from "node:zlib";
|
|
11713
|
+
import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
|
|
11714
|
+
|
|
11715
|
+
// src/lib/fetch-timeout.ts
|
|
11716
|
+
class FetchPassthroughError extends Error {
|
|
11717
|
+
}
|
|
11718
|
+
|
|
11719
|
+
class FriendlyHttpError extends FetchPassthroughError {
|
|
11720
|
+
}
|
|
11721
|
+
|
|
11722
|
+
class FetchUnavailableError extends Error {
|
|
11723
|
+
}
|
|
11724
|
+
function composeSignal(signal, timeoutMs) {
|
|
11725
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
11726
|
+
return {
|
|
11727
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
11728
|
+
timeout
|
|
11853
11729
|
};
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
}
|
|
11858
|
-
let installRoot = null;
|
|
11730
|
+
}
|
|
11731
|
+
async function withFetchTimeout(options, run2) {
|
|
11732
|
+
const composed = composeSignal(options.signal, options.timeoutMs);
|
|
11859
11733
|
try {
|
|
11860
|
-
|
|
11861
|
-
} catch (
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11734
|
+
return await run2(composed.signal);
|
|
11735
|
+
} catch (cause) {
|
|
11736
|
+
if (cause instanceof FetchPassthroughError)
|
|
11737
|
+
throw cause;
|
|
11738
|
+
if (options.signal?.aborted)
|
|
11739
|
+
throw cause;
|
|
11740
|
+
const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
|
|
11741
|
+
const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
|
|
11742
|
+
if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
|
|
11743
|
+
throw cause;
|
|
11744
|
+
throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
|
|
11871
11745
|
}
|
|
11872
|
-
const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
|
|
11873
|
-
return withWarnings({
|
|
11874
|
-
...described,
|
|
11875
|
-
carries: { ...described.carries, markdown: true }
|
|
11876
|
-
});
|
|
11877
11746
|
}
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11747
|
+
|
|
11748
|
+
// src/lib/npm-plugin-installer.ts
|
|
11749
|
+
var DEFAULT_LIMITS = {
|
|
11750
|
+
metadataBytes: 15 * 1024 * 1024,
|
|
11751
|
+
packageTarballBytes: 50 * 1024 * 1024,
|
|
11752
|
+
totalNetworkBytes: 250 * 1024 * 1024,
|
|
11753
|
+
packageFileBytes: 50 * 1024 * 1024,
|
|
11754
|
+
totalUnpackedBytes: 512 * 1024 * 1024,
|
|
11755
|
+
totalFiles: 50000,
|
|
11756
|
+
totalPackages: 512,
|
|
11757
|
+
dependencyDepth: 64
|
|
11758
|
+
};
|
|
11759
|
+
var METADATA_TIMEOUT_MS = 30000;
|
|
11760
|
+
var TARBALL_TIMEOUT_MS = 120000;
|
|
11761
|
+
|
|
11762
|
+
class SecurityInstallError extends FetchPassthroughError {
|
|
11763
|
+
}
|
|
11764
|
+
|
|
11765
|
+
class PackageUnavailableError extends Error {
|
|
11766
|
+
}
|
|
11767
|
+
function isOptionalAvailabilityError(error2) {
|
|
11768
|
+
return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
|
|
11769
|
+
}
|
|
11770
|
+
function object3(value) {
|
|
11771
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
11772
|
+
}
|
|
11773
|
+
function stringMap2(value) {
|
|
11774
|
+
const map = object3(value);
|
|
11775
|
+
if (!map)
|
|
11776
|
+
return {};
|
|
11777
|
+
const result = {};
|
|
11778
|
+
for (const [name, selector] of Object.entries(map)) {
|
|
11779
|
+
if (typeof selector === "string" && selector.trim())
|
|
11780
|
+
result[name] = selector.trim();
|
|
11889
11781
|
}
|
|
11890
|
-
return
|
|
11782
|
+
return result;
|
|
11891
11783
|
}
|
|
11892
|
-
|
|
11893
|
-
return
|
|
11784
|
+
function mb(bytes) {
|
|
11785
|
+
return `${Math.ceil(bytes / 1024 / 1024)}MB`;
|
|
11894
11786
|
}
|
|
11895
|
-
function
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
...entry.kind === "path" ? { path: entry.name } : { name: entry.name, ...entry.version ? { version: entry.version } : {} },
|
|
11899
|
-
...entry.export ? { export: entry.export } : {},
|
|
11900
|
-
options: {}
|
|
11901
|
-
};
|
|
11787
|
+
function isContained2(root, candidate) {
|
|
11788
|
+
const relative2 = path19.relative(root, candidate);
|
|
11789
|
+
return relative2 === "" || !relative2.startsWith("..") && !path19.isAbsolute(relative2);
|
|
11902
11790
|
}
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11791
|
+
function mergeLimits(overrides) {
|
|
11792
|
+
const limits = { ...DEFAULT_LIMITS, ...overrides };
|
|
11793
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
11794
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
11795
|
+
throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
|
|
11796
|
+
}
|
|
11797
|
+
}
|
|
11798
|
+
return limits;
|
|
11799
|
+
}
|
|
11800
|
+
async function streamResponse(ctx, response, maxBytes, totalLimitMessage, onChunk) {
|
|
11801
|
+
if (!response.body)
|
|
11802
|
+
throw new Error("Registry response had no body.");
|
|
11803
|
+
const declared = Number(response.headers.get("content-length"));
|
|
11804
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
11805
|
+
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
11806
|
+
}
|
|
11807
|
+
if (Number.isFinite(declared) && declared > ctx.limits.totalNetworkBytes - ctx.totalNetworkBytes) {
|
|
11808
|
+
throw new SecurityInstallError(totalLimitMessage);
|
|
11809
|
+
}
|
|
11810
|
+
const reader = response.body.getReader();
|
|
11811
|
+
let total = 0;
|
|
11812
|
+
try {
|
|
11813
|
+
while (true) {
|
|
11814
|
+
const { done, value } = await reader.read();
|
|
11815
|
+
if (done)
|
|
11816
|
+
break;
|
|
11817
|
+
total += value.length;
|
|
11818
|
+
if (total > maxBytes) {
|
|
11819
|
+
await reader.cancel("size limit exceeded").catch(() => {});
|
|
11820
|
+
throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
|
|
11917
11821
|
}
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
seq.items[idx] = map;
|
|
11822
|
+
if (ctx.totalNetworkBytes + value.length > ctx.limits.totalNetworkBytes) {
|
|
11823
|
+
await reader.cancel("total size limit exceeded").catch(() => {});
|
|
11824
|
+
throw new SecurityInstallError(totalLimitMessage);
|
|
11825
|
+
}
|
|
11826
|
+
ctx.totalNetworkBytes += value.length;
|
|
11827
|
+
await onChunk(value);
|
|
11925
11828
|
}
|
|
11926
|
-
|
|
11927
|
-
|
|
11829
|
+
} finally {
|
|
11830
|
+
reader.releaseLock();
|
|
11831
|
+
}
|
|
11832
|
+
return total;
|
|
11928
11833
|
}
|
|
11929
|
-
async function
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11834
|
+
async function fetchPackument(ctx, name) {
|
|
11835
|
+
let pending = ctx.packuments.get(name);
|
|
11836
|
+
if (pending)
|
|
11837
|
+
return pending;
|
|
11838
|
+
pending = withFetchTimeout({
|
|
11839
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
11840
|
+
signal: ctx.signal,
|
|
11841
|
+
timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
|
|
11842
|
+
offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
11843
|
+
}, async (signal) => {
|
|
11844
|
+
const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
|
|
11845
|
+
signal,
|
|
11846
|
+
redirect: "error",
|
|
11847
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
11848
|
+
});
|
|
11849
|
+
if (response.status === 404) {
|
|
11850
|
+
throw new FriendlyHttpError(`npm package "${name}" was not found.`);
|
|
11943
11851
|
}
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11852
|
+
if (!response.ok) {
|
|
11853
|
+
throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
|
|
11854
|
+
}
|
|
11855
|
+
const chunks = [];
|
|
11856
|
+
const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
|
|
11857
|
+
chunks.push(chunk2);
|
|
11948
11858
|
});
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11955
|
-
const seq = doc.get("extensions", true);
|
|
11956
|
-
const idx = isSeq2(seq) ? indexOfUse(seq, use) : -1;
|
|
11957
|
-
if (!isSeq2(seq) || idx < 0)
|
|
11958
|
-
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
11959
|
-
const written = entryUse(seq.items[idx]);
|
|
11960
|
-
seq.items.splice(idx, 1);
|
|
11961
|
-
if (seq.items.length === 0)
|
|
11962
|
-
doc.delete("extensions");
|
|
11963
|
-
await writeManifestDoc(file, doc);
|
|
11964
|
-
let parsed = null;
|
|
11965
|
-
try {
|
|
11966
|
-
parsed = parseExtensionSpecifier(written);
|
|
11967
|
-
} catch {
|
|
11968
|
-
return;
|
|
11859
|
+
const bytes = new Uint8Array(size);
|
|
11860
|
+
let offset = 0;
|
|
11861
|
+
for (const chunk2 of chunks) {
|
|
11862
|
+
bytes.set(chunk2, offset);
|
|
11863
|
+
offset += chunk2.length;
|
|
11969
11864
|
}
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11865
|
+
let metadata = null;
|
|
11866
|
+
try {
|
|
11867
|
+
metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
|
|
11868
|
+
} catch {}
|
|
11869
|
+
if (!metadata || metadata.name !== name) {
|
|
11870
|
+
throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
|
|
11976
11871
|
}
|
|
11872
|
+
return metadata;
|
|
11873
|
+
}).catch((error2) => {
|
|
11874
|
+
ctx.packuments.delete(name);
|
|
11875
|
+
throw error2;
|
|
11977
11876
|
});
|
|
11877
|
+
ctx.packuments.set(name, pending);
|
|
11878
|
+
return pending;
|
|
11978
11879
|
}
|
|
11979
|
-
|
|
11980
|
-
const
|
|
11981
|
-
const
|
|
11982
|
-
const
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
else
|
|
11990
|
-
item.delete("export");
|
|
11991
|
-
if (item.items.length === 1)
|
|
11992
|
-
seq.items[idx] = new Scalar(use);
|
|
11993
|
-
} else if (exportName) {
|
|
11994
|
-
const map = new YAMLMap(doc.schema);
|
|
11995
|
-
map.set("use", use);
|
|
11996
|
-
map.set("export", exportName);
|
|
11997
|
-
seq.items[idx] = map;
|
|
11998
|
-
} else {
|
|
11999
|
-
seq.items[idx] = new Scalar(use);
|
|
11880
|
+
function parseIntegrity(dist) {
|
|
11881
|
+
const integrity = typeof dist.integrity === "string" ? dist.integrity.trim() : "";
|
|
11882
|
+
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]));
|
|
11883
|
+
for (const algorithm of ["sha512", "sha384", "sha256"]) {
|
|
11884
|
+
const match = parsed.find((candidate) => candidate[1] === algorithm);
|
|
11885
|
+
if (!match)
|
|
11886
|
+
continue;
|
|
11887
|
+
const digest = Buffer.from(match[2], "base64");
|
|
11888
|
+
if (digest.length > 0) {
|
|
11889
|
+
return { algorithm, digest, label: `${algorithm}-${match[2]}`, legacySha1: false };
|
|
12000
11890
|
}
|
|
12001
|
-
} else if (exportName) {
|
|
12002
|
-
const map = new YAMLMap(doc.schema);
|
|
12003
|
-
map.set("use", use);
|
|
12004
|
-
map.set("export", exportName);
|
|
12005
|
-
seq.add(map);
|
|
12006
|
-
} else {
|
|
12007
|
-
seq.add(new Scalar(use));
|
|
12008
11891
|
}
|
|
12009
|
-
|
|
11892
|
+
const sha1 = parsed.find((candidate) => candidate[1] === "sha1");
|
|
11893
|
+
if (sha1) {
|
|
11894
|
+
return {
|
|
11895
|
+
algorithm: "sha1",
|
|
11896
|
+
digest: Buffer.from(sha1[2], "base64"),
|
|
11897
|
+
label: `sha1-${sha1[2]}`,
|
|
11898
|
+
legacySha1: true
|
|
11899
|
+
};
|
|
11900
|
+
}
|
|
11901
|
+
if (integrity) {
|
|
11902
|
+
throw new SecurityInstallError("The npm registry supplied integrity data, but no supported SHA-256-or-stronger hash.");
|
|
11903
|
+
}
|
|
11904
|
+
const shasum = typeof dist.shasum === "string" ? dist.shasum.trim().toLowerCase() : "";
|
|
11905
|
+
if (/^[a-f0-9]{40}$/.test(shasum)) {
|
|
11906
|
+
return {
|
|
11907
|
+
algorithm: "sha1",
|
|
11908
|
+
digest: Buffer.from(shasum, "hex"),
|
|
11909
|
+
label: `sha1-hex-${shasum}`,
|
|
11910
|
+
legacySha1: true
|
|
11911
|
+
};
|
|
11912
|
+
}
|
|
11913
|
+
throw new SecurityInstallError("The npm registry provided no usable integrity hash, so the package was not installed.");
|
|
12010
11914
|
}
|
|
12011
|
-
|
|
11915
|
+
function unsupportedSelector(selector) {
|
|
11916
|
+
return /^(?:file:|git\+|https?:|ssh:|github:|workspace:|link:|npm:)/i.test(selector);
|
|
11917
|
+
}
|
|
11918
|
+
async function selectPackage(ctx, name, selector) {
|
|
11919
|
+
if (selector && unsupportedSelector(selector)) {
|
|
11920
|
+
throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
|
|
11921
|
+
}
|
|
11922
|
+
const metadata = await fetchPackument(ctx, name);
|
|
11923
|
+
const versions = object3(metadata.versions);
|
|
11924
|
+
const tags = object3(metadata["dist-tags"]);
|
|
11925
|
+
if (!versions || !tags)
|
|
11926
|
+
throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
|
|
11927
|
+
let version;
|
|
11928
|
+
if (!selector) {
|
|
11929
|
+
if (typeof tags.latest === "string")
|
|
11930
|
+
version = tags.latest;
|
|
11931
|
+
} else if (object3(versions[selector])) {
|
|
11932
|
+
version = selector;
|
|
11933
|
+
} else if (typeof tags[selector] === "string") {
|
|
11934
|
+
version = tags[selector];
|
|
11935
|
+
} else if (validRange2(selector)) {
|
|
11936
|
+
version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
|
|
11937
|
+
}
|
|
11938
|
+
if (!version || !isExactNpmVersion(version)) {
|
|
11939
|
+
throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
|
|
11940
|
+
}
|
|
11941
|
+
const selected = object3(versions[version]);
|
|
11942
|
+
if (!selected || selected.name !== name || selected.version !== version) {
|
|
11943
|
+
throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
|
|
11944
|
+
}
|
|
11945
|
+
const dist = object3(selected.dist);
|
|
11946
|
+
if (!dist || typeof dist.tarball !== "string") {
|
|
11947
|
+
throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
|
|
11948
|
+
}
|
|
11949
|
+
let tarball;
|
|
12012
11950
|
try {
|
|
12013
|
-
|
|
11951
|
+
tarball = new URL(dist.tarball);
|
|
12014
11952
|
} catch {
|
|
12015
|
-
throw new
|
|
11953
|
+
throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
|
|
12016
11954
|
}
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
return
|
|
11955
|
+
if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
|
|
11956
|
+
throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
|
|
11957
|
+
}
|
|
11958
|
+
return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
|
|
12021
11959
|
}
|
|
12022
|
-
async function
|
|
12023
|
-
|
|
11960
|
+
async function downloadTarball(ctx, pkg, archivePath) {
|
|
11961
|
+
const file = await open2(archivePath, "wx");
|
|
11962
|
+
const hash = createHash7(pkg.integrity.algorithm);
|
|
11963
|
+
try {
|
|
11964
|
+
await withFetchTimeout({
|
|
11965
|
+
timeoutMs: TARBALL_TIMEOUT_MS,
|
|
11966
|
+
signal: ctx.signal,
|
|
11967
|
+
timeoutMessage: `Downloading ${pkg.name}@${pkg.version} timed out.`,
|
|
11968
|
+
offlineMessage: (cause) => `Downloading ${pkg.name}@${pkg.version} failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
11969
|
+
}, async (signal) => {
|
|
11970
|
+
const response = await ctx.fetch(pkg.tarball, { signal, redirect: "error" });
|
|
11971
|
+
if (!response.ok) {
|
|
11972
|
+
throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
|
|
11973
|
+
}
|
|
11974
|
+
await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
|
|
11975
|
+
hash.update(chunk2);
|
|
11976
|
+
try {
|
|
11977
|
+
await file.write(chunk2);
|
|
11978
|
+
} catch (cause) {
|
|
11979
|
+
throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
|
|
11980
|
+
}
|
|
11981
|
+
});
|
|
11982
|
+
});
|
|
11983
|
+
} finally {
|
|
11984
|
+
await file.close();
|
|
11985
|
+
}
|
|
11986
|
+
const actual = hash.digest();
|
|
11987
|
+
if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
|
|
11988
|
+
throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
|
|
11989
|
+
}
|
|
12024
11990
|
}
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
if (path17.isAbsolute(input))
|
|
12029
|
-
return addPathExtension(projectDir, input, exportName);
|
|
12030
|
-
const parsed = parseExtensionSpecifier(input);
|
|
12031
|
-
if (parsed.kind === "path") {
|
|
12032
|
-
return addPathExtension(projectDir, path17.resolve(projectDir, parsed.path), exportName);
|
|
11991
|
+
function safeTarRelative(entryPath, packageName) {
|
|
11992
|
+
if (!entryPath || entryPath.includes("\\") || entryPath.startsWith("/") || /^[a-zA-Z]:/.test(entryPath)) {
|
|
11993
|
+
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath || "(empty)"}`);
|
|
12033
11994
|
}
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
11995
|
+
const parts = entryPath.split("/");
|
|
11996
|
+
if (parts.at(-1) === "")
|
|
11997
|
+
parts.pop();
|
|
11998
|
+
const legacyTypesRoot = packageName.startsWith("@types/") ? packageName.slice("@types/".length) : null;
|
|
11999
|
+
if (parts.some((part) => !part || part === "." || part === "..") || parts[0] !== "package" && parts[0] !== legacyTypesRoot) {
|
|
12000
|
+
throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath}`);
|
|
12039
12001
|
}
|
|
12040
|
-
const
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12002
|
+
const relative2 = parts.slice(1).join("/");
|
|
12003
|
+
if (!relative2)
|
|
12004
|
+
return { relative: relative2, key: "" };
|
|
12005
|
+
assertWindowsSafeRelativePath(relative2);
|
|
12006
|
+
if (relative2.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
|
|
12007
|
+
throw new SecurityInstallError(`The npm tarball contains bundled node_modules (${entryPath}); bundled dependencies are not accepted.`);
|
|
12008
|
+
}
|
|
12009
|
+
return { relative: relative2, key: windowsPathKey(relative2) };
|
|
12010
|
+
}
|
|
12011
|
+
async function extractTarball(ctx, archivePath, packageDir, packageName) {
|
|
12012
|
+
const { extract } = await import("tar");
|
|
12013
|
+
let rejected = null;
|
|
12014
|
+
const seen = new Set;
|
|
12015
|
+
const metadataTypes = new Set([
|
|
12016
|
+
"GlobalExtendedHeader",
|
|
12017
|
+
"ExtendedHeader",
|
|
12018
|
+
"NextFileHasLongLinkpath",
|
|
12019
|
+
"NextFileHasLongPath",
|
|
12020
|
+
"OldGnuLongPath",
|
|
12021
|
+
"OldExtendedHeader"
|
|
12022
|
+
]);
|
|
12023
|
+
await mkdir10(packageDir, { recursive: true });
|
|
12024
|
+
const unpack = extract({
|
|
12025
|
+
cwd: packageDir,
|
|
12026
|
+
strip: 1,
|
|
12027
|
+
strict: true,
|
|
12028
|
+
preservePaths: false,
|
|
12029
|
+
preserveOwner: false,
|
|
12030
|
+
noMtime: true,
|
|
12031
|
+
chmod: false,
|
|
12032
|
+
maxDepth: ctx.limits.dependencyDepth * 4,
|
|
12033
|
+
filter: (entryPath, rawEntry) => {
|
|
12034
|
+
if (rejected)
|
|
12035
|
+
return false;
|
|
12036
|
+
const entry = rawEntry;
|
|
12037
|
+
if (metadataTypes.has(entry.type))
|
|
12038
|
+
return true;
|
|
12039
|
+
if (entry.type === "Link" || entry.type === "SymbolicLink") {
|
|
12040
|
+
rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
|
|
12041
|
+
return false;
|
|
12042
|
+
}
|
|
12043
|
+
if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
|
|
12044
|
+
rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
|
|
12045
|
+
return false;
|
|
12046
|
+
}
|
|
12047
|
+
try {
|
|
12048
|
+
const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
|
|
12049
|
+
if (!relative2) {
|
|
12050
|
+
if (entry.type === "Directory")
|
|
12051
|
+
return true;
|
|
12052
|
+
throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
|
|
12053
|
+
}
|
|
12054
|
+
if (seen.has(key)) {
|
|
12055
|
+
throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
|
|
12056
|
+
}
|
|
12057
|
+
seen.add(key);
|
|
12058
|
+
ctx.totalFiles++;
|
|
12059
|
+
if (ctx.totalFiles > ctx.limits.totalFiles) {
|
|
12060
|
+
throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
|
|
12061
|
+
}
|
|
12062
|
+
if (entry.type !== "Directory") {
|
|
12063
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
|
|
12064
|
+
throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
|
|
12065
|
+
}
|
|
12066
|
+
}
|
|
12067
|
+
return true;
|
|
12068
|
+
} catch (error2) {
|
|
12069
|
+
rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
|
|
12070
|
+
return false;
|
|
12071
|
+
}
|
|
12072
|
+
}
|
|
12073
|
+
});
|
|
12074
|
+
const limiter = new Transform({
|
|
12075
|
+
transform(chunk2, _encoding, callback) {
|
|
12076
|
+
ctx.totalUnpackedBytes += chunk2.length;
|
|
12077
|
+
if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
|
|
12078
|
+
callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
|
|
12079
|
+
} else
|
|
12080
|
+
callback(null, chunk2);
|
|
12060
12081
|
}
|
|
12061
|
-
throw new Error(`Downloaded ${use}, but it is not a loadable markdown-it plugin: ` + `${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
12062
|
-
}
|
|
12063
|
-
const warnings = [...installed.warnings];
|
|
12064
|
-
await finalizeNpmPluginInstall(installed).catch((error2) => {
|
|
12065
|
-
warnings.push(`The extension was installed, but an old backup could not be removed: ` + `${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
12066
12082
|
});
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
}
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
out.push({
|
|
12076
|
-
id,
|
|
12077
|
-
name: prettify(meta.name?.trim() || id),
|
|
12078
|
-
description: meta.description?.trim() || ""
|
|
12079
|
-
});
|
|
12083
|
+
try {
|
|
12084
|
+
await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
|
|
12085
|
+
} catch (error2) {
|
|
12086
|
+
if (rejected)
|
|
12087
|
+
throw rejected;
|
|
12088
|
+
if (error2 instanceof SecurityInstallError)
|
|
12089
|
+
throw error2;
|
|
12090
|
+
throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
|
|
12080
12091
|
}
|
|
12081
|
-
|
|
12092
|
+
if (rejected)
|
|
12093
|
+
throw rejected;
|
|
12082
12094
|
}
|
|
12083
|
-
async function
|
|
12084
|
-
const
|
|
12085
|
-
let
|
|
12086
|
-
|
|
12087
|
-
|
|
12095
|
+
async function readInstalledManifest(packageDir, selected) {
|
|
12096
|
+
const packageJsonPath = path19.join(packageDir, "package.json");
|
|
12097
|
+
let manifest = null;
|
|
12098
|
+
try {
|
|
12099
|
+
const info2 = await lstat2(packageJsonPath);
|
|
12100
|
+
if (!info2.isFile() || info2.isSymbolicLink())
|
|
12101
|
+
throw new Error("not a regular file");
|
|
12102
|
+
manifest = object3(JSON.parse(await readFile30(packageJsonPath, "utf8")));
|
|
12103
|
+
} catch (error2) {
|
|
12104
|
+
throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
|
|
12088
12105
|
}
|
|
12089
|
-
|
|
12090
|
-
}
|
|
12091
|
-
async function addBuiltInStyleSet(projectDir, id) {
|
|
12092
|
-
if (!BUILT_IN_STYLE_SET_IDS.includes(id)) {
|
|
12093
|
-
throw new Error(`Unknown built-in look "${id}" (one of: ${BUILT_IN_STYLE_SET_IDS.join(", ")}).`);
|
|
12106
|
+
if (manifest?.name !== selected.name || manifest.version !== selected.version) {
|
|
12107
|
+
throw new SecurityInstallError(`Downloaded ${String(manifest?.name)}@${String(manifest?.version)} instead of ${selected.name}@${selected.version}.`);
|
|
12094
12108
|
}
|
|
12095
|
-
return
|
|
12096
|
-
const dest = path17.join(projectDir, EXTENSIONS_DIR, id);
|
|
12097
|
-
if (!existsSync12(dest)) {
|
|
12098
|
-
const src = path17.join(await getAssetsDir(), "themes", id);
|
|
12099
|
-
await mkdir8(path17.dirname(dest), { recursive: true });
|
|
12100
|
-
await cp2(src, dest, { recursive: true });
|
|
12101
|
-
}
|
|
12102
|
-
return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
|
|
12103
|
-
});
|
|
12109
|
+
return manifest;
|
|
12104
12110
|
}
|
|
12105
|
-
async function
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
}
|
|
12112
|
-
try {
|
|
12113
|
-
await loadPlugin(extensionConfigFor(entry), projectDir);
|
|
12114
|
-
out.push({ use: entry.use, kind: entry.kind, enabled: true, ok: true });
|
|
12115
|
-
} catch (e) {
|
|
12116
|
-
out.push({
|
|
12117
|
-
use: entry.use,
|
|
12118
|
-
kind: entry.kind,
|
|
12119
|
-
enabled: true,
|
|
12120
|
-
ok: false,
|
|
12121
|
-
error: e instanceof Error ? e.message : String(e)
|
|
12122
|
-
});
|
|
12111
|
+
async function optionalPackageEntry(packageDir, manifest, condition) {
|
|
12112
|
+
try {
|
|
12113
|
+
return await resolvePackageEntry(packageDir, manifest, condition);
|
|
12114
|
+
} catch (error2) {
|
|
12115
|
+
if (error2 instanceof Error && error2.message.includes("no contained JavaScript entry point")) {
|
|
12116
|
+
return;
|
|
12123
12117
|
}
|
|
12118
|
+
throw error2;
|
|
12124
12119
|
}
|
|
12125
|
-
return out;
|
|
12126
|
-
}
|
|
12127
|
-
async function readExtensionCss(projectDir, use) {
|
|
12128
|
-
const entry = await describeExtension(projectDir, use);
|
|
12129
|
-
if (!entry)
|
|
12130
|
-
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
12131
|
-
if (!entry.dir)
|
|
12132
|
-
throw new Error(`Extension "${use}" has no stylesheets to preview.`);
|
|
12133
|
-
const meta = await readExtensionMeta(entry.dir);
|
|
12134
|
-
const sheets = resolveExtension(entry.dir, meta, `Extension "${use}"`).styles ?? [];
|
|
12135
|
-
if (sheets.length === 0)
|
|
12136
|
-
throw new Error(`Extension "${use}" declares no stylesheets.`);
|
|
12137
|
-
const parts = [];
|
|
12138
|
-
for (const sheet of sheets)
|
|
12139
|
-
parts.push(await readFile28(sheet, "utf8"));
|
|
12140
|
-
return parts.join(`
|
|
12141
|
-
|
|
12142
|
-
`);
|
|
12143
|
-
}
|
|
12144
|
-
|
|
12145
|
-
// src/lib/project-scaffold.ts
|
|
12146
|
-
var STARTER_THEME_FOR_TEMPLATE = {
|
|
12147
|
-
book: "clean-book",
|
|
12148
|
-
zine: "zine",
|
|
12149
|
-
technical: "technical-doc"
|
|
12150
|
-
};
|
|
12151
|
-
function starterOverridesCss(themeId) {
|
|
12152
|
-
return `/**
|
|
12153
|
-
* Your book's own styles.
|
|
12154
|
-
*
|
|
12155
|
-
* Your book's look lives in ${EXTENSIONS_DIR}/${themeId}/theme.css — an
|
|
12156
|
-
* extension listed under \`extensions:\` in manifest.yaml. Add or swap looks
|
|
12157
|
-
* with \`gutterpress ext add\` (or the desktop's Look view) without losing
|
|
12158
|
-
* anything you write here. This file loads AFTER every extension, so any
|
|
12159
|
-
* rule you add below overrides the look's defaults.
|
|
12160
|
-
*
|
|
12161
|
-
* Most everyday adjustments (colors, fonts, margins, spacing) are already
|
|
12162
|
-
* exposed as CSS custom properties on the theme's :root — try those first.
|
|
12163
|
-
*/
|
|
12164
|
-
`;
|
|
12165
|
-
}
|
|
12166
|
-
|
|
12167
|
-
class CreateProjectErrorImpl extends Error {
|
|
12168
|
-
code;
|
|
12169
|
-
constructor(code, message) {
|
|
12170
|
-
super(message);
|
|
12171
|
-
this.name = "CreateProjectError";
|
|
12172
|
-
this.code = code;
|
|
12173
|
-
}
|
|
12174
|
-
}
|
|
12175
|
-
function slugifyProjectName(name) {
|
|
12176
|
-
return slugify(name);
|
|
12177
|
-
}
|
|
12178
|
-
function escapeYamlScalar(value) {
|
|
12179
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
12180
|
-
}
|
|
12181
|
-
var DEFAULT_AUTHOR = "Anonymous";
|
|
12182
|
-
function isPositivePoints(value) {
|
|
12183
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
12184
12120
|
}
|
|
12185
|
-
function
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
}
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
missing.push("page width");
|
|
12194
|
-
if (!customPage || !isPositivePoints(customPage.height))
|
|
12195
|
-
missing.push("page height");
|
|
12196
|
-
if (missing.length > 0) {
|
|
12197
|
-
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).`);
|
|
12198
|
-
}
|
|
12199
|
-
} else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
|
|
12200
|
-
throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
|
|
12201
|
-
}
|
|
12202
|
-
if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
|
|
12203
|
-
throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
|
|
12121
|
+
function platformAllowed(rule, current) {
|
|
12122
|
+
let values;
|
|
12123
|
+
if (typeof rule === "string") {
|
|
12124
|
+
values = [rule];
|
|
12125
|
+
} else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
|
|
12126
|
+
values = rule;
|
|
12127
|
+
} else {
|
|
12128
|
+
return true;
|
|
12204
12129
|
}
|
|
12205
|
-
|
|
12130
|
+
if (values.length === 0)
|
|
12131
|
+
return true;
|
|
12132
|
+
if (values.length === 1 && values[0] === "any")
|
|
12133
|
+
return true;
|
|
12134
|
+
if (values.includes(`!${current}`))
|
|
12135
|
+
return false;
|
|
12136
|
+
const positives = values.filter((value) => !value.startsWith("!"));
|
|
12137
|
+
return positives.length === 0 || positives.includes(current);
|
|
12206
12138
|
}
|
|
12207
|
-
function
|
|
12208
|
-
const
|
|
12209
|
-
|
|
12210
|
-
if (unknown.length > 0) {
|
|
12211
|
-
throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
|
|
12212
|
-
}
|
|
12213
|
-
return [...new Set(ids)];
|
|
12139
|
+
function relativePackagePath(parentPackagePath, name) {
|
|
12140
|
+
const segments = name.split("/");
|
|
12141
|
+
return parentPackagePath ? `${parentPackagePath}/node_modules/${segments.join("/")}` : `node_modules/${segments.join("/")}`;
|
|
12214
12142
|
}
|
|
12215
|
-
async function
|
|
12216
|
-
const
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
if (!parentDir || !path18.isAbsolute(parentDir)) {
|
|
12227
|
-
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
12228
|
-
}
|
|
12229
|
-
try {
|
|
12230
|
-
await access(parentDir, FS.W_OK);
|
|
12231
|
-
} catch {
|
|
12232
|
-
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
12143
|
+
async function installPackage(ctx, name, selector, parentPackagePath, ancestors, depth, optional) {
|
|
12144
|
+
const recordStart = ctx.records.length;
|
|
12145
|
+
const skippedStart = ctx.skipped.length;
|
|
12146
|
+
const warningsBefore = new Set(ctx.warnings);
|
|
12147
|
+
const discardOptionalSubtree = () => {
|
|
12148
|
+
ctx.records.splice(recordStart);
|
|
12149
|
+
ctx.skipped.splice(skippedStart);
|
|
12150
|
+
ctx.warnings = warningsBefore;
|
|
12151
|
+
};
|
|
12152
|
+
if (depth > ctx.limits.dependencyDepth) {
|
|
12153
|
+
throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
|
|
12233
12154
|
}
|
|
12234
|
-
|
|
12235
|
-
let targetExists = true;
|
|
12155
|
+
let selected;
|
|
12236
12156
|
try {
|
|
12237
|
-
await
|
|
12238
|
-
} catch {
|
|
12239
|
-
|
|
12157
|
+
selected = await selectPackage(ctx, name, selector);
|
|
12158
|
+
} catch (error2) {
|
|
12159
|
+
if (optional && isOptionalAvailabilityError(error2)) {
|
|
12160
|
+
discardOptionalSubtree();
|
|
12161
|
+
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
12162
|
+
}
|
|
12163
|
+
throw error2;
|
|
12240
12164
|
}
|
|
12241
|
-
|
|
12242
|
-
|
|
12165
|
+
const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
|
|
12166
|
+
if (ancestor)
|
|
12167
|
+
return { path: ancestor.path };
|
|
12168
|
+
if (ctx.records.length >= ctx.limits.totalPackages) {
|
|
12169
|
+
throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
|
|
12243
12170
|
}
|
|
12244
|
-
const
|
|
12245
|
-
const
|
|
12246
|
-
const
|
|
12247
|
-
const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
|
|
12248
|
-
const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
|
|
12171
|
+
const relativePath = relativePackagePath(parentPackagePath, name);
|
|
12172
|
+
const packageDir = path19.join(ctx.stageRoot, ...relativePath.split("/"));
|
|
12173
|
+
const archivePath = path19.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
|
|
12249
12174
|
try {
|
|
12250
|
-
await
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12175
|
+
await mkdir10(path19.dirname(packageDir), { recursive: true });
|
|
12176
|
+
await downloadTarball(ctx, selected, archivePath);
|
|
12177
|
+
await extractTarball(ctx, archivePath, packageDir, selected.name);
|
|
12178
|
+
const manifest = await readInstalledManifest(packageDir, selected);
|
|
12179
|
+
if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
|
|
12180
|
+
const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
|
|
12181
|
+
if (optional) {
|
|
12182
|
+
await rm3(packageDir, { recursive: true, force: true });
|
|
12183
|
+
return { skipped: reason };
|
|
12257
12184
|
}
|
|
12258
|
-
|
|
12259
|
-
}
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12185
|
+
throw new PackageUnavailableError(reason);
|
|
12186
|
+
}
|
|
12187
|
+
const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
|
|
12188
|
+
const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
|
|
12189
|
+
if (parentPackagePath === null && !importEntry && !requireEntry) {
|
|
12190
|
+
throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
|
|
12191
|
+
}
|
|
12192
|
+
const record = {
|
|
12193
|
+
path: relativePath,
|
|
12194
|
+
name,
|
|
12195
|
+
version: selected.version,
|
|
12196
|
+
tarball: selected.tarball,
|
|
12197
|
+
integrity: selected.integrity.label,
|
|
12198
|
+
legacySha1: selected.integrity.legacySha1,
|
|
12199
|
+
...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
|
|
12200
|
+
...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
|
|
12201
|
+
dependencies: {},
|
|
12202
|
+
packageDir,
|
|
12203
|
+
manifest
|
|
12204
|
+
};
|
|
12205
|
+
ctx.records.push(record);
|
|
12206
|
+
if (selected.integrity.legacySha1) {
|
|
12207
|
+
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.`);
|
|
12267
12208
|
}
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(existsSync13) ?? path18.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
12277
|
-
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
12278
|
-
const substitutions = {
|
|
12279
|
-
"{{TITLE}}": escapeYamlScalar(name),
|
|
12280
|
-
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
12281
|
-
};
|
|
12282
|
-
let openFile = path18.join(projectDir, "chapter-01.md");
|
|
12283
|
-
try {
|
|
12284
|
-
await fillTemplateFile(manifestPath, substitutions);
|
|
12285
|
-
if (!customTemplateDir) {
|
|
12286
|
-
await addBuiltInStyleSet(projectDir, starterThemeId);
|
|
12209
|
+
const dependencies = stringMap2(manifest.dependencies);
|
|
12210
|
+
const optionalDependencies = stringMap2(manifest.optionalDependencies);
|
|
12211
|
+
for (const optionalName of Object.keys(optionalDependencies))
|
|
12212
|
+
delete dependencies[optionalName];
|
|
12213
|
+
const peerDependencies = stringMap2(manifest.peerDependencies);
|
|
12214
|
+
const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
|
|
12215
|
+
for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
|
|
12216
|
+
delete peerDependencies[existing];
|
|
12287
12217
|
}
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
};
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12218
|
+
const nextAncestors = [...ancestors, record];
|
|
12219
|
+
const requiredEntries = [
|
|
12220
|
+
...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
|
|
12221
|
+
...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
|
|
12222
|
+
].sort((a, b) => a.dependency.localeCompare(b.dependency));
|
|
12223
|
+
for (const { dependency, range } of requiredEntries) {
|
|
12224
|
+
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
|
|
12225
|
+
if (!child.path)
|
|
12226
|
+
throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
|
|
12227
|
+
record.dependencies[dependency] = child.path;
|
|
12228
|
+
}
|
|
12229
|
+
const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
|
|
12230
|
+
for (const [dependency, range] of optionalEntries) {
|
|
12231
|
+
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
|
|
12232
|
+
if (child.path)
|
|
12233
|
+
record.dependencies[dependency] = child.path;
|
|
12234
|
+
else if (child.skipped) {
|
|
12235
|
+
ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
|
|
12301
12236
|
}
|
|
12302
|
-
await writeManifestDoc(file, doc);
|
|
12303
12237
|
}
|
|
12304
|
-
const
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
if (requested === "local-git") {
|
|
12315
|
-
try {
|
|
12316
|
-
const { providerFor } = await import("./source-provider-jkppq9fc.js");
|
|
12317
|
-
const provider = providerFor({ type: "local-folder", path: projectDir });
|
|
12318
|
-
await provider.initVersionHistory({
|
|
12319
|
-
projectDir,
|
|
12320
|
-
authorName: options.author?.trim() || undefined,
|
|
12321
|
-
initialMessage: "Created project"
|
|
12322
|
-
});
|
|
12323
|
-
versionHistory = "local-git";
|
|
12324
|
-
} catch (e) {
|
|
12325
|
-
versionHistory = "none";
|
|
12326
|
-
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
12327
|
-
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
12328
|
-
versionHistoryError = enclosingRepoDir ? `This book was added to ${path18.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
|
|
12238
|
+
for (const [dependency, range] of Object.entries(peerDependencies)) {
|
|
12239
|
+
if (object3(peerMeta[dependency])?.optional === true) {
|
|
12240
|
+
ctx.skipped.push({
|
|
12241
|
+
from: relativePath,
|
|
12242
|
+
name: dependency,
|
|
12243
|
+
selector: range,
|
|
12244
|
+
kind: "optional-peer",
|
|
12245
|
+
reason: "Optional peer dependencies are not auto-installed."
|
|
12246
|
+
});
|
|
12247
|
+
}
|
|
12329
12248
|
}
|
|
12249
|
+
return { path: relativePath };
|
|
12250
|
+
} catch (error2) {
|
|
12251
|
+
await rm3(packageDir, { recursive: true, force: true });
|
|
12252
|
+
if (optional && isOptionalAvailabilityError(error2)) {
|
|
12253
|
+
discardOptionalSubtree();
|
|
12254
|
+
return { skipped: error2 instanceof Error ? error2.message : String(error2) };
|
|
12255
|
+
}
|
|
12256
|
+
throw error2;
|
|
12257
|
+
} finally {
|
|
12258
|
+
await rm3(archivePath, { force: true });
|
|
12330
12259
|
}
|
|
12331
|
-
const result = {
|
|
12332
|
-
projectDir,
|
|
12333
|
-
manifestPath,
|
|
12334
|
-
openFile,
|
|
12335
|
-
versionHistory
|
|
12336
|
-
};
|
|
12337
|
-
if (versionHistoryError !== undefined) {
|
|
12338
|
-
result.versionHistoryError = versionHistoryError;
|
|
12339
|
-
}
|
|
12340
|
-
return result;
|
|
12341
|
-
}
|
|
12342
|
-
function prettifyFolderName(base) {
|
|
12343
|
-
const words = base.replace(/[-_]+/g, " ").trim();
|
|
12344
|
-
return words.replace(/\b\w/g, (c) => c.toUpperCase()) || "Untitled Book";
|
|
12345
12260
|
}
|
|
12346
|
-
async function
|
|
12347
|
-
const dir = options.dir;
|
|
12348
|
-
if (typeof dir !== "string" || !path18.isAbsolute(dir)) {
|
|
12349
|
-
throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
|
|
12350
|
-
}
|
|
12351
|
-
let st;
|
|
12261
|
+
async function ensureDirectory(pathname) {
|
|
12352
12262
|
try {
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12263
|
+
const info2 = await lstat2(pathname);
|
|
12264
|
+
if (info2.isSymbolicLink() || !info2.isDirectory()) {
|
|
12265
|
+
throw new Error(`Plugin install path is not a normal directory: ${pathname}`);
|
|
12266
|
+
}
|
|
12267
|
+
} catch (error2) {
|
|
12268
|
+
if (error2.code !== "ENOENT")
|
|
12269
|
+
throw error2;
|
|
12270
|
+
await mkdir10(pathname);
|
|
12359
12271
|
}
|
|
12360
|
-
|
|
12361
|
-
|
|
12272
|
+
}
|
|
12273
|
+
async function prepareVendorParent(projectDir, name) {
|
|
12274
|
+
const plugins = path19.join(projectDir, PLUGINS_DIR);
|
|
12275
|
+
const npm = path19.join(plugins, VENDORED_NPM_DIR);
|
|
12276
|
+
const packageParent = path19.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
|
|
12277
|
+
await mkdir10(projectDir, { recursive: true });
|
|
12278
|
+
await ensureDirectory(plugins);
|
|
12279
|
+
await ensureDirectory(npm);
|
|
12280
|
+
await ensureDirectory(packageParent);
|
|
12281
|
+
const [realNpm, realPackageParent] = await Promise.all([
|
|
12282
|
+
realpath2(npm),
|
|
12283
|
+
realpath2(packageParent)
|
|
12284
|
+
]);
|
|
12285
|
+
if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
|
|
12286
|
+
throw new Error("Plugin install path resolves outside the project.");
|
|
12362
12287
|
}
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12288
|
+
return realNpm;
|
|
12289
|
+
}
|
|
12290
|
+
function publicReceipt(record) {
|
|
12291
|
+
return {
|
|
12292
|
+
path: record.path,
|
|
12293
|
+
name: record.name,
|
|
12294
|
+
version: record.version,
|
|
12295
|
+
tarball: record.tarball,
|
|
12296
|
+
integrity: record.integrity,
|
|
12297
|
+
legacySha1: record.legacySha1,
|
|
12298
|
+
...record.entry ? { entry: record.entry } : {},
|
|
12299
|
+
...record.requireEntry ? { requireEntry: record.requireEntry } : {},
|
|
12300
|
+
dependencies: record.dependencies
|
|
12301
|
+
};
|
|
12302
|
+
}
|
|
12303
|
+
async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
|
|
12304
|
+
const requestedProjectDir = path19.resolve(projectDirInput);
|
|
12305
|
+
await mkdir10(requestedProjectDir, { recursive: true });
|
|
12306
|
+
const projectDir = await realpath2(requestedProjectDir);
|
|
12307
|
+
const requested = parseNpmPluginSpec(packageSpec);
|
|
12308
|
+
const npmRoot = await prepareVendorParent(projectDir, requested.name);
|
|
12309
|
+
const container = await mkdtemp2(path19.join(npmRoot, ".install-"));
|
|
12310
|
+
const stageRoot = path19.join(container, "root");
|
|
12311
|
+
const downloadsDir = path19.join(container, "downloads");
|
|
12312
|
+
await Promise.all([mkdir10(stageRoot), mkdir10(downloadsDir)]);
|
|
12313
|
+
const ctx = {
|
|
12314
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
12315
|
+
signal: options.signal,
|
|
12316
|
+
limits: mergeLimits(options.limits),
|
|
12317
|
+
stageRoot,
|
|
12318
|
+
downloadsDir,
|
|
12319
|
+
packuments: new Map,
|
|
12320
|
+
records: [],
|
|
12321
|
+
skipped: [],
|
|
12322
|
+
warnings: new Set,
|
|
12323
|
+
totalNetworkBytes: 0,
|
|
12324
|
+
totalUnpackedBytes: 0,
|
|
12325
|
+
totalFiles: 0,
|
|
12326
|
+
archiveCounter: 0
|
|
12327
|
+
};
|
|
12328
|
+
let finalRoot = "";
|
|
12329
|
+
let backupRoot = null;
|
|
12367
12330
|
try {
|
|
12368
|
-
const
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
const
|
|
12379
|
-
const
|
|
12380
|
-
|
|
12381
|
-
|
|
12331
|
+
const rootResult = await installPackage(ctx, requested.name, requested.selector, null, [], 0, false);
|
|
12332
|
+
if (!rootResult.path)
|
|
12333
|
+
throw new Error("Root plugin package was unexpectedly skipped.");
|
|
12334
|
+
const rootRecord = ctx.records.find((record) => record.path === rootResult.path);
|
|
12335
|
+
if (!rootRecord)
|
|
12336
|
+
throw new Error("Root plugin package is missing from the dependency graph.");
|
|
12337
|
+
const importEntry = rootRecord.entry ?? rootRecord.requireEntry;
|
|
12338
|
+
if (!importEntry)
|
|
12339
|
+
throw new Error("Root plugin package has no recorded JavaScript entry.");
|
|
12340
|
+
const importFormat = packageEntryFormat(rootRecord.manifest, importEntry);
|
|
12341
|
+
const entry = importFormat === "commonjs" ? rootRecord.requireEntry ?? importEntry : importEntry;
|
|
12342
|
+
const format = packageEntryFormat(rootRecord.manifest, entry);
|
|
12343
|
+
const tree = await computeVendorTreeDigest(stageRoot);
|
|
12344
|
+
const receipt = {
|
|
12345
|
+
schemaVersion: VENDOR_RECEIPT_VERSION,
|
|
12346
|
+
root: {
|
|
12347
|
+
name: rootRecord.name,
|
|
12348
|
+
version: rootRecord.version,
|
|
12349
|
+
packagePath: rootRecord.path,
|
|
12350
|
+
entry,
|
|
12351
|
+
format
|
|
12352
|
+
},
|
|
12353
|
+
packages: ctx.records.map(publicReceipt).sort((a, b) => a.path.localeCompare(b.path)),
|
|
12354
|
+
skipped: ctx.skipped,
|
|
12355
|
+
tree: { algorithm: "sha256", ...tree }
|
|
12356
|
+
};
|
|
12357
|
+
const receiptFile = await open2(path19.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
|
|
12358
|
+
try {
|
|
12359
|
+
await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
|
|
12360
|
+
`, "utf8");
|
|
12361
|
+
await receiptFile.sync();
|
|
12362
|
+
} finally {
|
|
12363
|
+
await receiptFile.close();
|
|
12382
12364
|
}
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
- styles/book.css
|
|
12396
|
-
`;
|
|
12397
|
-
await writeFile8(path18.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
|
|
12398
|
-
await mkdir9(path18.join(dir, "assets"), { recursive: true });
|
|
12399
|
-
if (!hadOwnStylesheet) {
|
|
12400
|
-
await addBuiltInStyleSet(dir, themeId);
|
|
12365
|
+
finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
|
|
12366
|
+
const realFinalParent = await realpath2(path19.dirname(finalRoot));
|
|
12367
|
+
if (!isContained2(projectDir, realFinalParent)) {
|
|
12368
|
+
throw new Error("Plugin install destination resolves outside the project.");
|
|
12369
|
+
}
|
|
12370
|
+
try {
|
|
12371
|
+
await lstat2(finalRoot);
|
|
12372
|
+
backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
|
|
12373
|
+
await rename4(finalRoot, backupRoot);
|
|
12374
|
+
} catch (error2) {
|
|
12375
|
+
if (error2.code !== "ENOENT")
|
|
12376
|
+
throw error2;
|
|
12401
12377
|
}
|
|
12402
|
-
await ensureGitignoreHasDist(dir);
|
|
12403
|
-
} catch (e) {
|
|
12404
|
-
if (e instanceof CreateProjectErrorImpl)
|
|
12405
|
-
throw e;
|
|
12406
|
-
throw new CreateProjectErrorImpl("scaffold-io", `Could not set up the folder: ${e instanceof Error ? e.message : String(e)}`);
|
|
12407
|
-
}
|
|
12408
|
-
const requested = options.versionHistory ?? "local-git";
|
|
12409
|
-
let versionHistory = "none";
|
|
12410
|
-
let versionHistoryError;
|
|
12411
|
-
if (requested === "local-git") {
|
|
12412
12378
|
try {
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
versionHistoryError = e instanceof Error ? e.message : String(e);
|
|
12379
|
+
await rename4(stageRoot, finalRoot);
|
|
12380
|
+
} catch (error2) {
|
|
12381
|
+
if (backupRoot) {
|
|
12382
|
+
try {
|
|
12383
|
+
await rename4(backupRoot, finalRoot);
|
|
12384
|
+
} catch (restoreError) {
|
|
12385
|
+
throw new Error(`Could not publish the new plugin tree or restore the previous tree: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`, { cause: error2 });
|
|
12386
|
+
}
|
|
12387
|
+
}
|
|
12388
|
+
throw error2;
|
|
12424
12389
|
}
|
|
12390
|
+
return {
|
|
12391
|
+
name: rootRecord.name,
|
|
12392
|
+
version: rootRecord.version,
|
|
12393
|
+
installRoot: finalRoot,
|
|
12394
|
+
backupRoot,
|
|
12395
|
+
warnings: [...ctx.warnings]
|
|
12396
|
+
};
|
|
12397
|
+
} finally {
|
|
12398
|
+
await rm3(container, { recursive: true, force: true }).catch(() => {});
|
|
12425
12399
|
}
|
|
12426
|
-
const result = {
|
|
12427
|
-
projectDir: dir,
|
|
12428
|
-
manifestPath: path18.join(dir, MANIFEST_FILENAMES[0]),
|
|
12429
|
-
openFile: path18.join(dir, mdFiles[0]),
|
|
12430
|
-
versionHistory
|
|
12431
|
-
};
|
|
12432
|
-
if (versionHistoryError !== undefined)
|
|
12433
|
-
result.versionHistoryError = versionHistoryError;
|
|
12434
|
-
return result;
|
|
12435
12400
|
}
|
|
12436
|
-
async function
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
try {
|
|
12440
|
-
existing = await readFile29(gitignorePath, "utf8");
|
|
12441
|
-
} catch {}
|
|
12442
|
-
if (existing === null) {
|
|
12443
|
-
await writeFile8(gitignorePath, `dist/
|
|
12444
|
-
`, "utf8");
|
|
12445
|
-
return;
|
|
12401
|
+
async function finalizeNpmPluginInstall(installed) {
|
|
12402
|
+
if (installed.backupRoot) {
|
|
12403
|
+
await rm3(installed.backupRoot, { recursive: true, force: true });
|
|
12446
12404
|
}
|
|
12447
|
-
const alreadyIgnoresDist = existing.split(`
|
|
12448
|
-
`).some((line) => /^\/?dist\/?$/.test(line.trim()));
|
|
12449
|
-
if (alreadyIgnoresDist)
|
|
12450
|
-
return;
|
|
12451
|
-
const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
|
|
12452
|
-
`);
|
|
12453
|
-
await writeFile8(gitignorePath, existing + (needsLeadingNewline ? `
|
|
12454
|
-
` : "") + `dist/
|
|
12455
|
-
`, "utf8");
|
|
12456
12405
|
}
|
|
12457
|
-
async function
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12406
|
+
async function rollbackNpmPluginInstall(installed) {
|
|
12407
|
+
await rm3(installed.installRoot, { recursive: true, force: true });
|
|
12408
|
+
if (installed.backupRoot)
|
|
12409
|
+
await rename4(installed.backupRoot, installed.installRoot);
|
|
12410
|
+
}
|
|
12411
|
+
|
|
12412
|
+
// src/lib/extension-manager.ts
|
|
12413
|
+
var EXTENSIONS_DIR = "extensions";
|
|
12414
|
+
var RECOMMENDED_EXTENSIONS = [
|
|
12415
|
+
{
|
|
12416
|
+
use: "markdown-it-mark",
|
|
12417
|
+
label: "Highlight",
|
|
12418
|
+
description: "Highlighted text with `==marked==` -> `<mark>`."
|
|
12419
|
+
},
|
|
12420
|
+
{
|
|
12421
|
+
use: "markdown-it-sub",
|
|
12422
|
+
label: "Subscript",
|
|
12423
|
+
description: "Subscript text with `H~2~O`."
|
|
12424
|
+
},
|
|
12425
|
+
{
|
|
12426
|
+
use: "markdown-it-sup",
|
|
12427
|
+
label: "Superscript",
|
|
12428
|
+
description: "Superscript text with `29^th^`."
|
|
12429
|
+
},
|
|
12430
|
+
{
|
|
12431
|
+
use: "markdown-it-abbr",
|
|
12432
|
+
label: "Abbreviations",
|
|
12433
|
+
description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
|
|
12434
|
+
},
|
|
12435
|
+
{
|
|
12436
|
+
use: "gutterpress-gfm-alerts",
|
|
12437
|
+
label: "Callouts",
|
|
12438
|
+
description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
|
|
12463
12439
|
}
|
|
12464
|
-
|
|
12465
|
-
|
|
12440
|
+
];
|
|
12441
|
+
var BUILT_IN_STYLE_SET_IDS = ["clean-book", "zine", "technical-doc"];
|
|
12442
|
+
var mutationQueues = new Map;
|
|
12443
|
+
function withMutationLock(projectDir, mutation) {
|
|
12444
|
+
const resolved = path20.resolve(projectDir);
|
|
12445
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
12446
|
+
const previous = mutationQueues.get(key) ?? Promise.resolve();
|
|
12447
|
+
const run2 = previous.then(mutation, mutation);
|
|
12448
|
+
const tail = run2.then(() => {
|
|
12466
12449
|
return;
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12450
|
+
}, () => {
|
|
12451
|
+
return;
|
|
12452
|
+
});
|
|
12453
|
+
mutationQueues.set(key, tail);
|
|
12454
|
+
tail.then(() => {
|
|
12455
|
+
if (mutationQueues.get(key) === tail)
|
|
12456
|
+
mutationQueues.delete(key);
|
|
12457
|
+
});
|
|
12458
|
+
return run2;
|
|
12470
12459
|
}
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12460
|
+
function entryUse(item) {
|
|
12461
|
+
if (isScalar2(item))
|
|
12462
|
+
return typeof item.value === "string" ? item.value.trim() : null;
|
|
12463
|
+
if (isMap2(item)) {
|
|
12464
|
+
const u = item.get("use");
|
|
12465
|
+
return typeof u === "string" ? u.trim() : null;
|
|
12475
12466
|
}
|
|
12476
|
-
|
|
12467
|
+
return null;
|
|
12477
12468
|
}
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
["snippets/term-box.md", "snippets/term-box.md"],
|
|
12491
|
-
["test/fixture.md", "test/fixture.md"],
|
|
12492
|
-
["test/expected.html", "test/expected.html"],
|
|
12493
|
-
["test/plugin.test.js.tpl", "test/plugin.test.js"]
|
|
12494
|
-
],
|
|
12495
|
-
theme: [
|
|
12496
|
-
["package.json", "package.json"],
|
|
12497
|
-
["README.md", "README.md"],
|
|
12498
|
-
["components.yaml", "components.yaml"],
|
|
12499
|
-
["snippets/callout.md", "snippets/callout.md"],
|
|
12500
|
-
["styles/tokens.css", "styles/tokens.css"],
|
|
12501
|
-
["styles/base.css", "styles/base.css"],
|
|
12502
|
-
["styles/components.css", "styles/components.css"],
|
|
12503
|
-
["styles/page-templates.css", "styles/page-templates.css"],
|
|
12504
|
-
["styles/page-rules.css", "styles/page-rules.css"],
|
|
12505
|
-
["styles/book.css", "styles/book.css"]
|
|
12506
|
-
]
|
|
12507
|
-
};
|
|
12508
|
-
var OPEN_FILE = {
|
|
12509
|
-
plugin: "plugin.js",
|
|
12510
|
-
theme: "styles/tokens.css"
|
|
12511
|
-
};
|
|
12512
|
-
var DEFAULT_DESCRIPTION = {
|
|
12513
|
-
plugin: "A Gutterpress plugin.",
|
|
12514
|
-
theme: "A Gutterpress theme."
|
|
12515
|
-
};
|
|
12516
|
-
var DEFAULT_AUTHOR2 = "Anonymous";
|
|
12517
|
-
|
|
12518
|
-
class ExtensionScaffoldError extends Error {
|
|
12519
|
-
code;
|
|
12520
|
-
constructor(code, message) {
|
|
12521
|
-
super(message);
|
|
12522
|
-
this.name = "CreateProjectError";
|
|
12523
|
-
this.code = code;
|
|
12524
|
-
}
|
|
12469
|
+
function rawEntry(item) {
|
|
12470
|
+
const use = entryUse(item);
|
|
12471
|
+
if (!use)
|
|
12472
|
+
return null;
|
|
12473
|
+
if (!isMap2(item))
|
|
12474
|
+
return { use, enabled: true };
|
|
12475
|
+
const x = item.get("export");
|
|
12476
|
+
return {
|
|
12477
|
+
use,
|
|
12478
|
+
...typeof x === "string" && x.trim() ? { export: x.trim() } : {},
|
|
12479
|
+
enabled: item.get("enabled") !== false
|
|
12480
|
+
};
|
|
12525
12481
|
}
|
|
12526
|
-
function
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12482
|
+
function sameExtension(a, b) {
|
|
12483
|
+
if (a.trim() === b.trim())
|
|
12484
|
+
return true;
|
|
12485
|
+
let pa;
|
|
12486
|
+
let pb;
|
|
12487
|
+
try {
|
|
12488
|
+
pa = parseExtensionSpecifier(a);
|
|
12489
|
+
pb = parseExtensionSpecifier(b);
|
|
12490
|
+
} catch {
|
|
12491
|
+
return false;
|
|
12534
12492
|
}
|
|
12535
|
-
return
|
|
12536
|
-
}
|
|
12537
|
-
function identifierFor(slug) {
|
|
12538
|
-
const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
12539
|
-
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
12493
|
+
return pa.kind === "npm" && pb.kind === "npm" && pa.name === pb.name;
|
|
12540
12494
|
}
|
|
12541
|
-
function
|
|
12542
|
-
|
|
12495
|
+
function indexOfUse(seq, use) {
|
|
12496
|
+
const items = seq.items;
|
|
12497
|
+
const exact = items.findIndex((item) => entryUse(item) === use.trim());
|
|
12498
|
+
if (exact >= 0)
|
|
12499
|
+
return exact;
|
|
12500
|
+
return items.findIndex((item) => {
|
|
12501
|
+
const u = entryUse(item);
|
|
12502
|
+
return !!u && sameExtension(u, use);
|
|
12503
|
+
});
|
|
12543
12504
|
}
|
|
12544
|
-
|
|
12545
|
-
const
|
|
12546
|
-
if (
|
|
12547
|
-
throw new
|
|
12548
|
-
}
|
|
12549
|
-
const name = (options.name ?? "").trim();
|
|
12550
|
-
if (!name) {
|
|
12551
|
-
throw new ExtensionScaffoldError("invalid-name", "A name is required.");
|
|
12552
|
-
}
|
|
12553
|
-
const slug = (options.folderName ?? slugify(name)).trim();
|
|
12554
|
-
if (!slug || slug !== slugify(slug)) {
|
|
12555
|
-
throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
12556
|
-
}
|
|
12557
|
-
const prefix = resolveExtensionPrefix(slug, options.prefix);
|
|
12558
|
-
const parentDir = options.parentDir;
|
|
12559
|
-
if (!parentDir || !path19.isAbsolute(parentDir)) {
|
|
12560
|
-
throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
|
|
12505
|
+
function manifestPathFor(projectDir, abs) {
|
|
12506
|
+
const rel = path20.relative(path20.resolve(projectDir), abs).split(path20.sep).join("/");
|
|
12507
|
+
if (rel === "") {
|
|
12508
|
+
throw new Error("An extension cannot be the project folder itself.");
|
|
12561
12509
|
}
|
|
12510
|
+
if (isPathSpecifier(rel))
|
|
12511
|
+
return rel;
|
|
12512
|
+
return `./${rel}`;
|
|
12513
|
+
}
|
|
12514
|
+
var NO_CARRIES = {
|
|
12515
|
+
markdown: false,
|
|
12516
|
+
styles: false,
|
|
12517
|
+
snippets: false,
|
|
12518
|
+
components: false
|
|
12519
|
+
};
|
|
12520
|
+
async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
|
|
12521
|
+
const meta = await readExtensionMeta(dir);
|
|
12522
|
+
const styles = extensionStyleList(meta);
|
|
12523
|
+
return {
|
|
12524
|
+
...base,
|
|
12525
|
+
label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
|
|
12526
|
+
...meta.description?.trim() ? { description: meta.description.trim() } : {},
|
|
12527
|
+
...meta.author?.trim() ? { author: meta.author.trim() } : {},
|
|
12528
|
+
...meta.preview !== undefined ? { preview: meta.preview } : {},
|
|
12529
|
+
...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
|
|
12530
|
+
...styles.length > 0 ? { styles } : {},
|
|
12531
|
+
carries: extensionCarries(meta),
|
|
12532
|
+
dir
|
|
12533
|
+
};
|
|
12534
|
+
}
|
|
12535
|
+
async function describeEntry(projectDir, raw) {
|
|
12536
|
+
const warnings = [];
|
|
12537
|
+
let parsed = null;
|
|
12562
12538
|
try {
|
|
12563
|
-
|
|
12564
|
-
} catch {
|
|
12565
|
-
|
|
12539
|
+
parsed = parseExtensionSpecifier(raw.use);
|
|
12540
|
+
} catch (error2) {
|
|
12541
|
+
warnings.push(error2 instanceof Error ? error2.message : String(error2));
|
|
12566
12542
|
}
|
|
12567
|
-
const
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
}
|
|
12572
|
-
|
|
12543
|
+
const common = {
|
|
12544
|
+
use: raw.use,
|
|
12545
|
+
enabled: raw.enabled,
|
|
12546
|
+
...raw.export ? { export: raw.export } : {}
|
|
12547
|
+
};
|
|
12548
|
+
const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
12549
|
+
if (!parsed) {
|
|
12550
|
+
return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
|
|
12573
12551
|
}
|
|
12574
|
-
if (
|
|
12575
|
-
|
|
12552
|
+
if (parsed.kind === "bundled") {
|
|
12553
|
+
const bundledName = parsed.name;
|
|
12554
|
+
const rec = RECOMMENDED_EXTENSIONS.find((r) => r.use === bundledName);
|
|
12555
|
+
return withWarnings({
|
|
12556
|
+
...common,
|
|
12557
|
+
kind: "bundled",
|
|
12558
|
+
name: bundledName,
|
|
12559
|
+
label: rec?.label ?? bundledName,
|
|
12560
|
+
...rec ? { description: rec.description } : {},
|
|
12561
|
+
carries: { ...NO_CARRIES, markdown: true }
|
|
12562
|
+
});
|
|
12576
12563
|
}
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
};
|
|
12587
|
-
const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
|
|
12588
|
-
const entries = TEMPLATE_FILES[kind];
|
|
12589
|
-
const written = [];
|
|
12590
|
-
try {
|
|
12591
|
-
await mkdir10(extensionDir, { recursive: true });
|
|
12592
|
-
for (const [assetName, relDest] of entries) {
|
|
12593
|
-
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
12594
|
-
const dest = path19.join(extensionDir, relDest);
|
|
12595
|
-
await mkdir10(path19.dirname(dest), { recursive: true });
|
|
12596
|
-
const text = await readFile30(src, "utf8");
|
|
12597
|
-
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
12598
|
-
await writeFile9(dest, substitute(text, table), "utf8");
|
|
12599
|
-
written.push(relDest);
|
|
12564
|
+
if (parsed.kind === "path") {
|
|
12565
|
+
const abs = path20.resolve(projectDir, parsed.path);
|
|
12566
|
+
const base2 = { ...common, kind: "path", name: parsed.path };
|
|
12567
|
+
let info2;
|
|
12568
|
+
try {
|
|
12569
|
+
info2 = await stat7(abs);
|
|
12570
|
+
} catch {
|
|
12571
|
+
warnings.push(`Not found: ${abs}`);
|
|
12572
|
+
return withWarnings({ ...base2, label: path20.basename(abs), carries: NO_CARRIES });
|
|
12600
12573
|
}
|
|
12601
|
-
|
|
12602
|
-
|
|
12574
|
+
if (info2.isDirectory()) {
|
|
12575
|
+
return withWarnings(await describeFolder(abs, base2, prettify(path20.basename(abs))));
|
|
12576
|
+
}
|
|
12577
|
+
return withWarnings({
|
|
12578
|
+
...base2,
|
|
12579
|
+
label: path20.basename(abs),
|
|
12580
|
+
carries: { ...NO_CARRIES, markdown: true }
|
|
12581
|
+
});
|
|
12603
12582
|
}
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
prefix,
|
|
12610
|
-
openFile: path19.join(extensionDir, OPEN_FILE[kind]),
|
|
12611
|
-
files: written
|
|
12583
|
+
const base = {
|
|
12584
|
+
...common,
|
|
12585
|
+
kind: "npm",
|
|
12586
|
+
name: parsed.name,
|
|
12587
|
+
...parsed.version ? { version: parsed.version } : {}
|
|
12612
12588
|
};
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
for (const [token, value] of Object.entries(table)) {
|
|
12617
|
-
out = out.split(token).join(value);
|
|
12589
|
+
if (!parsed.version) {
|
|
12590
|
+
warnings.push(`Not pinned — run \`gutterpress ext add ${parsed.name}\` to install it and pin an exact version.`);
|
|
12591
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
12618
12592
|
}
|
|
12619
|
-
|
|
12620
|
-
}
|
|
12621
|
-
// src/lib/project-templates.ts
|
|
12622
|
-
import { cp as cp4, mkdir as mkdir11, readFile as readFile31, readdir as readdir7, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
|
|
12623
|
-
import { constants as FS3, existsSync as existsSync14 } from "node:fs";
|
|
12624
|
-
import { access as access3 } from "node:fs/promises";
|
|
12625
|
-
import path20 from "node:path";
|
|
12626
|
-
import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
|
|
12627
|
-
var BUILT_IN_TEMPLATE_IDS = [
|
|
12628
|
-
"book",
|
|
12629
|
-
"zine",
|
|
12630
|
-
"technical"
|
|
12631
|
-
];
|
|
12632
|
-
async function readTemplateChoices(manifestPath) {
|
|
12593
|
+
let installRoot = null;
|
|
12633
12594
|
try {
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
const preset = doc.get("preset");
|
|
12638
|
-
if (typeof preset === "string" && preset)
|
|
12639
|
-
out.preset = preset;
|
|
12640
|
-
const targets = doc.get("targets", true);
|
|
12641
|
-
if (isSeq3(targets)) {
|
|
12642
|
-
out.targets = targets.items.map((i) => scalarString(i)).filter((s) => s !== null);
|
|
12643
|
-
}
|
|
12644
|
-
return out;
|
|
12645
|
-
} catch {
|
|
12646
|
-
return {};
|
|
12595
|
+
installRoot = await resolveVendoredPluginInstallRoot(projectDir, parsed.name, parsed.version);
|
|
12596
|
+
} catch (error2) {
|
|
12597
|
+
warnings.push(error2 instanceof Error ? error2.message : String(error2));
|
|
12647
12598
|
}
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
label: "Book",
|
|
12652
|
-
description: "A clean starting point for a novel, memoir, or any long-form book."
|
|
12653
|
-
},
|
|
12654
|
-
zine: {
|
|
12655
|
-
label: "Zine",
|
|
12656
|
-
description: "A short, personal zine made to be printed, folded, and shared."
|
|
12657
|
-
},
|
|
12658
|
-
technical: {
|
|
12659
|
-
label: "Technical document",
|
|
12660
|
-
description: "A manual or guide with steps, reference tables, and code examples."
|
|
12599
|
+
if (!installRoot) {
|
|
12600
|
+
warnings.push(`Not installed — run \`gutterpress ext add ${raw.use}\`.`);
|
|
12601
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
12661
12602
|
}
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
...
|
|
12670
|
-
})
|
|
12603
|
+
const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
|
|
12604
|
+
if (!existsSync14(path20.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
|
|
12605
|
+
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
12606
|
+
}
|
|
12607
|
+
const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
|
|
12608
|
+
return withWarnings({
|
|
12609
|
+
...described,
|
|
12610
|
+
carries: { ...described.carries, markdown: true }
|
|
12611
|
+
});
|
|
12671
12612
|
}
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
"
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
"build",
|
|
12678
|
-
".gutterpress",
|
|
12679
|
-
".DS_Store"
|
|
12680
|
-
]);
|
|
12681
|
-
var VENDOR_DIR = "shared";
|
|
12682
|
-
function collectEscaping(nodes, getValue, escapes) {
|
|
12613
|
+
async function listProjectExtensions(projectDir) {
|
|
12614
|
+
const { doc } = await loadManifestDoc(projectDir);
|
|
12615
|
+
const seq = doc.get("extensions", true);
|
|
12616
|
+
if (!isSeq3(seq))
|
|
12617
|
+
return [];
|
|
12683
12618
|
const out = [];
|
|
12684
|
-
for (const
|
|
12685
|
-
const
|
|
12686
|
-
if (
|
|
12687
|
-
|
|
12619
|
+
for (const item of seq.items) {
|
|
12620
|
+
const raw = rawEntry(item);
|
|
12621
|
+
if (!raw)
|
|
12622
|
+
continue;
|
|
12623
|
+
out.push(await describeEntry(projectDir, raw));
|
|
12688
12624
|
}
|
|
12689
12625
|
return out;
|
|
12690
12626
|
}
|
|
12691
|
-
function
|
|
12692
|
-
|
|
12693
|
-
return use !== null && isPathSpecifier(use) ? use : null;
|
|
12694
|
-
}
|
|
12695
|
-
function dropEscaping(doc, seq, key, escaping) {
|
|
12696
|
-
if (!isSeq3(seq))
|
|
12697
|
-
return;
|
|
12698
|
-
const drop = new Set(escaping.map((e) => e.node));
|
|
12699
|
-
seq.items = seq.items.filter((node) => !drop.has(node));
|
|
12700
|
-
if (seq.items.length === 0)
|
|
12701
|
-
doc.delete(key);
|
|
12627
|
+
async function describeExtension(projectDir, use) {
|
|
12628
|
+
return (await listProjectExtensions(projectDir)).find((e) => sameExtension(e.use, use)) ?? null;
|
|
12702
12629
|
}
|
|
12703
|
-
function
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12630
|
+
function extensionConfigFor(entry) {
|
|
12631
|
+
return {
|
|
12632
|
+
use: entry.use,
|
|
12633
|
+
...entry.kind === "path" ? { path: entry.name } : { name: entry.name, ...entry.version ? { version: entry.version } : {} },
|
|
12634
|
+
...entry.export ? { export: entry.export } : {},
|
|
12635
|
+
options: {}
|
|
12636
|
+
};
|
|
12708
12637
|
}
|
|
12709
|
-
function
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12638
|
+
async function setExtensionEnabled(projectDir, use, enabled) {
|
|
12639
|
+
return withMutationLock(projectDir, async () => {
|
|
12640
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
12641
|
+
const seq = doc.get("extensions", true);
|
|
12642
|
+
const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
|
|
12643
|
+
if (!isSeq3(seq) || idx < 0)
|
|
12644
|
+
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
12645
|
+
const item = seq.items[idx];
|
|
12646
|
+
if (enabled) {
|
|
12647
|
+
if (isMap2(item)) {
|
|
12648
|
+
item.delete("enabled");
|
|
12649
|
+
if (item.items.length === 1 && typeof item.get("use") === "string") {
|
|
12650
|
+
seq.items[idx] = new Scalar(item.get("use"));
|
|
12651
|
+
}
|
|
12652
|
+
}
|
|
12653
|
+
} else if (isMap2(item)) {
|
|
12654
|
+
item.set("enabled", false);
|
|
12655
|
+
} else {
|
|
12656
|
+
const map = new YAMLMap(doc.schema);
|
|
12657
|
+
map.set("use", entryUse(item));
|
|
12658
|
+
map.set("enabled", false);
|
|
12659
|
+
seq.items[idx] = map;
|
|
12660
|
+
}
|
|
12661
|
+
await writeManifestDoc(file, doc);
|
|
12662
|
+
});
|
|
12721
12663
|
}
|
|
12722
|
-
function
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12664
|
+
async function reorderExtensions(projectDir, order) {
|
|
12665
|
+
return withMutationLock(projectDir, async () => {
|
|
12666
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
12667
|
+
const seq = doc.get("extensions", true);
|
|
12668
|
+
if (!isSeq3(seq))
|
|
12669
|
+
throw new Error(`No extensions are configured in ${path20.basename(file)}.`);
|
|
12670
|
+
const items = seq.items;
|
|
12671
|
+
const current = items.map((item) => entryUse(item));
|
|
12672
|
+
const wanted = order.map((u) => u.trim());
|
|
12673
|
+
const sameSet = current.length === wanted.length && [...current].sort().join(`
|
|
12674
|
+
`) === [...wanted].sort().join(`
|
|
12675
|
+
`);
|
|
12676
|
+
if (!sameSet) {
|
|
12677
|
+
throw new Error("The new order must list every configured extension exactly once — reload the list and try again.");
|
|
12678
|
+
}
|
|
12679
|
+
const remaining = [...items];
|
|
12680
|
+
seq.items = wanted.map((u) => {
|
|
12681
|
+
const i = remaining.findIndex((item) => entryUse(item) === u);
|
|
12682
|
+
return remaining.splice(i, 1)[0];
|
|
12683
|
+
});
|
|
12684
|
+
await writeManifestDoc(file, doc);
|
|
12685
|
+
});
|
|
12731
12686
|
}
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
if (mode === "exclude") {
|
|
12744
|
-
const excludedRefs = [...escapingStyles, ...escapingExtensions].map((e) => e.value);
|
|
12745
|
-
dropEscaping(doc, stylesSeq, "styles", escapingStyles);
|
|
12746
|
-
dropEscaping(doc, extensionsSeq, "extensions", escapingExtensions);
|
|
12687
|
+
async function removeExtension(projectDir, use) {
|
|
12688
|
+
return withMutationLock(projectDir, async () => {
|
|
12689
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
12690
|
+
const seq = doc.get("extensions", true);
|
|
12691
|
+
const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
|
|
12692
|
+
if (!isSeq3(seq) || idx < 0)
|
|
12693
|
+
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
12694
|
+
const written = entryUse(seq.items[idx]);
|
|
12695
|
+
seq.items.splice(idx, 1);
|
|
12696
|
+
if (seq.items.length === 0)
|
|
12697
|
+
doc.delete("extensions");
|
|
12747
12698
|
await writeManifestDoc(file, doc);
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
const
|
|
12769
|
-
|
|
12770
|
-
|
|
12699
|
+
let parsed = null;
|
|
12700
|
+
try {
|
|
12701
|
+
parsed = parseExtensionSpecifier(written);
|
|
12702
|
+
} catch {
|
|
12703
|
+
return;
|
|
12704
|
+
}
|
|
12705
|
+
if (parsed.kind === "npm" && parsed.version) {
|
|
12706
|
+
await rm4(vendoredNpmPluginRoot(projectDir, parsed.name, parsed.version), {
|
|
12707
|
+
recursive: true,
|
|
12708
|
+
force: true
|
|
12709
|
+
});
|
|
12710
|
+
clearVendoredPluginResolver(projectDir, parsed.name, parsed.version);
|
|
12711
|
+
}
|
|
12712
|
+
});
|
|
12713
|
+
}
|
|
12714
|
+
async function upsertEntry(projectDir, use, exportName) {
|
|
12715
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
12716
|
+
const seq = ensureSeq(doc, "extensions");
|
|
12717
|
+
const idx = indexOfUse(seq, use);
|
|
12718
|
+
if (idx >= 0) {
|
|
12719
|
+
const item = seq.items[idx];
|
|
12720
|
+
if (isMap2(item)) {
|
|
12721
|
+
item.set("use", use);
|
|
12722
|
+
if (exportName)
|
|
12723
|
+
item.set("export", exportName);
|
|
12724
|
+
else
|
|
12725
|
+
item.delete("export");
|
|
12726
|
+
if (item.items.length === 1)
|
|
12727
|
+
seq.items[idx] = new Scalar(use);
|
|
12728
|
+
} else if (exportName) {
|
|
12729
|
+
const map = new YAMLMap(doc.schema);
|
|
12730
|
+
map.set("use", use);
|
|
12731
|
+
map.set("export", exportName);
|
|
12732
|
+
seq.items[idx] = map;
|
|
12733
|
+
} else {
|
|
12734
|
+
seq.items[idx] = new Scalar(use);
|
|
12735
|
+
}
|
|
12736
|
+
} else if (exportName) {
|
|
12737
|
+
const map = new YAMLMap(doc.schema);
|
|
12738
|
+
map.set("use", use);
|
|
12739
|
+
map.set("export", exportName);
|
|
12740
|
+
seq.add(map);
|
|
12741
|
+
} else {
|
|
12742
|
+
seq.add(new Scalar(use));
|
|
12771
12743
|
}
|
|
12772
12744
|
await writeManifestDoc(file, doc);
|
|
12773
|
-
return { vendoredRefs, excludedRefs: [] };
|
|
12774
12745
|
}
|
|
12775
|
-
async function
|
|
12776
|
-
const { projectDir, name, templatesRoot } = options;
|
|
12777
|
-
const id = slugify(name);
|
|
12778
|
-
if (!id)
|
|
12779
|
-
throw new Error(`Could not derive a template id from "${name}".`);
|
|
12780
|
-
const dir = path20.join(templatesRoot, id);
|
|
12781
|
-
let exists = true;
|
|
12746
|
+
async function addPathExtension(projectDir, abs, exportName) {
|
|
12782
12747
|
try {
|
|
12783
|
-
await
|
|
12748
|
+
await stat7(abs);
|
|
12784
12749
|
} catch {
|
|
12785
|
-
|
|
12750
|
+
throw new Error(`Extension not found: ${abs}`);
|
|
12786
12751
|
}
|
|
12787
|
-
|
|
12788
|
-
|
|
12752
|
+
const use = manifestPathFor(projectDir, abs);
|
|
12753
|
+
await loadPlugin(extensionConfigFor({ use, kind: "path", name: use, export: exportName }), projectDir);
|
|
12754
|
+
await upsertEntry(projectDir, use, exportName);
|
|
12755
|
+
return await describeExtension(projectDir, use);
|
|
12756
|
+
}
|
|
12757
|
+
async function addExtension(projectDir, specifier, options = {}) {
|
|
12758
|
+
return withMutationLock(projectDir, () => addExtensionUnlocked(projectDir, specifier, options));
|
|
12759
|
+
}
|
|
12760
|
+
async function addExtensionUnlocked(projectDir, specifier, options) {
|
|
12761
|
+
const input = specifier.trim();
|
|
12762
|
+
const exportName = options.exportName?.trim() || undefined;
|
|
12763
|
+
if (path20.isAbsolute(input))
|
|
12764
|
+
return addPathExtension(projectDir, input, exportName);
|
|
12765
|
+
const parsed = parseExtensionSpecifier(input);
|
|
12766
|
+
if (parsed.kind === "path") {
|
|
12767
|
+
return addPathExtension(projectDir, path20.resolve(projectDir, parsed.path), exportName);
|
|
12789
12768
|
}
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
if (entry.name === TEMPLATE_META_FILE)
|
|
12796
|
-
continue;
|
|
12797
|
-
await cp4(path20.join(projectDir, entry.name), path20.join(dir, entry.name), { recursive: true });
|
|
12769
|
+
if (parsed.kind === "bundled") {
|
|
12770
|
+
if (exportName)
|
|
12771
|
+
throw new Error(`Bundled extension "${parsed.name}" does not take a named export.`);
|
|
12772
|
+
await upsertEntry(projectDir, parsed.name);
|
|
12773
|
+
return await describeExtension(projectDir, parsed.name);
|
|
12798
12774
|
}
|
|
12799
|
-
const
|
|
12800
|
-
const
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
await writeFile10(path20.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
|
|
12804
|
-
return {
|
|
12805
|
-
id,
|
|
12806
|
-
label: name,
|
|
12807
|
-
description: "Your saved template.",
|
|
12808
|
-
kind: "custom",
|
|
12809
|
-
dir,
|
|
12810
|
-
...shared
|
|
12811
|
-
};
|
|
12812
|
-
}
|
|
12813
|
-
async function retokeniseManifest(manifestPath) {
|
|
12814
|
-
let text;
|
|
12775
|
+
const existing = await describeExtension(projectDir, parsed.name);
|
|
12776
|
+
const chosenExport = exportName ?? existing?.export;
|
|
12777
|
+
const installed = await installNpmPlugin(projectDir, input, options);
|
|
12778
|
+
const use = pinnedNpmSpecifier(installed.name, installed.version);
|
|
12815
12779
|
try {
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12780
|
+
await loadPlugin(extensionConfigFor({
|
|
12781
|
+
use,
|
|
12782
|
+
kind: "npm",
|
|
12783
|
+
name: installed.name,
|
|
12784
|
+
version: installed.version,
|
|
12785
|
+
export: chosenExport
|
|
12786
|
+
}), projectDir);
|
|
12787
|
+
await options.__testFailBeforeManifestCommit?.();
|
|
12788
|
+
await upsertEntry(projectDir, use, chosenExport);
|
|
12789
|
+
} catch (cause) {
|
|
12790
|
+
try {
|
|
12791
|
+
await rollbackNpmPluginInstall(installed);
|
|
12792
|
+
clearVendoredPluginResolver(projectDir, installed.name, installed.version);
|
|
12793
|
+
} catch (rollbackError) {
|
|
12794
|
+
throw new Error(`Extension install failed and its previous vendor tree could not be restored: ` + `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause });
|
|
12795
|
+
}
|
|
12796
|
+
throw new Error(`Downloaded ${use}, but it is not a loadable markdown-it plugin: ` + `${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
12819
12797
|
}
|
|
12820
|
-
|
|
12821
|
-
await
|
|
12798
|
+
const warnings = [...installed.warnings];
|
|
12799
|
+
await finalizeNpmPluginInstall(installed).catch((error2) => {
|
|
12800
|
+
warnings.push(`The extension was installed, but an old backup could not be removed: ` + `${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
12801
|
+
});
|
|
12802
|
+
const entry = await describeExtension(projectDir, use);
|
|
12803
|
+
return warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
|
|
12822
12804
|
}
|
|
12823
|
-
async function
|
|
12824
|
-
|
|
12825
|
-
try {
|
|
12826
|
-
entries = await readdir7(templatesRoot, { withFileTypes: true });
|
|
12827
|
-
} catch {
|
|
12828
|
-
return [];
|
|
12829
|
-
}
|
|
12805
|
+
async function listBuiltInStyleSets() {
|
|
12806
|
+
const assets = await getAssetsDir();
|
|
12830
12807
|
const out = [];
|
|
12831
|
-
for (const
|
|
12832
|
-
|
|
12833
|
-
continue;
|
|
12834
|
-
const dir = path20.join(templatesRoot, entry.name);
|
|
12835
|
-
let label = prettify(entry.name);
|
|
12836
|
-
try {
|
|
12837
|
-
const meta = JSON.parse(await readFile31(path20.join(dir, TEMPLATE_META_FILE), "utf8"));
|
|
12838
|
-
if (typeof meta.label === "string" && meta.label)
|
|
12839
|
-
label = meta.label;
|
|
12840
|
-
} catch {}
|
|
12808
|
+
for (const id of BUILT_IN_STYLE_SET_IDS) {
|
|
12809
|
+
const meta = await readExtensionMeta(path20.join(assets, "themes", id));
|
|
12841
12810
|
out.push({
|
|
12842
|
-
id
|
|
12843
|
-
|
|
12844
|
-
description:
|
|
12845
|
-
kind: "custom",
|
|
12846
|
-
dir,
|
|
12847
|
-
...await readTemplateChoices(path20.join(dir, MANIFEST_FILENAMES[0]))
|
|
12811
|
+
id,
|
|
12812
|
+
name: prettify(meta.name?.trim() || id),
|
|
12813
|
+
description: meta.description?.trim() || ""
|
|
12848
12814
|
});
|
|
12849
12815
|
}
|
|
12850
|
-
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
12851
12816
|
return out;
|
|
12852
12817
|
}
|
|
12853
|
-
async function
|
|
12854
|
-
const
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12818
|
+
async function uniqueExtensionId(projectDir, base) {
|
|
12819
|
+
const slug = slugify(base) || "extension";
|
|
12820
|
+
let candidate = slug;
|
|
12821
|
+
for (let n = 2;existsSync14(path20.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
|
|
12822
|
+
candidate = `${slug}-${n}`;
|
|
12858
12823
|
}
|
|
12859
|
-
|
|
12860
|
-
|
|
12824
|
+
return candidate;
|
|
12825
|
+
}
|
|
12826
|
+
async function addBuiltInStyleSet(projectDir, id) {
|
|
12827
|
+
if (!BUILT_IN_STYLE_SET_IDS.includes(id)) {
|
|
12828
|
+
throw new Error(`Unknown built-in look "${id}" (one of: ${BUILT_IN_STYLE_SET_IDS.join(", ")}).`);
|
|
12829
|
+
}
|
|
12830
|
+
return withMutationLock(projectDir, async () => {
|
|
12831
|
+
const dest = path20.join(projectDir, EXTENSIONS_DIR, id);
|
|
12832
|
+
if (!existsSync14(dest)) {
|
|
12833
|
+
const src = path20.join(await getAssetsDir(), "themes", id);
|
|
12834
|
+
await mkdir11(path20.dirname(dest), { recursive: true });
|
|
12835
|
+
await cp4(src, dest, { recursive: true });
|
|
12836
|
+
}
|
|
12837
|
+
return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
|
|
12838
|
+
});
|
|
12839
|
+
}
|
|
12840
|
+
async function validateProjectExtensions(projectDir) {
|
|
12841
|
+
const out = [];
|
|
12842
|
+
for (const entry of await listProjectExtensions(projectDir)) {
|
|
12843
|
+
if (!entry.enabled) {
|
|
12844
|
+
out.push({ use: entry.use, kind: entry.kind, enabled: false, ok: true });
|
|
12845
|
+
continue;
|
|
12846
|
+
}
|
|
12847
|
+
try {
|
|
12848
|
+
await loadPlugin(extensionConfigFor(entry), projectDir);
|
|
12849
|
+
out.push({ use: entry.use, kind: entry.kind, enabled: true, ok: true });
|
|
12850
|
+
} catch (e) {
|
|
12851
|
+
out.push({
|
|
12852
|
+
use: entry.use,
|
|
12853
|
+
kind: entry.kind,
|
|
12854
|
+
enabled: true,
|
|
12855
|
+
ok: false,
|
|
12856
|
+
error: e instanceof Error ? e.message : String(e)
|
|
12857
|
+
});
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12860
|
+
return out;
|
|
12861
|
+
}
|
|
12862
|
+
async function readExtensionCss(projectDir, use) {
|
|
12863
|
+
const entry = await describeExtension(projectDir, use);
|
|
12864
|
+
if (!entry)
|
|
12865
|
+
throw new Error(`Extension "${use}" is not in the manifest.`);
|
|
12866
|
+
if (!entry.dir)
|
|
12867
|
+
throw new Error(`Extension "${use}" has no stylesheets to preview.`);
|
|
12868
|
+
const meta = await readExtensionMeta(entry.dir);
|
|
12869
|
+
const sheets = resolveExtension(entry.dir, meta, `Extension "${use}"`).styles ?? [];
|
|
12870
|
+
if (sheets.length === 0)
|
|
12871
|
+
throw new Error(`Extension "${use}" declares no stylesheets.`);
|
|
12872
|
+
const parts = [];
|
|
12873
|
+
for (const sheet of sheets)
|
|
12874
|
+
parts.push(await readFile31(sheet, "utf8"));
|
|
12875
|
+
return parts.join(`
|
|
12876
|
+
|
|
12877
|
+
`);
|
|
12861
12878
|
}
|
|
12879
|
+
|
|
12862
12880
|
// src/lib/snippets.ts
|
|
12863
|
-
import { readdir as readdir8, readFile as readFile32, writeFile as writeFile11, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
|
|
12864
|
-
import path21 from "node:path";
|
|
12865
12881
|
var SNIPPETS_DIR = "snippets";
|
|
12866
12882
|
var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
12867
12883
|
function extractVariables(template) {
|
|
@@ -16745,4 +16761,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
|
|
|
16745
16761
|
await writeManifestDoc(file, doc);
|
|
16746
16762
|
return settingsFromDoc(doc);
|
|
16747
16763
|
}
|
|
16748
|
-
export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics,
|
|
16764
|
+
export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, MAX_SEARCH_BYTES, SEARCH_KEYWORDS, searchNpmExtensions, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, refreshRemoteCopies, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
|