gutterpress 0.11.17-alpha.2 → 0.11.17-alpha.4
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/dist/api/index.d.ts +4 -0
- package/dist/api/index.js +11 -2
- package/dist/black-text-gray-7dz61456.js +15 -0
- package/dist/{build-1p999cmg.js → build-fam0tqg6.js} +5 -4
- package/dist/checks/pdf/black-text.d.ts +3 -0
- package/dist/checks/pdf/index.d.ts +2 -0
- package/dist/checks/pdf/signature.d.ts +3 -0
- package/dist/{cli-6z5zthzt.js → cli-2kc34kme.js} +470 -489
- package/dist/{cli-70xgehq2.js → cli-8apk8571.js} +1 -1
- package/dist/cli-ht1bcqdd.js +493 -0
- package/dist/{cli-gymfg04q.js → cli-jyy9pqf3.js} +15 -10
- package/dist/{cli-ppc0vz4b.js → cli-pqcb2nnb.js} +1 -1
- package/dist/cli.js +12 -12
- package/dist/{doctor-reqpnn3h.js → doctor-zre5k5fg.js} +2 -2
- package/dist/engine/compiler/build.d.ts +3 -2
- package/dist/engine/compiler/postprocess.d.ts +6 -4
- package/dist/engine/shared/synthesis.d.ts +29 -5
- package/dist/{engine-5jzhjjvv.js → engine-3xqgm82d.js} +2 -2
- package/dist/{engine-078ggczp.js → engine-zdb41ay1.js} +1 -1
- package/dist/{ext-dsbfejt3.js → ext-hfqcg6ax.js} +5 -4
- package/dist/{gutterpress-viewer-zkv2stfy.js → gutterpress-viewer-1n793gwd.js} +5 -3
- package/dist/{index-c275p061.js → index-gp503ze9.js} +1622 -1332
- package/dist/{index-5dcj1vts.js → index-hrx0gfkj.js} +23 -684
- package/dist/index-ht1bcqdd.js +493 -0
- package/dist/index-q1xqxrh9.js +949 -0
- package/dist/index.js +12 -3
- package/dist/lib/black-text-gray.d.ts +55 -8
- package/dist/lib/build-fingerprint.d.ts +2 -0
- package/dist/lib/engine.d.ts +2 -1
- package/dist/lib/lint-rules.d.ts +9 -0
- package/dist/lib/markdown/assemble.d.ts +3 -5
- package/dist/lib/markdown/index.d.ts +3 -8
- package/dist/lib/markdown/renderer.d.ts +9 -1
- package/dist/lib/page-size.d.ts +66 -0
- package/dist/lib/pdf-inspect.d.ts +6 -0
- package/dist/lib/printsafe.d.ts +8 -1
- package/dist/lib/project-scaffold.d.ts +3 -9
- package/dist/lib/remote-auth/github-repos.d.ts +7 -0
- package/dist/lib/remote-auth/online-backup.d.ts +51 -0
- package/dist/lib/snippets.d.ts +3 -0
- package/dist/{manifest.schema-40sfq49z.json → manifest.schema-gh0jjve9.json} +14 -2
- package/dist/{new-m5y2wepr.js → new-bjdf7bhg.js} +5 -4
- package/dist/{plugin.js-m4a7m3t4.tpl → plugin.js-cswq7wvd.tpl} +4 -1
- package/dist/{plugin.test.js-6n8ct5e3.tpl → plugin.test.js-xgxrd8hf.tpl} +4 -1
- package/dist/{preflight-bwpqwwny.js → preflight-gvw01661.js} +5 -4
- package/dist/preview/file-watcher.d.ts +2 -4
- package/dist/{preview-62y7qhkp.js → preview-1cschq8s.js} +5 -4
- package/dist/{publish-k2zyt3x6.js → publish-ddqrb8s9.js} +5 -4
- package/dist/render.js +30 -5
- package/dist/schema/manifest.types.d.ts +17 -0
- package/dist/{validate-fgwwxtbn.js → validate-3hjkgxrz.js} +5 -4
- package/package.json +1 -1
- package/dist/index-eyaj6nkc.js +0 -267
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blackTextToGray
|
|
3
|
+
} from "./index-ht1bcqdd.js";
|
|
1
4
|
import {
|
|
2
5
|
BuildError,
|
|
3
6
|
EXIT_CODES,
|
|
4
7
|
MARGIN_BOX_IGNORED_PROPERTIES,
|
|
5
8
|
getAssetPath,
|
|
6
|
-
getAssetsDir
|
|
7
|
-
|
|
9
|
+
getAssetsDir,
|
|
10
|
+
lastPageMode,
|
|
11
|
+
parseSize
|
|
12
|
+
} from "./index-q1xqxrh9.js";
|
|
8
13
|
import {
|
|
9
14
|
INSTALL_HINTS,
|
|
10
15
|
execCapture,
|
|
@@ -140,7 +145,7 @@ var DTRPG_PRESET = {
|
|
|
140
145
|
source: {
|
|
141
146
|
files: null
|
|
142
147
|
},
|
|
143
|
-
print: { signature: 4, reserveLastPage: true },
|
|
148
|
+
print: { signature: 4, reserveLastPage: true, lastPage: undefined },
|
|
144
149
|
pdfx: {
|
|
145
150
|
flavor: "x1a",
|
|
146
151
|
icc: "profiles/CGATS21_CRPC1.icc",
|
|
@@ -158,7 +163,8 @@ var DTRPG_PRESET = {
|
|
|
158
163
|
},
|
|
159
164
|
lint: {
|
|
160
165
|
enabled: true,
|
|
161
|
-
configPath: null
|
|
166
|
+
configPath: null,
|
|
167
|
+
rules: {}
|
|
162
168
|
},
|
|
163
169
|
validate: {
|
|
164
170
|
enabled: true,
|
|
@@ -215,7 +221,7 @@ var BOOK_PRESET = {
|
|
|
215
221
|
source: {
|
|
216
222
|
files: null
|
|
217
223
|
},
|
|
218
|
-
print: { signature: 1, reserveLastPage: false },
|
|
224
|
+
print: { signature: 1, reserveLastPage: false, lastPage: undefined },
|
|
219
225
|
pdfx: {
|
|
220
226
|
flavor: "x1a",
|
|
221
227
|
icc: "profiles/CGATS21_CRPC1.icc",
|
|
@@ -233,7 +239,8 @@ var BOOK_PRESET = {
|
|
|
233
239
|
},
|
|
234
240
|
lint: {
|
|
235
241
|
enabled: true,
|
|
236
|
-
configPath: null
|
|
242
|
+
configPath: null,
|
|
243
|
+
rules: {}
|
|
237
244
|
},
|
|
238
245
|
validate: {
|
|
239
246
|
enabled: true,
|
|
@@ -606,6 +613,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
|
|
|
606
613
|
if (legacyPlugins !== undefined) {
|
|
607
614
|
throw new UsageError(pluginsRemovedMessage(legacyPlugins));
|
|
608
615
|
}
|
|
616
|
+
const lastPage = c.print?.lastPage ?? m.print?.lastPage;
|
|
617
|
+
if (lastPage !== undefined && lastPage !== "blank" && lastPage !== "short-by-one") {
|
|
618
|
+
throw new UsageError(`Manifest field \`print.lastPage\` must be "blank" or "short-by-one", got ${JSON.stringify(lastPage)}.`);
|
|
619
|
+
}
|
|
609
620
|
const rawExtensions = c.extensions ?? m.extensions ?? [];
|
|
610
621
|
if (!Array.isArray(rawExtensions)) {
|
|
611
622
|
throw new UsageError("Manifest field `extensions` must be a list of specifiers.");
|
|
@@ -622,7 +633,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
|
|
|
622
633
|
print: mergeShape(c.print, m.print, preset.print),
|
|
623
634
|
page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
|
|
624
635
|
ink: mergeShape(c.ink, m.ink, preset.ink),
|
|
625
|
-
lint:
|
|
636
|
+
lint: {
|
|
637
|
+
...mergeShape(c.lint, m.lint, preset.lint),
|
|
638
|
+
rules: { ...preset.lint.rules, ...m.lint?.rules, ...c.lint?.rules }
|
|
639
|
+
},
|
|
626
640
|
validate: {
|
|
627
641
|
enabled: c.validate?.enabled ?? m.validate?.enabled ?? preset.validate.enabled,
|
|
628
642
|
checks: { ...preset.validate.checks, ...m.validate?.checks, ...c.validate?.checks },
|
|
@@ -1037,7 +1051,7 @@ function resolveContainerShape(name, pluginName, decl) {
|
|
|
1037
1051
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`section\` that is not a boolean.`);
|
|
1038
1052
|
}
|
|
1039
1053
|
if (decl.section) {
|
|
1040
|
-
for (const field of ["tag", "label", "autoCloseAt"]) {
|
|
1054
|
+
for (const field of ["tag", "label", "autoCloseAt", "selfClosing"]) {
|
|
1041
1055
|
if (decl[field] !== undefined) {
|
|
1042
1056
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`section: true\` and \`${field}\` — a section ` + `marker is a core @section, so \`${field}\` does not apply. Remove one of them.`);
|
|
1043
1057
|
}
|
|
@@ -1094,11 +1108,26 @@ function resolveContainerShape(name, pluginName, decl) {
|
|
|
1094
1108
|
}
|
|
1095
1109
|
autoCloseAtEof = decl.autoCloseAt.includes("eof");
|
|
1096
1110
|
}
|
|
1111
|
+
if (decl.selfClosing !== undefined && typeof decl.selfClosing !== "boolean") {
|
|
1112
|
+
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`selfClosing\` that is not a boolean.`);
|
|
1113
|
+
}
|
|
1114
|
+
if (decl.selfClosing && decl.autoCloseAt !== undefined) {
|
|
1115
|
+
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`selfClosing: true\` and \`autoCloseAt\` — a ` + `self-closing marker has no open container to auto-close. Remove one of them.`);
|
|
1116
|
+
}
|
|
1097
1117
|
if (decl.validate !== undefined && typeof decl.validate !== "function") {
|
|
1098
1118
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
|
|
1099
1119
|
}
|
|
1100
1120
|
const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
|
|
1101
|
-
return {
|
|
1121
|
+
return {
|
|
1122
|
+
tag,
|
|
1123
|
+
classBase,
|
|
1124
|
+
variants,
|
|
1125
|
+
label,
|
|
1126
|
+
autoCloseAtEof,
|
|
1127
|
+
...decl.section ? { section: true } : {},
|
|
1128
|
+
...decl.selfClosing ? { selfClosing: true } : {},
|
|
1129
|
+
...check
|
|
1130
|
+
};
|
|
1102
1131
|
}
|
|
1103
1132
|
function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
|
|
1104
1133
|
const pluginName = originOf.get(name);
|
|
@@ -1357,7 +1386,8 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1357
1386
|
addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
|
|
1358
1387
|
attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
|
|
1359
1388
|
out.push(t);
|
|
1360
|
-
|
|
1389
|
+
if (!decl.selfClosing)
|
|
1390
|
+
declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
|
|
1361
1391
|
if (decl.label) {
|
|
1362
1392
|
const value = meta.attrs && meta.attrs[decl.label.attr];
|
|
1363
1393
|
if (value) {
|
|
@@ -1368,6 +1398,8 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1368
1398
|
t.meta.labelled = true;
|
|
1369
1399
|
}
|
|
1370
1400
|
}
|
|
1401
|
+
if (decl.selfClosing)
|
|
1402
|
+
out.push(new state.Token("layout_component_close", decl.tag, -1));
|
|
1371
1403
|
}
|
|
1372
1404
|
const stack = {
|
|
1373
1405
|
frames: [],
|
|
@@ -1646,7 +1678,7 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1646
1678
|
continue;
|
|
1647
1679
|
}
|
|
1648
1680
|
if (!closeDeclaredFrame(closeDecl.baseKind)) {
|
|
1649
|
-
warn(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
|
|
1681
|
+
warn(state.env, line, "declared_marker_close_without_open", closeDecl.selfClosing ? `@${kind} is not a marker: @${closeDecl.baseKind} is self-closing and has nothing to close; ignoring marker.` : `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
|
|
1650
1682
|
}
|
|
1651
1683
|
continue;
|
|
1652
1684
|
}
|
|
@@ -2859,7 +2891,8 @@ ${projectCss.trim()}` : null
|
|
|
2859
2891
|
`);
|
|
2860
2892
|
const preloadTags = (opts.preloadImages ?? []).map((href) => `
|
|
2861
2893
|
<link rel="preload" as="image" href="${href}">`).join("");
|
|
2862
|
-
const
|
|
2894
|
+
const lastPage = opts.print && lastPageMode(opts.print);
|
|
2895
|
+
const printAttrs = (opts.print?.signature && opts.print.signature > 1 ? ` data-gp-signature="${opts.print.signature}"` : "") + (lastPage ? ` data-gp-last-page="${lastPage}"` : "");
|
|
2863
2896
|
return `<!DOCTYPE html>
|
|
2864
2897
|
<html lang="en"${printAttrs}>
|
|
2865
2898
|
<head>
|
|
@@ -5641,7 +5674,7 @@ import git from "isomorphic-git";
|
|
|
5641
5674
|
// package.json
|
|
5642
5675
|
var package_default = {
|
|
5643
5676
|
name: "gutterpress",
|
|
5644
|
-
version: "0.11.17-alpha.
|
|
5677
|
+
version: "0.11.17-alpha.4",
|
|
5645
5678
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
5646
5679
|
author: "itlackey",
|
|
5647
5680
|
license: "MPL-2.0",
|
|
@@ -5895,7 +5928,8 @@ async function writeBuildFingerprint(input) {
|
|
|
5895
5928
|
requestedFlavor: input.pdfx.requestedFlavor,
|
|
5896
5929
|
resolvedFlavor: input.pdfx.resolvedFlavor,
|
|
5897
5930
|
iccPath: input.pdfx.iccPath,
|
|
5898
|
-
stripAnnotations: input.pdfx.stripAnnotations
|
|
5931
|
+
stripAnnotations: input.pdfx.stripAnnotations,
|
|
5932
|
+
blackText: input.pdfx.blackText ?? null
|
|
5899
5933
|
},
|
|
5900
5934
|
outputDir: input.recordedOutputDir ? path10.resolve(input.recordedOutputDir) : outputDir
|
|
5901
5935
|
},
|
|
@@ -5907,261 +5941,6 @@ async function writeBuildFingerprint(input) {
|
|
|
5907
5941
|
return outPath;
|
|
5908
5942
|
}
|
|
5909
5943
|
|
|
5910
|
-
// src/lib/black-text-gray.ts
|
|
5911
|
-
var MAX_CHANNEL = 0.12;
|
|
5912
|
-
var MAX_SPREAD = 0.04;
|
|
5913
|
-
function isNearBlack([r, g, b]) {
|
|
5914
|
-
const max = Math.max(r, g, b);
|
|
5915
|
-
return max <= MAX_CHANNEL && max - Math.min(r, g, b) <= MAX_SPREAD;
|
|
5916
|
-
}
|
|
5917
|
-
var WS = new Set([0, 9, 10, 12, 13, 32]);
|
|
5918
|
-
var DELIM = new Set("()<>[]{}/%".split("").map((c) => c.charCodeAt(0)));
|
|
5919
|
-
function* tokenize(s) {
|
|
5920
|
-
let i = 0;
|
|
5921
|
-
const n = s.length;
|
|
5922
|
-
while (i < n) {
|
|
5923
|
-
const c = s.charCodeAt(i);
|
|
5924
|
-
if (WS.has(c)) {
|
|
5925
|
-
i++;
|
|
5926
|
-
continue;
|
|
5927
|
-
}
|
|
5928
|
-
const start = i;
|
|
5929
|
-
if (c === 37) {
|
|
5930
|
-
while (i < n && s[i] !== `
|
|
5931
|
-
` && s[i] !== "\r")
|
|
5932
|
-
i++;
|
|
5933
|
-
continue;
|
|
5934
|
-
}
|
|
5935
|
-
if (c === 40) {
|
|
5936
|
-
let depth = 0;
|
|
5937
|
-
while (i < n) {
|
|
5938
|
-
const d = s[i++];
|
|
5939
|
-
if (d === "\\")
|
|
5940
|
-
i++;
|
|
5941
|
-
else if (d === "(")
|
|
5942
|
-
depth++;
|
|
5943
|
-
else if (d === ")" && --depth === 0)
|
|
5944
|
-
break;
|
|
5945
|
-
}
|
|
5946
|
-
yield { text: s.slice(start, i), start, end: i, op: false };
|
|
5947
|
-
continue;
|
|
5948
|
-
}
|
|
5949
|
-
if (c === 60 && s[i + 1] === "<") {
|
|
5950
|
-
i += 2;
|
|
5951
|
-
yield { text: "<<", start, end: i, op: false };
|
|
5952
|
-
continue;
|
|
5953
|
-
}
|
|
5954
|
-
if (c === 62 && s[i + 1] === ">") {
|
|
5955
|
-
i += 2;
|
|
5956
|
-
yield { text: ">>", start, end: i, op: false };
|
|
5957
|
-
continue;
|
|
5958
|
-
}
|
|
5959
|
-
if (c === 60) {
|
|
5960
|
-
while (i < n && s[i] !== ">")
|
|
5961
|
-
i++;
|
|
5962
|
-
i++;
|
|
5963
|
-
yield { text: s.slice(start, i), start, end: i, op: false };
|
|
5964
|
-
continue;
|
|
5965
|
-
}
|
|
5966
|
-
if (c === 91 || c === 93 || c === 62 || c === 123 || c === 125 || c === 41) {
|
|
5967
|
-
i++;
|
|
5968
|
-
yield { text: s[start], start, end: i, op: false };
|
|
5969
|
-
continue;
|
|
5970
|
-
}
|
|
5971
|
-
i++;
|
|
5972
|
-
while (i < n && !WS.has(s.charCodeAt(i)) && !DELIM.has(s.charCodeAt(i)))
|
|
5973
|
-
i++;
|
|
5974
|
-
const text = s.slice(start, i);
|
|
5975
|
-
const isOperand = c === 47 || /^[+-]?(\d+\.?\d*|\.\d+)$/.test(text);
|
|
5976
|
-
yield { text, start, end: i, op: !isOperand };
|
|
5977
|
-
if (text === "ID") {
|
|
5978
|
-
const m = /\sEI(?=\s|$)/g;
|
|
5979
|
-
m.lastIndex = i;
|
|
5980
|
-
const hit = m.exec(s);
|
|
5981
|
-
i = hit ? hit.index + hit[0].length : n;
|
|
5982
|
-
}
|
|
5983
|
-
}
|
|
5984
|
-
}
|
|
5985
|
-
function rewriteBlackText(src) {
|
|
5986
|
-
let fill = null;
|
|
5987
|
-
let stroke = null;
|
|
5988
|
-
const stack = [];
|
|
5989
|
-
let inText = false;
|
|
5990
|
-
let fillDirty = false;
|
|
5991
|
-
let strokeDirty = false;
|
|
5992
|
-
const edits = [];
|
|
5993
|
-
let operands = [];
|
|
5994
|
-
const rgbOf = () => {
|
|
5995
|
-
if (operands.length !== 3)
|
|
5996
|
-
return null;
|
|
5997
|
-
const v = operands.map((t) => Number(t.text));
|
|
5998
|
-
return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
|
|
5999
|
-
};
|
|
6000
|
-
for (const t of tokenize(src)) {
|
|
6001
|
-
if (!t.op) {
|
|
6002
|
-
operands.push(t);
|
|
6003
|
-
continue;
|
|
6004
|
-
}
|
|
6005
|
-
const op = t.text;
|
|
6006
|
-
switch (op) {
|
|
6007
|
-
case "q":
|
|
6008
|
-
stack.push([fill, stroke]);
|
|
6009
|
-
break;
|
|
6010
|
-
case "Q": {
|
|
6011
|
-
const p = stack.pop();
|
|
6012
|
-
if (p)
|
|
6013
|
-
[fill, stroke] = p;
|
|
6014
|
-
break;
|
|
6015
|
-
}
|
|
6016
|
-
case "BT":
|
|
6017
|
-
inText = true;
|
|
6018
|
-
fillDirty = strokeDirty = false;
|
|
6019
|
-
{
|
|
6020
|
-
let ins = "";
|
|
6021
|
-
if (fill && isNearBlack(fill.rgb)) {
|
|
6022
|
-
ins += " 0 g";
|
|
6023
|
-
fillDirty = true;
|
|
6024
|
-
}
|
|
6025
|
-
if (stroke && isNearBlack(stroke.rgb)) {
|
|
6026
|
-
ins += " 0 G";
|
|
6027
|
-
strokeDirty = true;
|
|
6028
|
-
}
|
|
6029
|
-
if (ins)
|
|
6030
|
-
edits.push({ start: t.end, end: t.end, text: ins });
|
|
6031
|
-
}
|
|
6032
|
-
break;
|
|
6033
|
-
case "ET":
|
|
6034
|
-
if (inText) {
|
|
6035
|
-
let ins = "";
|
|
6036
|
-
if (fillDirty && fill)
|
|
6037
|
-
ins += `${fill.raw} rg `;
|
|
6038
|
-
if (strokeDirty && stroke)
|
|
6039
|
-
ins += `${stroke.raw} RG `;
|
|
6040
|
-
if (ins)
|
|
6041
|
-
edits.push({ start: t.start, end: t.start, text: ins });
|
|
6042
|
-
}
|
|
6043
|
-
inText = false;
|
|
6044
|
-
break;
|
|
6045
|
-
case "rg":
|
|
6046
|
-
case "RG": {
|
|
6047
|
-
const c = rgbOf();
|
|
6048
|
-
const isFill = op === "rg";
|
|
6049
|
-
if (isFill)
|
|
6050
|
-
fill = c;
|
|
6051
|
-
else
|
|
6052
|
-
stroke = c;
|
|
6053
|
-
if (inText && c && isNearBlack(c.rgb)) {
|
|
6054
|
-
edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
|
|
6055
|
-
if (isFill)
|
|
6056
|
-
fillDirty = true;
|
|
6057
|
-
else
|
|
6058
|
-
strokeDirty = true;
|
|
6059
|
-
} else if (inText) {
|
|
6060
|
-
if (isFill)
|
|
6061
|
-
fillDirty = false;
|
|
6062
|
-
else
|
|
6063
|
-
strokeDirty = false;
|
|
6064
|
-
}
|
|
6065
|
-
break;
|
|
6066
|
-
}
|
|
6067
|
-
case "g":
|
|
6068
|
-
case "k":
|
|
6069
|
-
case "sc":
|
|
6070
|
-
case "scn":
|
|
6071
|
-
case "cs":
|
|
6072
|
-
fill = null;
|
|
6073
|
-
if (inText)
|
|
6074
|
-
fillDirty = false;
|
|
6075
|
-
break;
|
|
6076
|
-
case "G":
|
|
6077
|
-
case "K":
|
|
6078
|
-
case "SC":
|
|
6079
|
-
case "SCN":
|
|
6080
|
-
case "CS":
|
|
6081
|
-
stroke = null;
|
|
6082
|
-
if (inText)
|
|
6083
|
-
strokeDirty = false;
|
|
6084
|
-
break;
|
|
6085
|
-
}
|
|
6086
|
-
operands = [];
|
|
6087
|
-
}
|
|
6088
|
-
return applyEdits(src, edits);
|
|
6089
|
-
}
|
|
6090
|
-
function applyEdits(s, edits) {
|
|
6091
|
-
if (!edits.length)
|
|
6092
|
-
return s;
|
|
6093
|
-
let out = "";
|
|
6094
|
-
let pos = 0;
|
|
6095
|
-
for (const e of edits) {
|
|
6096
|
-
out += s.slice(pos, e.start) + e.text;
|
|
6097
|
-
pos = e.end;
|
|
6098
|
-
}
|
|
6099
|
-
return out + s.slice(pos);
|
|
6100
|
-
}
|
|
6101
|
-
async function blackTextToGray(pdf) {
|
|
6102
|
-
const { PDFDocument, PDFName, PDFRawStream, PDFArray, PDFRef, decodePDFRawStream } = await import("pdf-lib");
|
|
6103
|
-
const doc = await PDFDocument.load(pdf, { updateMetadata: false });
|
|
6104
|
-
const ctx = doc.context;
|
|
6105
|
-
const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
|
|
6106
|
-
let changed = false;
|
|
6107
|
-
const decode = (s) => {
|
|
6108
|
-
try {
|
|
6109
|
-
return Buffer.from(decodePDFRawStream(s).decode()).toString("latin1");
|
|
6110
|
-
} catch {
|
|
6111
|
-
return null;
|
|
6112
|
-
}
|
|
6113
|
-
};
|
|
6114
|
-
const replace = (ref, old, text) => {
|
|
6115
|
-
const entries = {};
|
|
6116
|
-
for (const [k, v] of old.dict.entries())
|
|
6117
|
-
if (!SKIP.includes(k))
|
|
6118
|
-
entries[k.decodeText()] = v;
|
|
6119
|
-
ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
|
|
6120
|
-
changed = true;
|
|
6121
|
-
};
|
|
6122
|
-
for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
|
|
6123
|
-
if (!(obj instanceof PDFRawStream))
|
|
6124
|
-
continue;
|
|
6125
|
-
if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
|
|
6126
|
-
continue;
|
|
6127
|
-
const text = decode(obj);
|
|
6128
|
-
if (text === null)
|
|
6129
|
-
continue;
|
|
6130
|
-
const next = rewriteBlackText(text);
|
|
6131
|
-
if (next !== text)
|
|
6132
|
-
replace(ref, obj, next);
|
|
6133
|
-
}
|
|
6134
|
-
for (const page of doc.getPages()) {
|
|
6135
|
-
const contents = page.node.Contents();
|
|
6136
|
-
const refs = [];
|
|
6137
|
-
if (contents instanceof PDFArray) {
|
|
6138
|
-
for (let i = 0;i < contents.size(); i++) {
|
|
6139
|
-
const r = contents.get(i);
|
|
6140
|
-
if (r instanceof PDFRef)
|
|
6141
|
-
refs.push(r);
|
|
6142
|
-
}
|
|
6143
|
-
} else if (contents instanceof PDFRawStream) {
|
|
6144
|
-
const r = page.node.get(PDFName.of("Contents"));
|
|
6145
|
-
if (r instanceof PDFRef)
|
|
6146
|
-
refs.push(r);
|
|
6147
|
-
}
|
|
6148
|
-
const streams = refs.map((r) => ctx.lookup(r));
|
|
6149
|
-
if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
|
|
6150
|
-
continue;
|
|
6151
|
-
const parts = streams.map((s) => decode(s));
|
|
6152
|
-
if (parts.some((p) => p === null))
|
|
6153
|
-
continue;
|
|
6154
|
-
const joined = parts.join(`
|
|
6155
|
-
`);
|
|
6156
|
-
const next = rewriteBlackText(joined);
|
|
6157
|
-
if (next === joined)
|
|
6158
|
-
continue;
|
|
6159
|
-
replace(refs[0], streams[0], next);
|
|
6160
|
-
page.node.set(PDFName.of("Contents"), refs[0]);
|
|
6161
|
-
}
|
|
6162
|
-
return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
|
|
6163
|
-
}
|
|
6164
|
-
|
|
6165
5944
|
// src/lib/validation-exec.ts
|
|
6166
5945
|
import { existsSync as existsSync12 } from "node:fs";
|
|
6167
5946
|
import { dirname as dirname4, join as join4, resolve as resolve8 } from "node:path";
|
|
@@ -6766,6 +6545,32 @@ function getImagePixelSize(page, name, timeoutMs = 2000) {
|
|
|
6766
6545
|
}
|
|
6767
6546
|
});
|
|
6768
6547
|
}
|
|
6548
|
+
var PAINT_OPS = new Set([
|
|
6549
|
+
"stroke",
|
|
6550
|
+
"closeStroke",
|
|
6551
|
+
"fill",
|
|
6552
|
+
"eoFill",
|
|
6553
|
+
"fillStroke",
|
|
6554
|
+
"eoFillStroke",
|
|
6555
|
+
"closeFillStroke",
|
|
6556
|
+
"closeEOFillStroke",
|
|
6557
|
+
"shadingFill",
|
|
6558
|
+
"paintImageXObject",
|
|
6559
|
+
"paintInlineImageXObject",
|
|
6560
|
+
"paintImageXObjectRepeat",
|
|
6561
|
+
"paintImageMaskXObject",
|
|
6562
|
+
"paintImageMaskXObjectGroup",
|
|
6563
|
+
"paintImageMaskXObjectRepeat",
|
|
6564
|
+
"paintSolidColorImageMask",
|
|
6565
|
+
"showText",
|
|
6566
|
+
"showSpacedText",
|
|
6567
|
+
"nextLineShowText",
|
|
6568
|
+
"nextLineSetSpacingShowText"
|
|
6569
|
+
].map((name) => OPS[name]).filter((op) => op !== undefined));
|
|
6570
|
+
async function pageHasMarks(doc, pageNum) {
|
|
6571
|
+
const ops = await (await doc.getPage(pageNum)).getOperatorList();
|
|
6572
|
+
return ops.fnArray.some((fn, i) => fn === OPS.constructPath ? PAINT_OPS.has(ops.argsArray[i][0]) : PAINT_OPS.has(fn));
|
|
6573
|
+
}
|
|
6769
6574
|
async function getImageResolutions(doc) {
|
|
6770
6575
|
const { imagesByPage } = await getOpPass(doc);
|
|
6771
6576
|
const out = [];
|
|
@@ -7269,8 +7074,100 @@ var check8 = {
|
|
|
7269
7074
|
};
|
|
7270
7075
|
registerCheck(check8);
|
|
7271
7076
|
|
|
7272
|
-
// src/checks/pdf/
|
|
7077
|
+
// src/checks/pdf/black-text.ts
|
|
7078
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
7273
7079
|
var check9 = {
|
|
7080
|
+
id: "pdf.print.black-text",
|
|
7081
|
+
name: "Black Text",
|
|
7082
|
+
description: "Warns when text at or below 24pt is painted in a dark four-colour mix (black, or a near-black colour such as a dark brown)",
|
|
7083
|
+
category: "pdf",
|
|
7084
|
+
phase: "post-build",
|
|
7085
|
+
enabledWhen: (config) => config.pdfx.blackText === "k-only",
|
|
7086
|
+
async run(ctx) {
|
|
7087
|
+
if (!ctx.pdfPath)
|
|
7088
|
+
return [];
|
|
7089
|
+
try {
|
|
7090
|
+
const { findRichBlackText, LARGE_TEXT_PT } = await import("./black-text-gray-7dz61456.js");
|
|
7091
|
+
const found = await findRichBlackText(await readFile15(ctx.pdfPath));
|
|
7092
|
+
if (found.runs === 0)
|
|
7093
|
+
return [];
|
|
7094
|
+
return [
|
|
7095
|
+
finding(check9.id, {
|
|
7096
|
+
severity: "warning",
|
|
7097
|
+
message: `Dark four-colour text at ${LARGE_TEXT_PT}pt or smaller on page(s) ${found.pages.join(", ")}.`,
|
|
7098
|
+
file: ctx.pdfPath,
|
|
7099
|
+
detail: "Small text printed with all four inks can look fuzzy if the plates are slightly out of register, which is why print vendors such as DriveThruRPG want small black text on the black plate alone. `pdfx.blackText: k-only` already does that for near-black text; this text is either a dark colour (for example a dark brown, which keeps its colour) or painted in a way the build cannot rewrite (an image or a pattern). To print it on the black plate, make the text black (#000). If the colour is intended, set `lint.rules: { pdf.print.black-text: off }` in manifest.yaml.",
|
|
7100
|
+
code: "rich-black-text",
|
|
7101
|
+
data: { runs: found.runs, pages: found.pages }
|
|
7102
|
+
})
|
|
7103
|
+
];
|
|
7104
|
+
} catch {
|
|
7105
|
+
return [
|
|
7106
|
+
inspectionFailed(check9.id, "Could not inspect the PDF's text colours.", {
|
|
7107
|
+
file: ctx.pdfPath
|
|
7108
|
+
})
|
|
7109
|
+
];
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
};
|
|
7113
|
+
registerCheck(check9);
|
|
7114
|
+
|
|
7115
|
+
// src/checks/pdf/signature.ts
|
|
7116
|
+
var check10 = {
|
|
7117
|
+
id: "pdf.print.signature",
|
|
7118
|
+
name: "Signature",
|
|
7119
|
+
description: "Fails when the interior's page count does not fit the print signature (a multiple of it ending on a blank page, or one short of a multiple)",
|
|
7120
|
+
category: "pdf",
|
|
7121
|
+
phase: "post-build",
|
|
7122
|
+
enabledWhen: (config) => config.print.signature > 1,
|
|
7123
|
+
async run(ctx) {
|
|
7124
|
+
if (!ctx.pdfPath)
|
|
7125
|
+
return [];
|
|
7126
|
+
const doc = await loadPdf(ctx.pdfPath);
|
|
7127
|
+
if (!doc) {
|
|
7128
|
+
return [inspectionFailed(check10.id, "Could not parse the PDF's page count.", { file: ctx.pdfPath })];
|
|
7129
|
+
}
|
|
7130
|
+
const size = ctx.config.print.signature;
|
|
7131
|
+
const pages = doc.numPages;
|
|
7132
|
+
if ((pages + 1) % size === 0)
|
|
7133
|
+
return [];
|
|
7134
|
+
const data = { pages, signature: size };
|
|
7135
|
+
if (pages % size !== 0) {
|
|
7136
|
+
return [
|
|
7137
|
+
finding(check10.id, {
|
|
7138
|
+
severity: "error",
|
|
7139
|
+
message: `Page count ${pages} does not fit a signature of ${size}: it must be a multiple of ${size}, or one short of one.`,
|
|
7140
|
+
file: ctx.pdfPath,
|
|
7141
|
+
detail: "Set `print.signature` in the manifest and rebuild; the build pads the interior for you.",
|
|
7142
|
+
code: "signature-page-count",
|
|
7143
|
+
data
|
|
7144
|
+
})
|
|
7145
|
+
];
|
|
7146
|
+
}
|
|
7147
|
+
if (!lastPageMode(ctx.config.print))
|
|
7148
|
+
return [];
|
|
7149
|
+
try {
|
|
7150
|
+
if (!await pageHasMarks(doc, pages))
|
|
7151
|
+
return [];
|
|
7152
|
+
} catch {
|
|
7153
|
+
return [inspectionFailed(check10.id, "Could not read the last page.", { file: ctx.pdfPath })];
|
|
7154
|
+
}
|
|
7155
|
+
return [
|
|
7156
|
+
finding(check10.id, {
|
|
7157
|
+
severity: "error",
|
|
7158
|
+
message: `The last page (${pages}) has content, but the printer needs it blank for its own information.`,
|
|
7159
|
+
file: ctx.pdfPath,
|
|
7160
|
+
detail: "The build adds that page for you (`print.lastPage: blank`, or `short-by-one` to stop a page short of a multiple " + `of ${size} and let the printer add it). Check that the interior was built with this manifest, and that nothing in it ` + "forces content onto the final page.",
|
|
7161
|
+
code: "signature-last-page",
|
|
7162
|
+
data
|
|
7163
|
+
})
|
|
7164
|
+
];
|
|
7165
|
+
}
|
|
7166
|
+
};
|
|
7167
|
+
registerCheck(check10);
|
|
7168
|
+
|
|
7169
|
+
// src/checks/pdf/bookmarks.ts
|
|
7170
|
+
var check11 = {
|
|
7274
7171
|
id: "pdf.nav.bookmarks",
|
|
7275
7172
|
name: "Bookmarks",
|
|
7276
7173
|
description: "Checks for PDF outline (bookmarks) tree",
|
|
@@ -7284,14 +7181,14 @@ var check9 = {
|
|
|
7284
7181
|
const doc = await loadPdf(ctx.pdfPath);
|
|
7285
7182
|
if (!doc) {
|
|
7286
7183
|
return [
|
|
7287
|
-
inspectionFailed(
|
|
7184
|
+
inspectionFailed(check11.id, "Could not inspect PDF for bookmarks.", {
|
|
7288
7185
|
file: ctx.pdfPath
|
|
7289
7186
|
})
|
|
7290
7187
|
];
|
|
7291
7188
|
}
|
|
7292
7189
|
if (await getOutlineCount(doc) === 0) {
|
|
7293
7190
|
return [
|
|
7294
|
-
finding(
|
|
7191
|
+
finding(check11.id, {
|
|
7295
7192
|
severity: "warning",
|
|
7296
7193
|
message: "PDF does not contain bookmarks (outline tree).",
|
|
7297
7194
|
file: ctx.pdfPath
|
|
@@ -7301,10 +7198,10 @@ var check9 = {
|
|
|
7301
7198
|
return [];
|
|
7302
7199
|
}
|
|
7303
7200
|
};
|
|
7304
|
-
registerCheck(
|
|
7201
|
+
registerCheck(check11);
|
|
7305
7202
|
|
|
7306
7203
|
// src/checks/pdf/toc-links.ts
|
|
7307
|
-
var
|
|
7204
|
+
var check12 = {
|
|
7308
7205
|
id: "pdf.nav.toc-links",
|
|
7309
7206
|
name: "TOC Links",
|
|
7310
7207
|
description: "Detects link annotations on TOC pages",
|
|
@@ -7319,7 +7216,7 @@ var check10 = {
|
|
|
7319
7216
|
if (!doc) {
|
|
7320
7217
|
return [
|
|
7321
7218
|
{
|
|
7322
|
-
checkId:
|
|
7219
|
+
checkId: check12.id,
|
|
7323
7220
|
severity: "warning",
|
|
7324
7221
|
message: "Could not inspect PDF for TOC links.",
|
|
7325
7222
|
file: ctx.pdfPath
|
|
@@ -7330,7 +7227,7 @@ var check10 = {
|
|
|
7330
7227
|
if (!hasLinks) {
|
|
7331
7228
|
return [
|
|
7332
7229
|
{
|
|
7333
|
-
checkId:
|
|
7230
|
+
checkId: check12.id,
|
|
7334
7231
|
severity: "warning",
|
|
7335
7232
|
message: "No link annotations found in PDF (expected for TOC).",
|
|
7336
7233
|
file: ctx.pdfPath
|
|
@@ -7340,10 +7237,10 @@ var check10 = {
|
|
|
7340
7237
|
return [];
|
|
7341
7238
|
}
|
|
7342
7239
|
};
|
|
7343
|
-
registerCheck(
|
|
7240
|
+
registerCheck(check12);
|
|
7344
7241
|
|
|
7345
7242
|
// src/checks/pdf/cross-refs.ts
|
|
7346
|
-
var
|
|
7243
|
+
var check13 = {
|
|
7347
7244
|
id: "pdf.nav.cross-refs",
|
|
7348
7245
|
name: "Cross References",
|
|
7349
7246
|
description: "Counts and verifies internal link annotations",
|
|
@@ -7356,7 +7253,7 @@ var check11 = {
|
|
|
7356
7253
|
if (!doc) {
|
|
7357
7254
|
return [
|
|
7358
7255
|
{
|
|
7359
|
-
checkId:
|
|
7256
|
+
checkId: check13.id,
|
|
7360
7257
|
severity: "info",
|
|
7361
7258
|
message: "Could not inspect PDF for cross-references.",
|
|
7362
7259
|
file: ctx.pdfPath
|
|
@@ -7366,7 +7263,7 @@ var check11 = {
|
|
|
7366
7263
|
const linkCount = await countLinkAnnotations(doc);
|
|
7367
7264
|
return [
|
|
7368
7265
|
{
|
|
7369
|
-
checkId:
|
|
7266
|
+
checkId: check13.id,
|
|
7370
7267
|
severity: "info",
|
|
7371
7268
|
message: `PDF contains ${linkCount} internal link annotation${linkCount !== 1 ? "s" : ""}.`,
|
|
7372
7269
|
file: ctx.pdfPath
|
|
@@ -7374,10 +7271,10 @@ var check11 = {
|
|
|
7374
7271
|
];
|
|
7375
7272
|
}
|
|
7376
7273
|
};
|
|
7377
|
-
registerCheck(
|
|
7274
|
+
registerCheck(check13);
|
|
7378
7275
|
|
|
7379
7276
|
// src/checks/pdf/page-labels.ts
|
|
7380
|
-
var
|
|
7277
|
+
var check14 = {
|
|
7381
7278
|
id: "pdf.nav.page-labels",
|
|
7382
7279
|
name: "Page Labels",
|
|
7383
7280
|
description: "Verifies PDF page labels / folio numbering",
|
|
@@ -7392,7 +7289,7 @@ var check12 = {
|
|
|
7392
7289
|
const labels = await getPageLabels(doc);
|
|
7393
7290
|
return [
|
|
7394
7291
|
{
|
|
7395
|
-
checkId:
|
|
7292
|
+
checkId: check14.id,
|
|
7396
7293
|
severity: "info",
|
|
7397
7294
|
message: labels && labels.length > 0 ? "PDF contains page labels." : "PDF does not contain explicit page labels.",
|
|
7398
7295
|
file: ctx.pdfPath
|
|
@@ -7400,10 +7297,10 @@ var check12 = {
|
|
|
7400
7297
|
];
|
|
7401
7298
|
}
|
|
7402
7299
|
};
|
|
7403
|
-
registerCheck(
|
|
7300
|
+
registerCheck(check14);
|
|
7404
7301
|
|
|
7405
7302
|
// src/checks/pdf/image-resolution.ts
|
|
7406
|
-
var
|
|
7303
|
+
var check15 = {
|
|
7407
7304
|
id: "pdf.print.image-resolution",
|
|
7408
7305
|
name: "Image Resolution",
|
|
7409
7306
|
description: "Checks DPI of images embedded in the PDF",
|
|
@@ -7430,7 +7327,7 @@ var check13 = {
|
|
|
7430
7327
|
lowResPages.sort((a, b) => a - b);
|
|
7431
7328
|
return [
|
|
7432
7329
|
{
|
|
7433
|
-
checkId:
|
|
7330
|
+
checkId: check15.id,
|
|
7434
7331
|
severity: "warning",
|
|
7435
7332
|
message: `Low-resolution images (below ${minDpi} DPI) found on pages: ${lowResPages.join(", ")}`,
|
|
7436
7333
|
file: ctx.pdfPath
|
|
@@ -7440,10 +7337,10 @@ var check13 = {
|
|
|
7440
7337
|
return [];
|
|
7441
7338
|
}
|
|
7442
7339
|
};
|
|
7443
|
-
registerCheck(
|
|
7340
|
+
registerCheck(check15);
|
|
7444
7341
|
|
|
7445
7342
|
// src/checks/pdf/transparency.ts
|
|
7446
|
-
var
|
|
7343
|
+
var check16 = {
|
|
7447
7344
|
id: "pdf.print.transparency",
|
|
7448
7345
|
name: "Transparency",
|
|
7449
7346
|
description: "Checks for transparency markers (/Transparency, /SMask, /BM) in the PDF",
|
|
@@ -7465,7 +7362,7 @@ var check14 = {
|
|
|
7465
7362
|
if (found.length > 0) {
|
|
7466
7363
|
return [
|
|
7467
7364
|
{
|
|
7468
|
-
checkId:
|
|
7365
|
+
checkId: check16.id,
|
|
7469
7366
|
severity: "warning",
|
|
7470
7367
|
message: `Transparency detected in PDF: ${found.join(", ")}. This may cause issues with some printers.`,
|
|
7471
7368
|
file: ctx.pdfPath
|
|
@@ -7475,7 +7372,7 @@ var check14 = {
|
|
|
7475
7372
|
return [];
|
|
7476
7373
|
}
|
|
7477
7374
|
};
|
|
7478
|
-
registerCheck(
|
|
7375
|
+
registerCheck(check16);
|
|
7479
7376
|
|
|
7480
7377
|
// src/checks/pdf/bleed.ts
|
|
7481
7378
|
function findBox(bytes, box) {
|
|
@@ -7485,7 +7382,7 @@ function findBox(bytes, box) {
|
|
|
7485
7382
|
return null;
|
|
7486
7383
|
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
7487
7384
|
}
|
|
7488
|
-
var
|
|
7385
|
+
var check17 = {
|
|
7489
7386
|
id: "pdf.print.bleed",
|
|
7490
7387
|
name: "Bleed Box",
|
|
7491
7388
|
description: "Compares MediaBox vs TrimBox/BleedBox to verify bleed area",
|
|
@@ -7499,7 +7396,7 @@ var check15 = {
|
|
|
7499
7396
|
const bytes = await readPdfBytes(ctx.pdfPath).catch(() => "");
|
|
7500
7397
|
if (!bytes) {
|
|
7501
7398
|
return [
|
|
7502
|
-
inspectionFailed(
|
|
7399
|
+
inspectionFailed(check17.id, "Could not inspect PDF for bleed boxes.", {
|
|
7503
7400
|
file: ctx.pdfPath
|
|
7504
7401
|
})
|
|
7505
7402
|
];
|
|
@@ -7509,14 +7406,14 @@ var check15 = {
|
|
|
7509
7406
|
const bleed = findBox(bytes, "BleedBox");
|
|
7510
7407
|
if (!media) {
|
|
7511
7408
|
return [
|
|
7512
|
-
inspectionFailed(
|
|
7409
|
+
inspectionFailed(check17.id, "Could not parse MediaBox from PDF.", {
|
|
7513
7410
|
file: ctx.pdfPath
|
|
7514
7411
|
})
|
|
7515
7412
|
];
|
|
7516
7413
|
}
|
|
7517
7414
|
if (!trim && !bleed) {
|
|
7518
7415
|
return [
|
|
7519
|
-
finding(
|
|
7416
|
+
finding(check17.id, {
|
|
7520
7417
|
severity: "warning",
|
|
7521
7418
|
message: "No TrimBox or BleedBox found. Bleed area cannot be verified.",
|
|
7522
7419
|
file: ctx.pdfPath
|
|
@@ -7529,7 +7426,7 @@ var check15 = {
|
|
|
7529
7426
|
const bleedSize = ctx.config.validate.pdf.bleedSize;
|
|
7530
7427
|
if (mediaW - trimW < bleedSize * 2 * 0.9) {
|
|
7531
7428
|
return [
|
|
7532
|
-
finding(
|
|
7429
|
+
finding(check17.id, {
|
|
7533
7430
|
severity: "warning",
|
|
7534
7431
|
message: `Bleed area appears insufficient. Expected at least ${bleedSize}pt on each side.`,
|
|
7535
7432
|
file: ctx.pdfPath
|
|
@@ -7540,10 +7437,10 @@ var check15 = {
|
|
|
7540
7437
|
return [];
|
|
7541
7438
|
}
|
|
7542
7439
|
};
|
|
7543
|
-
registerCheck(
|
|
7440
|
+
registerCheck(check17);
|
|
7544
7441
|
|
|
7545
7442
|
// src/checks/pdf/pdfx-metadata.ts
|
|
7546
|
-
var
|
|
7443
|
+
var check18 = {
|
|
7547
7444
|
id: "pdf.print.pdfx-metadata",
|
|
7548
7445
|
name: "PDF/X Metadata",
|
|
7549
7446
|
description: "Verifies PDF/X DOCINFO metadata via qpdf JSON",
|
|
@@ -7562,30 +7459,30 @@ var check16 = {
|
|
|
7562
7459
|
const objects = parseQpdfObjectsJson(stdout);
|
|
7563
7460
|
if (!objects) {
|
|
7564
7461
|
return [
|
|
7565
|
-
inspectionFailed(
|
|
7462
|
+
inspectionFailed(check18.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
|
|
7566
7463
|
];
|
|
7567
7464
|
}
|
|
7568
|
-
return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(
|
|
7465
|
+
return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check18.id, {
|
|
7569
7466
|
severity: "error",
|
|
7570
7467
|
message,
|
|
7571
7468
|
file: ctx.pdfPath
|
|
7572
7469
|
}));
|
|
7573
7470
|
} catch {
|
|
7574
7471
|
return [
|
|
7575
|
-
inspectionFailed(
|
|
7472
|
+
inspectionFailed(check18.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
|
|
7576
7473
|
];
|
|
7577
7474
|
}
|
|
7578
7475
|
}
|
|
7579
7476
|
};
|
|
7580
|
-
registerCheck(
|
|
7477
|
+
registerCheck(check18);
|
|
7581
7478
|
|
|
7582
7479
|
// src/checks/source/htmlhint.ts
|
|
7583
7480
|
import { existsSync as existsSync9 } from "node:fs";
|
|
7584
|
-
import { readFile as
|
|
7481
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
7585
7482
|
import htmlhint from "htmlhint";
|
|
7586
7483
|
var { HTMLHint } = htmlhint;
|
|
7587
7484
|
var CONFIG_NAMES2 = [".htmlhintrc"];
|
|
7588
|
-
var
|
|
7485
|
+
var check19 = {
|
|
7589
7486
|
id: "source.htmlhint",
|
|
7590
7487
|
name: "HTMLHint",
|
|
7591
7488
|
description: "Runs htmlhint with project config to validate generated HTML",
|
|
@@ -7601,11 +7498,11 @@ var check17 = {
|
|
|
7601
7498
|
const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
|
|
7602
7499
|
if (!resolvedConfig && !configPath)
|
|
7603
7500
|
return [];
|
|
7604
|
-
const ruleset = resolvedConfig ? JSON.parse(await
|
|
7605
|
-
const html = await
|
|
7501
|
+
const ruleset = resolvedConfig ? JSON.parse(await readFile16(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
|
|
7502
|
+
const html = await readFile16(ctx.htmlPath, "utf8");
|
|
7606
7503
|
const messages = HTMLHint.verify(html, ruleset);
|
|
7607
7504
|
return messages.map((m) => ({
|
|
7608
|
-
checkId:
|
|
7505
|
+
checkId: check19.id,
|
|
7609
7506
|
severity: m.type === "error" ? "error" : "warning",
|
|
7610
7507
|
message: `${m.rule.id}: ${m.message}`,
|
|
7611
7508
|
file: ctx.htmlPath,
|
|
@@ -7614,13 +7511,35 @@ var check17 = {
|
|
|
7614
7511
|
}));
|
|
7615
7512
|
}
|
|
7616
7513
|
};
|
|
7617
|
-
registerCheck(
|
|
7514
|
+
registerCheck(check19);
|
|
7618
7515
|
|
|
7619
7516
|
// src/checks/source/stylelint.ts
|
|
7620
|
-
import { readFile as
|
|
7517
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
7621
7518
|
|
|
7622
7519
|
// src/lib/printsafe.ts
|
|
7623
7520
|
import postcss2 from "postcss";
|
|
7521
|
+
|
|
7522
|
+
// src/lib/lint-rules.ts
|
|
7523
|
+
function ruleLevel(rules, id) {
|
|
7524
|
+
const v = rules?.[id];
|
|
7525
|
+
if (v === "off" || v === false)
|
|
7526
|
+
return "off";
|
|
7527
|
+
if (v === "warn" || v === "warning")
|
|
7528
|
+
return "warn";
|
|
7529
|
+
if (v === "error")
|
|
7530
|
+
return "error";
|
|
7531
|
+
return;
|
|
7532
|
+
}
|
|
7533
|
+
var DIRECTIVE = /^\s*gutterpress-disable-next-line\s+(.*?)\s*$/;
|
|
7534
|
+
function parseDisableDirective(commentText) {
|
|
7535
|
+
const m = DIRECTIVE.exec(commentText);
|
|
7536
|
+
if (!m)
|
|
7537
|
+
return null;
|
|
7538
|
+
const ids = m[1].split(/\s--(?:\s|$)/)[0].split(/[\s,]+/).filter(Boolean);
|
|
7539
|
+
return ids.length > 0 ? ids : null;
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7542
|
+
// src/lib/printsafe.ts
|
|
7624
7543
|
var ruleRemoteUrls = "printsafe/no-remote-urls";
|
|
7625
7544
|
var ruleRiskyProps = "printsafe/no-risky-print-effects";
|
|
7626
7545
|
var ruleSyntax = "printsafe/syntax-error";
|
|
@@ -7645,9 +7564,11 @@ var riskyProperties = new Set([
|
|
|
7645
7564
|
"animation",
|
|
7646
7565
|
"animation-name",
|
|
7647
7566
|
"transition",
|
|
7648
|
-
"will-change"
|
|
7649
|
-
"clip-path"
|
|
7567
|
+
"will-change"
|
|
7650
7568
|
]);
|
|
7569
|
+
function isRasterizingClipPath(decl) {
|
|
7570
|
+
return decl.prop.toLowerCase() === "clip-path" && /\burl\(/i.test(decl.value);
|
|
7571
|
+
}
|
|
7651
7572
|
var marginBoxAtRuleNames = new Set([
|
|
7652
7573
|
"top-left-corner",
|
|
7653
7574
|
"top-left",
|
|
@@ -7774,7 +7695,34 @@ function nodeLoc(node) {
|
|
|
7774
7695
|
column: node.source?.start?.column ?? 1
|
|
7775
7696
|
};
|
|
7776
7697
|
}
|
|
7777
|
-
function checkCss(css, from) {
|
|
7698
|
+
function checkCss(css, from, options = {}) {
|
|
7699
|
+
return filterFindings(checkCssRaw(css, from), css, options.rules);
|
|
7700
|
+
}
|
|
7701
|
+
function filterFindings(findings, css, rules) {
|
|
7702
|
+
const silenced = new Map;
|
|
7703
|
+
for (const m of css.matchAll(/\/\*([\s\S]*?)\*\//g)) {
|
|
7704
|
+
const ids = parseDisableDirective(m[1]);
|
|
7705
|
+
if (!ids)
|
|
7706
|
+
continue;
|
|
7707
|
+
const endLine = css.slice(0, m.index + m[0].length).split(`
|
|
7708
|
+
`).length;
|
|
7709
|
+
const set = silenced.get(endLine + 1) ?? new Set;
|
|
7710
|
+
for (const id of ids)
|
|
7711
|
+
set.add(id);
|
|
7712
|
+
silenced.set(endLine + 1, set);
|
|
7713
|
+
}
|
|
7714
|
+
const out = [];
|
|
7715
|
+
for (const f of findings) {
|
|
7716
|
+
if (silenced.get(f.line)?.has(f.rule))
|
|
7717
|
+
continue;
|
|
7718
|
+
const level = ruleLevel(rules, f.rule);
|
|
7719
|
+
if (level === "off")
|
|
7720
|
+
continue;
|
|
7721
|
+
out.push(level === "warn" ? { ...f, severity: "warning" } : level === "error" ? { ...f, severity: "error" } : f);
|
|
7722
|
+
}
|
|
7723
|
+
return out;
|
|
7724
|
+
}
|
|
7725
|
+
function checkCssRaw(css, from) {
|
|
7778
7726
|
let root;
|
|
7779
7727
|
try {
|
|
7780
7728
|
root = postcss2.parse(css, from ? { from } : undefined);
|
|
@@ -7823,6 +7771,13 @@ function checkCss(css, from) {
|
|
|
7823
7771
|
message: `Property is high-risk for print/PDF: 'filter' rasterizes its subtree to a 300 DPI bitmap in the printed PDF (text becomes unselectable, unsearchable, and inaccessible), and it dominates build time (~90% measured; 57.0s -> 6.2s over 60pp when scoped — see ENGINE.md §10). Scope it to the smallest possible selector. It also means the render-parity gate cannot see a text-only change inside it (docs/render-parity-gate.md, "Known blind spot").`,
|
|
7824
7772
|
...nodeLoc(decl)
|
|
7825
7773
|
});
|
|
7774
|
+
} else if (isRasterizingClipPath(decl)) {
|
|
7775
|
+
warnings.push({
|
|
7776
|
+
rule: ruleRiskyProps,
|
|
7777
|
+
severity: "warning",
|
|
7778
|
+
message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop} with url(...) references an SVG mask. Use a basic shape (polygon(), inset(), circle(), ellipse(), path()) instead — those print as vector clips.`,
|
|
7779
|
+
...nodeLoc(decl)
|
|
7780
|
+
});
|
|
7826
7781
|
} else if (riskyProperties.has(prop)) {
|
|
7827
7782
|
warnings.push({
|
|
7828
7783
|
rule: ruleRiskyProps,
|
|
@@ -7876,7 +7831,7 @@ function checkCss(css, from) {
|
|
|
7876
7831
|
}
|
|
7877
7832
|
|
|
7878
7833
|
// src/checks/source/stylelint.ts
|
|
7879
|
-
var
|
|
7834
|
+
var check20 = {
|
|
7880
7835
|
id: "source.stylelint",
|
|
7881
7836
|
name: "Print-safety CSS",
|
|
7882
7837
|
description: "Checks CSS for print-safety issues (remote URLs, rasterizing effects, and page-layer containment)",
|
|
@@ -7893,19 +7848,19 @@ var check18 = {
|
|
|
7893
7848
|
for (const file of files) {
|
|
7894
7849
|
let css;
|
|
7895
7850
|
try {
|
|
7896
|
-
css = await
|
|
7851
|
+
css = await readFile17(file, "utf8");
|
|
7897
7852
|
} catch (err) {
|
|
7898
7853
|
results.push({
|
|
7899
|
-
checkId:
|
|
7854
|
+
checkId: check20.id,
|
|
7900
7855
|
severity: "error",
|
|
7901
7856
|
message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
|
|
7902
7857
|
file
|
|
7903
7858
|
});
|
|
7904
7859
|
continue;
|
|
7905
7860
|
}
|
|
7906
|
-
for (const w of checkCss(css, file)) {
|
|
7861
|
+
for (const w of checkCss(css, file, { rules: ctx.config.lint.rules })) {
|
|
7907
7862
|
results.push({
|
|
7908
|
-
checkId:
|
|
7863
|
+
checkId: check20.id,
|
|
7909
7864
|
severity: w.severity,
|
|
7910
7865
|
message: `${w.rule}: ${w.message}`,
|
|
7911
7866
|
file,
|
|
@@ -7917,15 +7872,15 @@ var check18 = {
|
|
|
7917
7872
|
return results;
|
|
7918
7873
|
}
|
|
7919
7874
|
};
|
|
7920
|
-
registerCheck(
|
|
7875
|
+
registerCheck(check20);
|
|
7921
7876
|
|
|
7922
7877
|
// src/checks/source/css-ownership.ts
|
|
7923
|
-
import { readFile as
|
|
7878
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
7924
7879
|
import postcss3 from "postcss";
|
|
7925
7880
|
import { parse as parseYaml3 } from "yaml";
|
|
7926
7881
|
var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
|
|
7927
7882
|
async function loadContract(configPath) {
|
|
7928
|
-
const raw = await
|
|
7883
|
+
const raw = await readFile18(configPath, "utf8");
|
|
7929
7884
|
const parsed = parseYaml3(raw);
|
|
7930
7885
|
if (parsed && typeof parsed === "object")
|
|
7931
7886
|
return parsed;
|
|
@@ -8038,7 +7993,7 @@ function checkOwnership(root, file, maps) {
|
|
|
8038
7993
|
});
|
|
8039
7994
|
return out;
|
|
8040
7995
|
}
|
|
8041
|
-
var
|
|
7996
|
+
var check21 = {
|
|
8042
7997
|
id: "source.css-ownership",
|
|
8043
7998
|
name: "CSS ownership contract",
|
|
8044
7999
|
description: "Checks an optional per-project CSS ownership contract (.gutterpress/css-contract.yaml): which file may declare which properties/at-rules, and which selectors must never appear unscoped.",
|
|
@@ -8061,7 +8016,7 @@ var check19 = {
|
|
|
8061
8016
|
} catch (err) {
|
|
8062
8017
|
return [
|
|
8063
8018
|
{
|
|
8064
|
-
checkId:
|
|
8019
|
+
checkId: check21.id,
|
|
8065
8020
|
severity: "error",
|
|
8066
8021
|
message: `Failed to parse CSS ownership contract "${configPath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
8067
8022
|
file: configPath
|
|
@@ -8076,7 +8031,7 @@ var check19 = {
|
|
|
8076
8031
|
continue;
|
|
8077
8032
|
let css;
|
|
8078
8033
|
try {
|
|
8079
|
-
css = await
|
|
8034
|
+
css = await readFile18(absFile, "utf8");
|
|
8080
8035
|
} catch {
|
|
8081
8036
|
continue;
|
|
8082
8037
|
}
|
|
@@ -8103,7 +8058,7 @@ var check19 = {
|
|
|
8103
8058
|
}
|
|
8104
8059
|
for (const v of violations) {
|
|
8105
8060
|
results.push({
|
|
8106
|
-
checkId:
|
|
8061
|
+
checkId: check21.id,
|
|
8107
8062
|
severity: "warning",
|
|
8108
8063
|
message: v.message,
|
|
8109
8064
|
file: absFile,
|
|
@@ -8115,14 +8070,14 @@ var check19 = {
|
|
|
8115
8070
|
return results;
|
|
8116
8071
|
}
|
|
8117
8072
|
};
|
|
8118
|
-
registerCheck(
|
|
8073
|
+
registerCheck(check21);
|
|
8119
8074
|
|
|
8120
8075
|
// src/checks/source/local-refs.ts
|
|
8121
8076
|
import { existsSync as existsSync10 } from "node:fs";
|
|
8122
8077
|
import { dirname as dirname3, resolve as resolve6 } from "node:path";
|
|
8123
8078
|
|
|
8124
8079
|
// src/checks/source/local-ref-parser.ts
|
|
8125
|
-
import { readFile as
|
|
8080
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
8126
8081
|
function newlineOffsets(content) {
|
|
8127
8082
|
const offsets = [];
|
|
8128
8083
|
for (let i = 0;i < content.length; i++)
|
|
@@ -8191,7 +8146,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
|
|
|
8191
8146
|
for (const file of files) {
|
|
8192
8147
|
let refs;
|
|
8193
8148
|
try {
|
|
8194
|
-
refs = collectRenderedLocalRefs(await
|
|
8149
|
+
refs = collectRenderedLocalRefs(await readFile19(file, "utf8"));
|
|
8195
8150
|
} catch {
|
|
8196
8151
|
results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
|
|
8197
8152
|
continue;
|
|
@@ -8202,7 +8157,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
|
|
|
8202
8157
|
}
|
|
8203
8158
|
|
|
8204
8159
|
// src/checks/source/local-refs.ts
|
|
8205
|
-
var
|
|
8160
|
+
var check22 = {
|
|
8206
8161
|
id: "source.links.local-refs",
|
|
8207
8162
|
name: "Local Markdown References",
|
|
8208
8163
|
description: "Checks local markdown links and image refs resolve on disk",
|
|
@@ -8210,12 +8165,12 @@ var check20 = {
|
|
|
8210
8165
|
phase: "pre-build",
|
|
8211
8166
|
async run(ctx) {
|
|
8212
8167
|
const results = [];
|
|
8213
|
-
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx,
|
|
8168
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check22.id, results)) {
|
|
8214
8169
|
if (isNonFilesystemRef(ref))
|
|
8215
8170
|
continue;
|
|
8216
8171
|
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
8217
8172
|
if (escape) {
|
|
8218
|
-
results.push(finding(
|
|
8173
|
+
results.push(finding(check22.id, {
|
|
8219
8174
|
severity: "error",
|
|
8220
8175
|
message: escape,
|
|
8221
8176
|
file,
|
|
@@ -8225,7 +8180,7 @@ var check20 = {
|
|
|
8225
8180
|
}
|
|
8226
8181
|
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
8227
8182
|
continue;
|
|
8228
|
-
results.push(finding(
|
|
8183
|
+
results.push(finding(check22.id, {
|
|
8229
8184
|
severity: kind === "image" ? "warning" : "error",
|
|
8230
8185
|
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
8231
8186
|
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
@@ -8250,7 +8205,7 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
|
|
|
8250
8205
|
return existsSync10(fileRelative);
|
|
8251
8206
|
}
|
|
8252
8207
|
}
|
|
8253
|
-
registerCheck(
|
|
8208
|
+
registerCheck(check22);
|
|
8254
8209
|
|
|
8255
8210
|
// src/lib/build-staging.ts
|
|
8256
8211
|
import path11 from "node:path";
|
|
@@ -8516,7 +8471,7 @@ async function createStageRoot() {
|
|
|
8516
8471
|
}
|
|
8517
8472
|
|
|
8518
8473
|
// src/checks/source/dangling-links.ts
|
|
8519
|
-
var
|
|
8474
|
+
var check23 = {
|
|
8520
8475
|
id: "source.links.dangling",
|
|
8521
8476
|
name: "Dangling Links",
|
|
8522
8477
|
description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
|
|
@@ -8524,10 +8479,10 @@ var check21 = {
|
|
|
8524
8479
|
phase: "pre-build",
|
|
8525
8480
|
async run(ctx) {
|
|
8526
8481
|
const results = [];
|
|
8527
|
-
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx,
|
|
8482
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check23.id, results)) {
|
|
8528
8483
|
if (kind !== "link" || isPrintResolvableHref(ref))
|
|
8529
8484
|
continue;
|
|
8530
|
-
results.push(finding(
|
|
8485
|
+
results.push(finding(check23.id, {
|
|
8531
8486
|
severity: "warning",
|
|
8532
8487
|
code: "dangling-link",
|
|
8533
8488
|
data: { ref },
|
|
@@ -8539,11 +8494,11 @@ var check21 = {
|
|
|
8539
8494
|
return results;
|
|
8540
8495
|
}
|
|
8541
8496
|
};
|
|
8542
|
-
registerCheck(
|
|
8497
|
+
registerCheck(check23);
|
|
8543
8498
|
|
|
8544
8499
|
// src/checks/source/accessibility-alt-text.ts
|
|
8545
|
-
import { readFile as
|
|
8546
|
-
var
|
|
8500
|
+
import { readFile as readFile20 } from "node:fs/promises";
|
|
8501
|
+
var check24 = {
|
|
8547
8502
|
id: "source.accessibility.alt-text",
|
|
8548
8503
|
name: "Image Alt Text",
|
|
8549
8504
|
description: "Checks markdown images include non-empty alt text",
|
|
@@ -8556,7 +8511,7 @@ var check22 = {
|
|
|
8556
8511
|
const results = [];
|
|
8557
8512
|
for (const file of files) {
|
|
8558
8513
|
try {
|
|
8559
|
-
const content = await
|
|
8514
|
+
const content = await readFile20(file, "utf8");
|
|
8560
8515
|
const lines = content.split(`
|
|
8561
8516
|
`);
|
|
8562
8517
|
let inFence = false;
|
|
@@ -8572,7 +8527,7 @@ var check22 = {
|
|
|
8572
8527
|
if (alt.trim())
|
|
8573
8528
|
continue;
|
|
8574
8529
|
results.push({
|
|
8575
|
-
checkId:
|
|
8530
|
+
checkId: check24.id,
|
|
8576
8531
|
severity: "warning",
|
|
8577
8532
|
message: "Image is missing alt text",
|
|
8578
8533
|
file,
|
|
@@ -8597,11 +8552,11 @@ function extractImageAlts(line) {
|
|
|
8597
8552
|
}
|
|
8598
8553
|
return alts;
|
|
8599
8554
|
}
|
|
8600
|
-
registerCheck(
|
|
8555
|
+
registerCheck(check24);
|
|
8601
8556
|
|
|
8602
8557
|
// src/checks/source/accessibility-heading-order.ts
|
|
8603
|
-
import { readFile as
|
|
8604
|
-
var
|
|
8558
|
+
import { readFile as readFile21 } from "node:fs/promises";
|
|
8559
|
+
var check25 = {
|
|
8605
8560
|
id: "source.accessibility.heading-order",
|
|
8606
8561
|
name: "Heading Order",
|
|
8607
8562
|
description: "Checks markdown heading levels do not jump by more than one",
|
|
@@ -8614,11 +8569,12 @@ var check23 = {
|
|
|
8614
8569
|
const results = [];
|
|
8615
8570
|
for (const file of files) {
|
|
8616
8571
|
try {
|
|
8617
|
-
const content = await
|
|
8572
|
+
const content = await readFile21(file, "utf8");
|
|
8618
8573
|
const lines = content.split(`
|
|
8619
8574
|
`);
|
|
8620
8575
|
let inFence = false;
|
|
8621
8576
|
let prevLevel;
|
|
8577
|
+
let silencedLine = -1;
|
|
8622
8578
|
for (let i = 0;i < lines.length; i++) {
|
|
8623
8579
|
const line = lines[i] ?? "";
|
|
8624
8580
|
if (/^\s*(```|~~~)/.test(line)) {
|
|
@@ -8627,13 +8583,18 @@ var check23 = {
|
|
|
8627
8583
|
}
|
|
8628
8584
|
if (inFence)
|
|
8629
8585
|
continue;
|
|
8586
|
+
const comment = /^\s*<!--(.*)-->\s*$/.exec(line);
|
|
8587
|
+
if (comment && parseDisableDirective(comment[1])?.includes(check25.id)) {
|
|
8588
|
+
silencedLine = i + 1;
|
|
8589
|
+
continue;
|
|
8590
|
+
}
|
|
8630
8591
|
const match = line.match(/^(#{1,6})\s+\S/);
|
|
8631
8592
|
if (!match)
|
|
8632
8593
|
continue;
|
|
8633
8594
|
const level = match[1].length;
|
|
8634
|
-
if (prevLevel != null && level > prevLevel + 1) {
|
|
8595
|
+
if (prevLevel != null && level > prevLevel + 1 && i !== silencedLine) {
|
|
8635
8596
|
results.push({
|
|
8636
|
-
checkId:
|
|
8597
|
+
checkId: check25.id,
|
|
8637
8598
|
severity: "warning",
|
|
8638
8599
|
message: `Heading level jump from h${prevLevel} to h${level}`,
|
|
8639
8600
|
file,
|
|
@@ -8647,11 +8608,11 @@ var check23 = {
|
|
|
8647
8608
|
return results;
|
|
8648
8609
|
}
|
|
8649
8610
|
};
|
|
8650
|
-
registerCheck(
|
|
8611
|
+
registerCheck(check25);
|
|
8651
8612
|
|
|
8652
8613
|
// src/checks/source/layout-markers.ts
|
|
8653
|
-
import { readFile as
|
|
8654
|
-
var
|
|
8614
|
+
import { readFile as readFile22 } from "node:fs/promises";
|
|
8615
|
+
var check26 = {
|
|
8655
8616
|
id: "source.markdown.layout-markers",
|
|
8656
8617
|
name: "Layout Markers",
|
|
8657
8618
|
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
|
|
@@ -8663,16 +8624,16 @@ var check24 = {
|
|
|
8663
8624
|
return [];
|
|
8664
8625
|
const results = [];
|
|
8665
8626
|
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
8666
|
-
results.push(inspectionFailed(
|
|
8627
|
+
results.push(inspectionFailed(check26.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
|
|
8667
8628
|
});
|
|
8668
8629
|
const md = createMarkdownRenderer(plugins);
|
|
8669
8630
|
for (const file of files) {
|
|
8670
8631
|
try {
|
|
8671
|
-
const content = await
|
|
8632
|
+
const content = await readFile22(file, "utf8");
|
|
8672
8633
|
const env = {};
|
|
8673
8634
|
md.render(content, env);
|
|
8674
8635
|
for (const w of env.layoutWarnings ?? []) {
|
|
8675
|
-
results.push(finding(
|
|
8636
|
+
results.push(finding(check26.id, {
|
|
8676
8637
|
severity: w.severity ?? "warning",
|
|
8677
8638
|
message: w.message,
|
|
8678
8639
|
file,
|
|
@@ -8681,16 +8642,16 @@ var check24 = {
|
|
|
8681
8642
|
}));
|
|
8682
8643
|
}
|
|
8683
8644
|
} catch (error2) {
|
|
8684
|
-
results.push(inspectionFailed(
|
|
8645
|
+
results.push(inspectionFailed(check26.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
|
|
8685
8646
|
}
|
|
8686
8647
|
}
|
|
8687
8648
|
return results;
|
|
8688
8649
|
}
|
|
8689
8650
|
};
|
|
8690
|
-
registerCheck(
|
|
8651
|
+
registerCheck(check26);
|
|
8691
8652
|
|
|
8692
8653
|
// src/checks/source/merge-markers.ts
|
|
8693
|
-
import { readFile as
|
|
8654
|
+
import { readFile as readFile23 } from "node:fs/promises";
|
|
8694
8655
|
import path12 from "node:path";
|
|
8695
8656
|
|
|
8696
8657
|
// src/checks/asset/extensions.ts
|
|
@@ -8720,7 +8681,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
|
|
|
8720
8681
|
function withoutOnlineTag(relPath) {
|
|
8721
8682
|
return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
|
|
8722
8683
|
}
|
|
8723
|
-
var
|
|
8684
|
+
var check27 = {
|
|
8724
8685
|
id: "source.sync.merge-markers",
|
|
8725
8686
|
name: "Combined Versions",
|
|
8726
8687
|
description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
|
|
@@ -8732,9 +8693,9 @@ var check25 = {
|
|
|
8732
8693
|
for (const file of textFiles) {
|
|
8733
8694
|
let content;
|
|
8734
8695
|
try {
|
|
8735
|
-
content = await
|
|
8696
|
+
content = await readFile23(file, "utf8");
|
|
8736
8697
|
} catch {
|
|
8737
|
-
results.push(inspectionFailed(
|
|
8698
|
+
results.push(inspectionFailed(check27.id, `Could not read source file: ${file}`, { file }));
|
|
8738
8699
|
continue;
|
|
8739
8700
|
}
|
|
8740
8701
|
if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
|
|
@@ -8746,7 +8707,7 @@ var check25 = {
|
|
|
8746
8707
|
const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
|
|
8747
8708
|
if (line === OPEN_SENTINEL) {
|
|
8748
8709
|
inBlock = true;
|
|
8749
|
-
results.push(finding(
|
|
8710
|
+
results.push(finding(check27.id, {
|
|
8750
8711
|
severity: "error",
|
|
8751
8712
|
code: "two-versions-passage",
|
|
8752
8713
|
message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
|
|
@@ -8755,7 +8716,7 @@ var check25 = {
|
|
|
8755
8716
|
}));
|
|
8756
8717
|
} else if (line === CLOSE_SENTINEL) {
|
|
8757
8718
|
if (!inBlock) {
|
|
8758
|
-
results.push(finding(
|
|
8719
|
+
results.push(finding(check27.id, {
|
|
8759
8720
|
severity: "error",
|
|
8760
8721
|
code: "leftover-version-marker",
|
|
8761
8722
|
message: "This marker line is left over from combining two versions — delete it.",
|
|
@@ -8776,7 +8737,7 @@ var check25 = {
|
|
|
8776
8737
|
});
|
|
8777
8738
|
for (const sibling of siblings.sort()) {
|
|
8778
8739
|
const rel = path12.relative(ctx.inputDir, sibling).split(path12.sep).join("/");
|
|
8779
|
-
results.push(finding(
|
|
8740
|
+
results.push(finding(check27.id, {
|
|
8780
8741
|
severity: "warning",
|
|
8781
8742
|
code: "kept-both-versions",
|
|
8782
8743
|
message: `Two versions of ${withoutOnlineTag(rel)} are in your book — keep the one you want, then delete the other.`,
|
|
@@ -8784,18 +8745,18 @@ var check25 = {
|
|
|
8784
8745
|
}));
|
|
8785
8746
|
}
|
|
8786
8747
|
} catch (error2) {
|
|
8787
|
-
results.push(inspectionFailed(
|
|
8748
|
+
results.push(inspectionFailed(check27.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
8788
8749
|
}
|
|
8789
8750
|
return results;
|
|
8790
8751
|
}
|
|
8791
8752
|
};
|
|
8792
|
-
registerCheck(
|
|
8753
|
+
registerCheck(check27);
|
|
8793
8754
|
|
|
8794
8755
|
// src/checks/asset/image-file-size.ts
|
|
8795
8756
|
import { stat as stat4 } from "node:fs/promises";
|
|
8796
8757
|
|
|
8797
8758
|
// src/lib/image-inspect.ts
|
|
8798
|
-
import { stat as stat3, readFile as
|
|
8759
|
+
import { stat as stat3, readFile as readFile24 } from "node:fs/promises";
|
|
8799
8760
|
var DEFAULT_DPI = 72;
|
|
8800
8761
|
var cache = new Map;
|
|
8801
8762
|
async function inspectImage(path13) {
|
|
@@ -8810,7 +8771,7 @@ async function inspectImage(path13) {
|
|
|
8810
8771
|
return hit.info;
|
|
8811
8772
|
let info2 = null;
|
|
8812
8773
|
try {
|
|
8813
|
-
const buf = await
|
|
8774
|
+
const buf = await readFile24(path13);
|
|
8814
8775
|
info2 = parseImage(buf);
|
|
8815
8776
|
} catch {
|
|
8816
8777
|
info2 = null;
|
|
@@ -9015,7 +8976,7 @@ async function collectImageFiles(dirs, exts, ignore2 = ASSET_SCAN_IGNORE_GLOBS)
|
|
|
9015
8976
|
}
|
|
9016
8977
|
|
|
9017
8978
|
// src/checks/asset/image-file-size.ts
|
|
9018
|
-
var
|
|
8979
|
+
var check28 = {
|
|
9019
8980
|
id: "asset.image.file-size",
|
|
9020
8981
|
name: "Image File Size",
|
|
9021
8982
|
description: "Checks that image files do not exceed the maximum size limit",
|
|
@@ -9036,14 +8997,14 @@ var check26 = {
|
|
|
9036
8997
|
if (info2.size > maxSize) {
|
|
9037
8998
|
const sizeMb = (info2.size / 1e6).toFixed(1);
|
|
9038
8999
|
const maxMb = (maxSize / 1e6).toFixed(1);
|
|
9039
|
-
results.push(finding(
|
|
9000
|
+
results.push(finding(check28.id, {
|
|
9040
9001
|
severity: "warning",
|
|
9041
9002
|
message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
|
|
9042
9003
|
file
|
|
9043
9004
|
}));
|
|
9044
9005
|
}
|
|
9045
9006
|
} catch {
|
|
9046
|
-
results.push(inspectionFailed(
|
|
9007
|
+
results.push(inspectionFailed(check28.id, `Could not stat image file: ${file}`, {
|
|
9047
9008
|
file
|
|
9048
9009
|
}));
|
|
9049
9010
|
}
|
|
@@ -9051,10 +9012,10 @@ var check26 = {
|
|
|
9051
9012
|
return results;
|
|
9052
9013
|
}
|
|
9053
9014
|
};
|
|
9054
|
-
registerCheck(
|
|
9015
|
+
registerCheck(check28);
|
|
9055
9016
|
|
|
9056
9017
|
// src/checks/asset/image-resolution.ts
|
|
9057
|
-
var
|
|
9018
|
+
var check29 = {
|
|
9058
9019
|
id: "asset.image.resolution",
|
|
9059
9020
|
name: "Image Resolution",
|
|
9060
9021
|
description: "Checks source image DPI from embedded density metadata",
|
|
@@ -9076,7 +9037,7 @@ var check27 = {
|
|
|
9076
9037
|
const { xDpi, yDpi } = info2;
|
|
9077
9038
|
if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
|
|
9078
9039
|
results.push({
|
|
9079
|
-
checkId:
|
|
9040
|
+
checkId: check29.id,
|
|
9080
9041
|
severity: "warning",
|
|
9081
9042
|
message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
|
|
9082
9043
|
file
|
|
@@ -9086,7 +9047,7 @@ var check27 = {
|
|
|
9086
9047
|
return results;
|
|
9087
9048
|
}
|
|
9088
9049
|
};
|
|
9089
|
-
registerCheck(
|
|
9050
|
+
registerCheck(check29);
|
|
9090
9051
|
|
|
9091
9052
|
// src/checks/asset/image-color-space.ts
|
|
9092
9053
|
var LABEL = {
|
|
@@ -9094,7 +9055,7 @@ var LABEL = {
|
|
|
9094
9055
|
gray: "Gray",
|
|
9095
9056
|
cmyk: "CMYK"
|
|
9096
9057
|
};
|
|
9097
|
-
var
|
|
9058
|
+
var check30 = {
|
|
9098
9059
|
id: "asset.image.color-space",
|
|
9099
9060
|
name: "Image Color Space",
|
|
9100
9061
|
description: "Validates image color spaces against allowed list",
|
|
@@ -9115,7 +9076,7 @@ var check28 = {
|
|
|
9115
9076
|
const cs = info2?.colorSpace;
|
|
9116
9077
|
if (cs && !allowedLower.has(cs)) {
|
|
9117
9078
|
results.push({
|
|
9118
|
-
checkId:
|
|
9079
|
+
checkId: check30.id,
|
|
9119
9080
|
severity: "warning",
|
|
9120
9081
|
message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
|
|
9121
9082
|
file
|
|
@@ -9125,10 +9086,10 @@ var check28 = {
|
|
|
9125
9086
|
return results;
|
|
9126
9087
|
}
|
|
9127
9088
|
};
|
|
9128
|
-
registerCheck(
|
|
9089
|
+
registerCheck(check30);
|
|
9129
9090
|
|
|
9130
9091
|
// src/checks/asset/image-alpha.ts
|
|
9131
|
-
var
|
|
9092
|
+
var check31 = {
|
|
9132
9093
|
id: "asset.image.alpha-channel",
|
|
9133
9094
|
name: "Image Alpha Channel",
|
|
9134
9095
|
description: "Checks for alpha channels in PNG/TIFF images",
|
|
@@ -9146,7 +9107,7 @@ var check29 = {
|
|
|
9146
9107
|
const info2 = await inspectImage(file);
|
|
9147
9108
|
if (info2?.hasAlpha) {
|
|
9148
9109
|
results.push({
|
|
9149
|
-
checkId:
|
|
9110
|
+
checkId: check31.id,
|
|
9150
9111
|
severity: "warning",
|
|
9151
9112
|
message: "Image contains alpha channel, which may cause print issues",
|
|
9152
9113
|
file
|
|
@@ -9156,10 +9117,10 @@ var check29 = {
|
|
|
9156
9117
|
return results;
|
|
9157
9118
|
}
|
|
9158
9119
|
};
|
|
9159
|
-
registerCheck(
|
|
9120
|
+
registerCheck(check31);
|
|
9160
9121
|
|
|
9161
9122
|
// src/checks/asset/image-tac.ts
|
|
9162
|
-
var
|
|
9123
|
+
var check32 = {
|
|
9163
9124
|
id: "asset.image.tac-raster",
|
|
9164
9125
|
name: "Image TAC (Raster)",
|
|
9165
9126
|
description: "Rasterizes and checks TAC per image using Ghostscript",
|
|
@@ -9178,7 +9139,7 @@ var check30 = {
|
|
|
9178
9139
|
const ghostscript = await resolveGhostscript();
|
|
9179
9140
|
if (!ghostscript) {
|
|
9180
9141
|
return [
|
|
9181
|
-
inspectionFailed(
|
|
9142
|
+
inspectionFailed(check32.id, "Could not inspect image ink coverage: Ghostscript executable not found")
|
|
9182
9143
|
];
|
|
9183
9144
|
}
|
|
9184
9145
|
for (const file of files) {
|
|
@@ -9196,7 +9157,7 @@ var check30 = {
|
|
|
9196
9157
|
if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
|
|
9197
9158
|
const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
|
|
9198
9159
|
if (tac > maxTac) {
|
|
9199
|
-
results.push(finding(
|
|
9160
|
+
results.push(finding(check32.id, {
|
|
9200
9161
|
severity: "warning",
|
|
9201
9162
|
message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
|
|
9202
9163
|
file,
|
|
@@ -9208,16 +9169,16 @@ var check30 = {
|
|
|
9208
9169
|
}
|
|
9209
9170
|
}
|
|
9210
9171
|
} catch {
|
|
9211
|
-
results.push(inspectionFailed(
|
|
9172
|
+
results.push(inspectionFailed(check32.id, `Could not inspect image ink coverage: ${file}`, { file }));
|
|
9212
9173
|
}
|
|
9213
9174
|
}
|
|
9214
9175
|
return results;
|
|
9215
9176
|
}
|
|
9216
9177
|
};
|
|
9217
|
-
registerCheck(
|
|
9178
|
+
registerCheck(check32);
|
|
9218
9179
|
|
|
9219
9180
|
// src/checks/asset/approved-fonts.ts
|
|
9220
|
-
var
|
|
9181
|
+
var check33 = {
|
|
9221
9182
|
id: "asset.font.approved-files",
|
|
9222
9183
|
name: "Approved Font Files",
|
|
9223
9184
|
description: "Checks font files against the approved file patterns",
|
|
@@ -9256,7 +9217,7 @@ var check31 = {
|
|
|
9256
9217
|
for (const font of allFonts) {
|
|
9257
9218
|
if (!approvedFonts.has(font)) {
|
|
9258
9219
|
results.push({
|
|
9259
|
-
checkId:
|
|
9220
|
+
checkId: check33.id,
|
|
9260
9221
|
severity: "warning",
|
|
9261
9222
|
message: "Font file not in approved list",
|
|
9262
9223
|
file: font
|
|
@@ -9266,7 +9227,7 @@ var check31 = {
|
|
|
9266
9227
|
return results;
|
|
9267
9228
|
}
|
|
9268
9229
|
};
|
|
9269
|
-
registerCheck(
|
|
9230
|
+
registerCheck(check33);
|
|
9270
9231
|
|
|
9271
9232
|
// src/checks/asset/font-license.ts
|
|
9272
9233
|
import { existsSync as existsSync11 } from "node:fs";
|
|
@@ -9281,7 +9242,7 @@ var LICENSE_NAMES = [
|
|
|
9281
9242
|
"OFL-1.1.txt",
|
|
9282
9243
|
"COPYING"
|
|
9283
9244
|
];
|
|
9284
|
-
var
|
|
9245
|
+
var check34 = {
|
|
9285
9246
|
id: "asset.font.license",
|
|
9286
9247
|
name: "Font License",
|
|
9287
9248
|
description: "Checks for license files in font directories",
|
|
@@ -9308,7 +9269,7 @@ var check32 = {
|
|
|
9308
9269
|
const hasLicense = LICENSE_NAMES.some((name) => existsSync11(resolve7(fontDir, name)));
|
|
9309
9270
|
if (!hasLicense) {
|
|
9310
9271
|
results.push({
|
|
9311
|
-
checkId:
|
|
9272
|
+
checkId: check34.id,
|
|
9312
9273
|
severity: "warning",
|
|
9313
9274
|
message: `No font license file found in directory`,
|
|
9314
9275
|
file: fontDir
|
|
@@ -9318,10 +9279,10 @@ var check32 = {
|
|
|
9318
9279
|
return results;
|
|
9319
9280
|
}
|
|
9320
9281
|
};
|
|
9321
|
-
registerCheck(
|
|
9282
|
+
registerCheck(check34);
|
|
9322
9283
|
|
|
9323
9284
|
// src/checks/heuristic/text-density.ts
|
|
9324
|
-
var
|
|
9285
|
+
var check35 = {
|
|
9325
9286
|
id: "heuristic.whitespace.text-density",
|
|
9326
9287
|
name: "Text Density",
|
|
9327
9288
|
description: "Checks characters-per-page ratio",
|
|
@@ -9350,7 +9311,7 @@ var check33 = {
|
|
|
9350
9311
|
});
|
|
9351
9312
|
if (lowPages.length > 0) {
|
|
9352
9313
|
results.push({
|
|
9353
|
-
checkId:
|
|
9314
|
+
checkId: check35.id,
|
|
9354
9315
|
severity: "info",
|
|
9355
9316
|
message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
|
|
9356
9317
|
file: ctx.pdfPath
|
|
@@ -9358,7 +9319,7 @@ var check33 = {
|
|
|
9358
9319
|
}
|
|
9359
9320
|
if (highPages.length > 0) {
|
|
9360
9321
|
results.push({
|
|
9361
|
-
checkId:
|
|
9322
|
+
checkId: check35.id,
|
|
9362
9323
|
severity: "info",
|
|
9363
9324
|
message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
|
|
9364
9325
|
file: ctx.pdfPath
|
|
@@ -9367,11 +9328,11 @@ var check33 = {
|
|
|
9367
9328
|
return results;
|
|
9368
9329
|
}
|
|
9369
9330
|
};
|
|
9370
|
-
registerCheck(
|
|
9331
|
+
registerCheck(check35);
|
|
9371
9332
|
|
|
9372
9333
|
// src/checks/heuristic/section-density.ts
|
|
9373
|
-
import { readFile as
|
|
9374
|
-
var
|
|
9334
|
+
import { readFile as readFile25 } from "node:fs/promises";
|
|
9335
|
+
var check36 = {
|
|
9375
9336
|
id: "heuristic.chunking.section-density",
|
|
9376
9337
|
name: "Section Density",
|
|
9377
9338
|
description: "Checks heading/paragraph/callout density from source Markdown",
|
|
@@ -9387,7 +9348,7 @@ var check34 = {
|
|
|
9387
9348
|
const results = [];
|
|
9388
9349
|
for (const file of files) {
|
|
9389
9350
|
try {
|
|
9390
|
-
const content = await
|
|
9351
|
+
const content = await readFile25(file, "utf8");
|
|
9391
9352
|
const lines = content.split(`
|
|
9392
9353
|
`);
|
|
9393
9354
|
let currentSectionStart = 0;
|
|
@@ -9397,7 +9358,7 @@ var check34 = {
|
|
|
9397
9358
|
const line = lines[i];
|
|
9398
9359
|
if (/^#{1,6}\s/.test(line)) {
|
|
9399
9360
|
if (paragraphCount > maxParas) {
|
|
9400
|
-
results.push(finding(
|
|
9361
|
+
results.push(finding(check36.id, {
|
|
9401
9362
|
severity: "info",
|
|
9402
9363
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
9403
9364
|
file,
|
|
@@ -9417,7 +9378,7 @@ var check34 = {
|
|
|
9417
9378
|
}
|
|
9418
9379
|
}
|
|
9419
9380
|
if (paragraphCount > maxParas) {
|
|
9420
|
-
results.push(finding(
|
|
9381
|
+
results.push(finding(check36.id, {
|
|
9421
9382
|
severity: "info",
|
|
9422
9383
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
9423
9384
|
file,
|
|
@@ -9425,7 +9386,7 @@ var check34 = {
|
|
|
9425
9386
|
}));
|
|
9426
9387
|
}
|
|
9427
9388
|
} catch {
|
|
9428
|
-
results.push(inspectionFailed(
|
|
9389
|
+
results.push(inspectionFailed(check36.id, `Could not read source file: ${file}`, {
|
|
9429
9390
|
file
|
|
9430
9391
|
}));
|
|
9431
9392
|
}
|
|
@@ -9433,10 +9394,10 @@ var check34 = {
|
|
|
9433
9394
|
return results;
|
|
9434
9395
|
}
|
|
9435
9396
|
};
|
|
9436
|
-
registerCheck(
|
|
9397
|
+
registerCheck(check36);
|
|
9437
9398
|
|
|
9438
9399
|
// src/checks/heuristic/layer-count.ts
|
|
9439
|
-
var
|
|
9400
|
+
var check37 = {
|
|
9440
9401
|
id: "heuristic.decoration.layer-count",
|
|
9441
9402
|
name: "Layer Count",
|
|
9442
9403
|
description: "Counts image objects per page",
|
|
@@ -9461,7 +9422,7 @@ var check35 = {
|
|
|
9461
9422
|
heavyPages.sort((a, b) => a - b);
|
|
9462
9423
|
return [
|
|
9463
9424
|
{
|
|
9464
|
-
checkId:
|
|
9425
|
+
checkId: check37.id,
|
|
9465
9426
|
severity: "info",
|
|
9466
9427
|
message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
|
|
9467
9428
|
file: ctx.pdfPath
|
|
@@ -9471,10 +9432,10 @@ var check35 = {
|
|
|
9471
9432
|
return [];
|
|
9472
9433
|
}
|
|
9473
9434
|
};
|
|
9474
|
-
registerCheck(
|
|
9435
|
+
registerCheck(check37);
|
|
9475
9436
|
|
|
9476
9437
|
// src/checks/heuristic/placement-variance.ts
|
|
9477
|
-
var
|
|
9438
|
+
var check38 = {
|
|
9478
9439
|
id: "heuristic.layout.placement-variance",
|
|
9479
9440
|
name: "Placement Variance",
|
|
9480
9441
|
description: "Analyzes text baseline coordinates for layout consistency",
|
|
@@ -9492,7 +9453,7 @@ var check36 = {
|
|
|
9492
9453
|
const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
|
|
9493
9454
|
return [
|
|
9494
9455
|
{
|
|
9495
|
-
checkId:
|
|
9456
|
+
checkId: check38.id,
|
|
9496
9457
|
severity: "info",
|
|
9497
9458
|
message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
|
|
9498
9459
|
file: ctx.pdfPath
|
|
@@ -9500,7 +9461,7 @@ var check36 = {
|
|
|
9500
9461
|
];
|
|
9501
9462
|
}
|
|
9502
9463
|
};
|
|
9503
|
-
registerCheck(
|
|
9464
|
+
registerCheck(check38);
|
|
9504
9465
|
|
|
9505
9466
|
// src/checks/runner.ts
|
|
9506
9467
|
function getCheckSeverityOverride(checkId, config) {
|
|
@@ -9530,25 +9491,32 @@ async function runChecks(ctx, opts = {}) {
|
|
|
9530
9491
|
}
|
|
9531
9492
|
const release = retainPdfCache();
|
|
9532
9493
|
try {
|
|
9533
|
-
for (const
|
|
9494
|
+
for (const check39 of checks2) {
|
|
9534
9495
|
try {
|
|
9535
|
-
|
|
9536
|
-
const severityOverride = getCheckSeverityOverride(
|
|
9496
|
+
let results = await check39.run(ctx);
|
|
9497
|
+
const severityOverride = getCheckSeverityOverride(check39.id, ctx.config);
|
|
9537
9498
|
if (severityOverride) {
|
|
9538
9499
|
for (const r of results) {
|
|
9539
9500
|
r.severity = severityOverride;
|
|
9540
9501
|
}
|
|
9541
9502
|
}
|
|
9503
|
+
const lintLevel = ruleLevel(ctx.config.lint.rules, check39.id);
|
|
9504
|
+
if (lintLevel === "off")
|
|
9505
|
+
results = [];
|
|
9506
|
+
else if (lintLevel) {
|
|
9507
|
+
for (const r of results)
|
|
9508
|
+
r.severity = lintLevel === "warn" ? "warning" : "error";
|
|
9509
|
+
}
|
|
9542
9510
|
if (results.length === 0) {
|
|
9543
|
-
passed.push(
|
|
9511
|
+
passed.push(check39.id);
|
|
9544
9512
|
} else {
|
|
9545
9513
|
allResults.push(...results);
|
|
9546
9514
|
}
|
|
9547
9515
|
} catch (err) {
|
|
9548
9516
|
allResults.push({
|
|
9549
|
-
checkId:
|
|
9517
|
+
checkId: check39.id,
|
|
9550
9518
|
severity: "error",
|
|
9551
|
-
message: `Check "${
|
|
9519
|
+
message: `Check "${check39.id}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
9552
9520
|
});
|
|
9553
9521
|
}
|
|
9554
9522
|
}
|
|
@@ -9588,15 +9556,15 @@ function emptyReport() {
|
|
|
9588
9556
|
async function checkToolAvailability(config, opts = {}) {
|
|
9589
9557
|
const { checks: checks2 } = selectChecks(opts, config);
|
|
9590
9558
|
const toolToChecks = new Map;
|
|
9591
|
-
for (const
|
|
9592
|
-
if (!
|
|
9559
|
+
for (const check39 of checks2) {
|
|
9560
|
+
if (!check39.requiredTools?.length)
|
|
9593
9561
|
continue;
|
|
9594
|
-
for (const tool of
|
|
9562
|
+
for (const tool of check39.requiredTools) {
|
|
9595
9563
|
const existing = toolToChecks.get(tool);
|
|
9596
9564
|
if (existing) {
|
|
9597
|
-
existing.push(
|
|
9565
|
+
existing.push(check39.id);
|
|
9598
9566
|
} else {
|
|
9599
|
-
toolToChecks.set(tool, [
|
|
9567
|
+
toolToChecks.set(tool, [check39.id]);
|
|
9600
9568
|
}
|
|
9601
9569
|
}
|
|
9602
9570
|
}
|
|
@@ -9612,11 +9580,11 @@ async function checkToolAvailability(config, opts = {}) {
|
|
|
9612
9580
|
const missing = results.filter((r) => !r.found).map((r) => r.tool);
|
|
9613
9581
|
const missingSet = new Set(missing);
|
|
9614
9582
|
const skippedChecks = [];
|
|
9615
|
-
for (const
|
|
9616
|
-
if (!
|
|
9583
|
+
for (const check39 of checks2) {
|
|
9584
|
+
if (!check39.requiredTools?.length)
|
|
9617
9585
|
continue;
|
|
9618
|
-
if (
|
|
9619
|
-
skippedChecks.push(
|
|
9586
|
+
if (check39.requiredTools.some((t) => missingSet.has(t))) {
|
|
9587
|
+
skippedChecks.push(check39.id);
|
|
9620
9588
|
}
|
|
9621
9589
|
}
|
|
9622
9590
|
return { available, missing, skippedChecks, toolToChecks };
|
|
@@ -9889,7 +9857,7 @@ async function executeValidation(args) {
|
|
|
9889
9857
|
};
|
|
9890
9858
|
if (args.fix) {
|
|
9891
9859
|
const selected = selectChecks(runnerOptions, config).checks;
|
|
9892
|
-
if (selected.some((
|
|
9860
|
+
if (selected.some((check39) => check39.id === MARKDOWNLINT_CHECK_ID)) {
|
|
9893
9861
|
await applyMarkdownlintFixes(context);
|
|
9894
9862
|
} else {
|
|
9895
9863
|
log.warn(`--fix: ${MARKDOWNLINT_CHECK_ID} is not part of this run — no files were fixed.`);
|
|
@@ -10241,7 +10209,8 @@ class HtmlOutput {
|
|
|
10241
10209
|
requestedFlavor: null,
|
|
10242
10210
|
resolvedFlavor: config.pdfx.flavor,
|
|
10243
10211
|
iccPath: null,
|
|
10244
|
-
stripAnnotations: null
|
|
10212
|
+
stripAnnotations: null,
|
|
10213
|
+
blackText: null
|
|
10245
10214
|
}
|
|
10246
10215
|
}, `Wrote: ${path13.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
|
|
10247
10216
|
}
|
|
@@ -10261,12 +10230,12 @@ class PdfOutput {
|
|
|
10261
10230
|
const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
|
|
10262
10231
|
await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
|
|
10263
10232
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
10264
|
-
const { buildNativePdf } = await import("./engine-
|
|
10233
|
+
const { buildNativePdf } = await import("./engine-3xqgm82d.js");
|
|
10265
10234
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
10266
10235
|
title: config.title,
|
|
10267
10236
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
10268
10237
|
signature: config.print.signature,
|
|
10269
|
-
|
|
10238
|
+
lastPage: lastPageMode(config.print),
|
|
10270
10239
|
allowShrink: opts.allowShrink
|
|
10271
10240
|
}, browser);
|
|
10272
10241
|
for (const d of engineDiagnostics)
|
|
@@ -10325,7 +10294,8 @@ class PdfOutput {
|
|
|
10325
10294
|
requestedFlavor: pdfxMode ?? null,
|
|
10326
10295
|
resolvedFlavor: config.pdfx.flavor,
|
|
10327
10296
|
iccPath: effectiveIccPath,
|
|
10328
|
-
stripAnnotations: shouldStripAnnotations
|
|
10297
|
+
stripAnnotations: shouldStripAnnotations,
|
|
10298
|
+
blackText: pdfxMode ? config.pdfx.blackText : null
|
|
10329
10299
|
}
|
|
10330
10300
|
}, `Wrote: ${reportedPdf}`, { htmlPath: htmlFile, pdfPath: reportedPdf }, pdfName, engineDiagnostics);
|
|
10331
10301
|
} finally {
|
|
@@ -10366,7 +10336,7 @@ async function runBuild(opts) {
|
|
|
10366
10336
|
import path16 from "path";
|
|
10367
10337
|
import { tmpdir } from "os";
|
|
10368
10338
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
10369
|
-
import { readdir as readdir5, readFile as
|
|
10339
|
+
import { readdir as readdir5, readFile as readFile26, writeFile as writeFile6 } from "node:fs/promises";
|
|
10370
10340
|
|
|
10371
10341
|
// src/utils/file-utils.ts
|
|
10372
10342
|
import { promises as fs3 } from "fs";
|
|
@@ -10915,7 +10885,7 @@ async function cleanupOrphanTempDirs() {
|
|
|
10915
10885
|
const dir = path16.join(TEMP_DIR_BASE, entry);
|
|
10916
10886
|
const pidFile = path16.join(dir, PID_FILE_NAME);
|
|
10917
10887
|
try {
|
|
10918
|
-
const raw = await
|
|
10888
|
+
const raw = await readFile26(pidFile, "utf8");
|
|
10919
10889
|
const pid = parseInt(raw.trim(), 10);
|
|
10920
10890
|
if (!isProcessAlive(pid)) {
|
|
10921
10891
|
debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
|
|
@@ -11022,7 +10992,7 @@ function createServerState(inputPath, tempDir, config, options) {
|
|
|
11022
10992
|
import http from "node:http";
|
|
11023
10993
|
import net from "node:net";
|
|
11024
10994
|
import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
|
|
11025
|
-
import { readFile as
|
|
10995
|
+
import { readFile as readFile27, stat as stat5 } from "node:fs/promises";
|
|
11026
10996
|
import path17 from "path";
|
|
11027
10997
|
import { WebSocket, WebSocketServer } from "ws";
|
|
11028
10998
|
|
|
@@ -11121,7 +11091,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
|
|
|
11121
11091
|
}
|
|
11122
11092
|
let data;
|
|
11123
11093
|
try {
|
|
11124
|
-
data = await
|
|
11094
|
+
data = await readFile27(filePath);
|
|
11125
11095
|
} catch {
|
|
11126
11096
|
res.writeHead(404);
|
|
11127
11097
|
res.end("Not Found");
|
|
@@ -11170,7 +11140,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
|
|
|
11170
11140
|
let data;
|
|
11171
11141
|
if (!validator || validator.stamp !== stamp) {
|
|
11172
11142
|
try {
|
|
11173
|
-
data = await
|
|
11143
|
+
data = await readFile27(absPath);
|
|
11174
11144
|
} catch {
|
|
11175
11145
|
await serveStatic(absPath, res);
|
|
11176
11146
|
return;
|
|
@@ -11482,7 +11452,7 @@ async function startPreviewServer(options) {
|
|
|
11482
11452
|
import { platform, arch, release } from "node:os";
|
|
11483
11453
|
|
|
11484
11454
|
// src/lib/remote-auth/token-store.ts
|
|
11485
|
-
import { chmod, mkdir as mkdir7, readFile as
|
|
11455
|
+
import { chmod, mkdir as mkdir7, readFile as readFile28, rename as rename4, writeFile as writeFile7 } from "node:fs/promises";
|
|
11486
11456
|
import os3 from "node:os";
|
|
11487
11457
|
import path18 from "node:path";
|
|
11488
11458
|
function credentialHostKey(hostOrUrl) {
|
|
@@ -11526,7 +11496,7 @@ class FileTokenStore {
|
|
|
11526
11496
|
async read() {
|
|
11527
11497
|
let raw;
|
|
11528
11498
|
try {
|
|
11529
|
-
raw = await
|
|
11499
|
+
raw = await readFile28(this.filePath, "utf8");
|
|
11530
11500
|
} catch (e) {
|
|
11531
11501
|
if (e?.code === "ENOENT") {
|
|
11532
11502
|
return { version: 1, credentials: {} };
|
|
@@ -11726,9 +11696,169 @@ async function getSystemDiagnostics() {
|
|
|
11726
11696
|
};
|
|
11727
11697
|
}
|
|
11728
11698
|
// src/lib/project-scaffold.ts
|
|
11729
|
-
import { access, copyFile, cp as cp2, mkdir as
|
|
11730
|
-
import { constants as FS, existsSync as
|
|
11699
|
+
import { access, copyFile, cp as cp2, mkdir as mkdir9, readdir as readdir6, readFile as readFile30, stat as stat6, writeFile as writeFile9 } from "node:fs/promises";
|
|
11700
|
+
import { constants as FS, existsSync as existsSync14 } from "node:fs";
|
|
11701
|
+
import path20 from "node:path";
|
|
11702
|
+
|
|
11703
|
+
// src/lib/page-size.ts
|
|
11704
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
11705
|
+
import { mkdir as mkdir8, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
|
|
11731
11706
|
import path19 from "node:path";
|
|
11707
|
+
import postcss4 from "postcss";
|
|
11708
|
+
import { Scalar as Scalar2 } from "yaml";
|
|
11709
|
+
function isPositivePoints(value) {
|
|
11710
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11711
|
+
}
|
|
11712
|
+
function writePageBounds(doc, page) {
|
|
11713
|
+
const out = { width: page.width, height: page.height };
|
|
11714
|
+
const tolerance = page.tolerance ?? numberAt(doc, ["page", "tolerance"]);
|
|
11715
|
+
if (tolerance !== undefined)
|
|
11716
|
+
out.tolerance = tolerance;
|
|
11717
|
+
doc.set("page", doc.createNode(out));
|
|
11718
|
+
}
|
|
11719
|
+
function numberAt(doc, keyPath) {
|
|
11720
|
+
const v = doc.getIn(keyPath);
|
|
11721
|
+
return isPositivePoints(v) ? v : undefined;
|
|
11722
|
+
}
|
|
11723
|
+
function pageSizeForChoice(choice) {
|
|
11724
|
+
if (!PRESET_IDS.includes(choice.preset)) {
|
|
11725
|
+
throw new UsageError(`Unknown preset "${choice.preset}". Choose one of: ${PRESET_IDS.join(", ")}.`);
|
|
11726
|
+
}
|
|
11727
|
+
if (choice.preset === "custom") {
|
|
11728
|
+
const { page } = choice;
|
|
11729
|
+
if (!page || !isPositivePoints(page.width) || !isPositivePoints(page.height)) {
|
|
11730
|
+
throw new UsageError("A custom page size needs a positive width and height in points (72pt = 1in).");
|
|
11731
|
+
}
|
|
11732
|
+
return { width: page.width, height: page.height };
|
|
11733
|
+
}
|
|
11734
|
+
const base = PRESETS[choice.preset].page;
|
|
11735
|
+
return { width: base.width, height: base.height };
|
|
11736
|
+
}
|
|
11737
|
+
var round3 = (n) => Math.round(n * 1000) / 1000;
|
|
11738
|
+
function cssLength(pt) {
|
|
11739
|
+
const inches = round3(pt / 72);
|
|
11740
|
+
return Math.abs(inches * 72 - pt) < 0.000001 ? `${inches}in` : `${round3(pt)}pt`;
|
|
11741
|
+
}
|
|
11742
|
+
function formatPageSize(page) {
|
|
11743
|
+
return `${cssLength(page.width)} ${cssLength(page.height)}`;
|
|
11744
|
+
}
|
|
11745
|
+
var SAME_SIZE_PT = 0.5;
|
|
11746
|
+
function sameSize(a, b) {
|
|
11747
|
+
return Math.abs(a.width - b.width) < SAME_SIZE_PT && Math.abs(a.height - b.height) < SAME_SIZE_PT;
|
|
11748
|
+
}
|
|
11749
|
+
function plainPageRules(root) {
|
|
11750
|
+
const rules = [];
|
|
11751
|
+
root.walkAtRules("page", (rule) => {
|
|
11752
|
+
if (rule.params.trim() === "")
|
|
11753
|
+
rules.push(rule);
|
|
11754
|
+
});
|
|
11755
|
+
return rules;
|
|
11756
|
+
}
|
|
11757
|
+
function sizeDeclsOf(rule) {
|
|
11758
|
+
const decls = [];
|
|
11759
|
+
rule.each((node) => {
|
|
11760
|
+
if (node.type === "decl" && node.prop.toLowerCase() === "size")
|
|
11761
|
+
decls.push(node);
|
|
11762
|
+
});
|
|
11763
|
+
return decls;
|
|
11764
|
+
}
|
|
11765
|
+
function readPageSizeFromCss(css) {
|
|
11766
|
+
let root;
|
|
11767
|
+
try {
|
|
11768
|
+
root = postcss4.parse(css);
|
|
11769
|
+
} catch {
|
|
11770
|
+
return null;
|
|
11771
|
+
}
|
|
11772
|
+
const decls = plainPageRules(root).flatMap(sizeDeclsOf);
|
|
11773
|
+
const last = decls[decls.length - 1];
|
|
11774
|
+
return last ? parseSize(last.value) ?? null : null;
|
|
11775
|
+
}
|
|
11776
|
+
function setPageSizeInCss(css, page) {
|
|
11777
|
+
const root = postcss4.parse(css);
|
|
11778
|
+
const decls = plainPageRules(root).flatMap(sizeDeclsOf);
|
|
11779
|
+
const last = decls[decls.length - 1];
|
|
11780
|
+
const value = formatPageSize(page);
|
|
11781
|
+
if (last) {
|
|
11782
|
+
const current = parseSize(last.value);
|
|
11783
|
+
if (current && sameSize(current, page))
|
|
11784
|
+
return css;
|
|
11785
|
+
last.value = value;
|
|
11786
|
+
return root.toString();
|
|
11787
|
+
}
|
|
11788
|
+
const rule = `@page {
|
|
11789
|
+
size: ${value};
|
|
11790
|
+
}
|
|
11791
|
+
`;
|
|
11792
|
+
if (css.trim() === "")
|
|
11793
|
+
return css + rule;
|
|
11794
|
+
return `${css}${css.endsWith(`
|
|
11795
|
+
`) ? "" : `
|
|
11796
|
+
`}
|
|
11797
|
+
${rule}`;
|
|
11798
|
+
}
|
|
11799
|
+
async function ownStylesheets(projectDir) {
|
|
11800
|
+
const rels = await resolveActiveStyles(projectDir).catch(() => []);
|
|
11801
|
+
return rels.map((rel) => path19.resolve(projectDir, rel)).filter((abs) => {
|
|
11802
|
+
const inside = path19.relative(projectDir, abs);
|
|
11803
|
+
return !inside.startsWith("..") && !path19.isAbsolute(inside) && existsSync13(abs);
|
|
11804
|
+
});
|
|
11805
|
+
}
|
|
11806
|
+
var relDisplay2 = (projectDir, abs) => path19.relative(projectDir, abs).split(path19.sep).join("/");
|
|
11807
|
+
async function findCssSize(projectDir, sheets) {
|
|
11808
|
+
let found = null;
|
|
11809
|
+
for (const file of sheets) {
|
|
11810
|
+
const size = readPageSizeFromCss(await readFile29(file, "utf8"));
|
|
11811
|
+
if (size)
|
|
11812
|
+
found = { file, size };
|
|
11813
|
+
}
|
|
11814
|
+
return found;
|
|
11815
|
+
}
|
|
11816
|
+
async function readPageSetup(projectDir) {
|
|
11817
|
+
const { doc } = await loadManifestDoc(projectDir);
|
|
11818
|
+
const declared = doc.get("preset");
|
|
11819
|
+
const preset = declared === undefined || declared === null ? "dtrpg" : PRESET_IDS.includes(String(declared)) ? declared : null;
|
|
11820
|
+
const base = preset ? PRESETS[preset].page : null;
|
|
11821
|
+
const width = numberAt(doc, ["page", "width"]) ?? base?.width;
|
|
11822
|
+
const height = numberAt(doc, ["page", "height"]) ?? base?.height;
|
|
11823
|
+
const tolerance = numberAt(doc, ["page", "tolerance"]) ?? base?.tolerance ?? 0.5;
|
|
11824
|
+
const found = await findCssSize(projectDir, await ownStylesheets(projectDir));
|
|
11825
|
+
return {
|
|
11826
|
+
preset,
|
|
11827
|
+
bounds: width !== undefined && height !== undefined ? { width, height, tolerance } : null,
|
|
11828
|
+
css: found ? { ...found.size, file: relDisplay2(projectDir, found.file) } : null
|
|
11829
|
+
};
|
|
11830
|
+
}
|
|
11831
|
+
async function setPageSetup(projectDir, choice) {
|
|
11832
|
+
const size = pageSizeForChoice(choice);
|
|
11833
|
+
const sheets = await ownStylesheets(projectDir);
|
|
11834
|
+
const found = await findCssSize(projectDir, sheets);
|
|
11835
|
+
const newSheet = path19.join(projectDir, "styles", "book.css");
|
|
11836
|
+
const target = found?.file ?? sheets[sheets.length - 1] ?? newSheet;
|
|
11837
|
+
const before = existsSync13(target) ? await readFile29(target, "utf8") : "";
|
|
11838
|
+
let after;
|
|
11839
|
+
try {
|
|
11840
|
+
after = setPageSizeInCss(before, size);
|
|
11841
|
+
} catch (e) {
|
|
11842
|
+
throw new UsageError(`Could not update the page size: ${relDisplay2(projectDir, target)} has a CSS error ` + `(${e instanceof Error ? e.message : String(e)}). Fix it, then try again.`);
|
|
11843
|
+
}
|
|
11844
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11845
|
+
doc.set("preset", choice.preset);
|
|
11846
|
+
if (choice.preset === "custom")
|
|
11847
|
+
writePageBounds(doc, size);
|
|
11848
|
+
else
|
|
11849
|
+
doc.delete("page");
|
|
11850
|
+
if (target === newSheet && !sheets.includes(newSheet)) {
|
|
11851
|
+
ensureSeq(doc, "styles").add(new Scalar2("styles/book.css"));
|
|
11852
|
+
}
|
|
11853
|
+
await writeManifestDoc(file, doc);
|
|
11854
|
+
if (after !== before || !existsSync13(target)) {
|
|
11855
|
+
await mkdir8(path19.dirname(target), { recursive: true });
|
|
11856
|
+
await writeFile8(target, after, "utf8");
|
|
11857
|
+
}
|
|
11858
|
+
return readPageSetup(projectDir);
|
|
11859
|
+
}
|
|
11860
|
+
|
|
11861
|
+
// src/lib/project-scaffold.ts
|
|
11732
11862
|
function starterOverridesCss() {
|
|
11733
11863
|
return `/**
|
|
11734
11864
|
* Your book's own styles.
|
|
@@ -11757,9 +11887,6 @@ function escapeYamlScalar(value) {
|
|
|
11757
11887
|
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
11758
11888
|
}
|
|
11759
11889
|
var DEFAULT_AUTHOR = "Anonymous";
|
|
11760
|
-
function isPositivePoints(value) {
|
|
11761
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11762
|
-
}
|
|
11763
11890
|
function requirePreset(preset, customPage) {
|
|
11764
11891
|
if (!preset || !PRESET_IDS.includes(preset)) {
|
|
11765
11892
|
const got = preset ? ` (got "${preset}")` : "";
|
|
@@ -11801,7 +11928,7 @@ async function scaffoldProject(options) {
|
|
|
11801
11928
|
throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
11802
11929
|
}
|
|
11803
11930
|
const parentDir = options.parentDir;
|
|
11804
|
-
if (!parentDir || !
|
|
11931
|
+
if (!parentDir || !path20.isAbsolute(parentDir)) {
|
|
11805
11932
|
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
11806
11933
|
}
|
|
11807
11934
|
try {
|
|
@@ -11809,7 +11936,7 @@ async function scaffoldProject(options) {
|
|
|
11809
11936
|
} catch {
|
|
11810
11937
|
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
11811
11938
|
}
|
|
11812
|
-
const projectDir =
|
|
11939
|
+
const projectDir = path20.join(parentDir, folderName);
|
|
11813
11940
|
let targetExists = true;
|
|
11814
11941
|
try {
|
|
11815
11942
|
await access(projectDir, FS.F_OK);
|
|
@@ -11824,23 +11951,23 @@ async function scaffoldProject(options) {
|
|
|
11824
11951
|
const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
|
|
11825
11952
|
const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
|
|
11826
11953
|
try {
|
|
11827
|
-
await
|
|
11954
|
+
await mkdir9(projectDir, { recursive: true });
|
|
11828
11955
|
if (customTemplateDir) {
|
|
11829
11956
|
const entries = await readdir6(customTemplateDir, { withFileTypes: true });
|
|
11830
11957
|
for (const entry of entries) {
|
|
11831
11958
|
if (entry.name === ".gutterpress-template.json")
|
|
11832
11959
|
continue;
|
|
11833
|
-
await cp2(
|
|
11960
|
+
await cp2(path20.join(customTemplateDir, entry.name), path20.join(projectDir, entry.name), { recursive: true });
|
|
11834
11961
|
}
|
|
11835
|
-
await
|
|
11962
|
+
await mkdir9(path20.join(projectDir, "assets"), { recursive: true });
|
|
11836
11963
|
} else {
|
|
11837
11964
|
const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
|
|
11838
11965
|
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11839
|
-
await
|
|
11840
|
-
await copyFile(tplManifest,
|
|
11841
|
-
await copyFile(tplChapter,
|
|
11842
|
-
await
|
|
11843
|
-
await
|
|
11966
|
+
await mkdir9(path20.join(projectDir, "assets"), { recursive: true });
|
|
11967
|
+
await copyFile(tplManifest, path20.join(projectDir, MANIFEST_FILENAMES[0]));
|
|
11968
|
+
await copyFile(tplChapter, path20.join(projectDir, "chapter-01.md"));
|
|
11969
|
+
await mkdir9(path20.join(projectDir, "styles"), { recursive: true });
|
|
11970
|
+
await writeFile9(path20.join(projectDir, "styles", "book.css"), starterOverridesCss(), "utf8");
|
|
11844
11971
|
}
|
|
11845
11972
|
} catch (e) {
|
|
11846
11973
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not create the book files: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -11850,34 +11977,26 @@ async function scaffoldProject(options) {
|
|
|
11850
11977
|
} catch (e) {
|
|
11851
11978
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
|
|
11852
11979
|
}
|
|
11853
|
-
const manifestPath = MANIFEST_FILENAMES.map((name2) =>
|
|
11980
|
+
const manifestPath = MANIFEST_FILENAMES.map((name2) => path20.join(projectDir, name2)).find(existsSync14) ?? path20.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
11854
11981
|
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11855
11982
|
const substitutions = {
|
|
11856
11983
|
"{{TITLE}}": escapeYamlScalar(name),
|
|
11857
11984
|
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
11858
11985
|
};
|
|
11859
|
-
let openFile =
|
|
11986
|
+
let openFile = path20.join(projectDir, "chapter-01.md");
|
|
11860
11987
|
try {
|
|
11861
11988
|
await fillTemplateFile(manifestPath, substitutions);
|
|
11862
11989
|
if (preset) {
|
|
11863
11990
|
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11864
11991
|
doc.set("preset", preset);
|
|
11865
11992
|
doc.set("targets", doc.createNode(targets ?? []));
|
|
11866
|
-
if (options.customPage)
|
|
11867
|
-
|
|
11868
|
-
width: options.customPage.width,
|
|
11869
|
-
height: options.customPage.height
|
|
11870
|
-
};
|
|
11871
|
-
if (options.customPage.tolerance !== undefined) {
|
|
11872
|
-
page.tolerance = options.customPage.tolerance;
|
|
11873
|
-
}
|
|
11874
|
-
doc.set("page", doc.createNode(page));
|
|
11875
|
-
}
|
|
11993
|
+
if (options.customPage)
|
|
11994
|
+
writePageBounds(doc, options.customPage);
|
|
11876
11995
|
await writeManifestDoc(file, doc);
|
|
11877
11996
|
}
|
|
11878
11997
|
const firstSource = await firstSourceFile(manifestPath);
|
|
11879
11998
|
if (firstSource)
|
|
11880
|
-
openFile =
|
|
11999
|
+
openFile = path20.join(projectDir, firstSource);
|
|
11881
12000
|
await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
|
|
11882
12001
|
} catch (e) {
|
|
11883
12002
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not finalize the book files: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -11899,7 +12018,7 @@ async function scaffoldProject(options) {
|
|
|
11899
12018
|
versionHistory = "none";
|
|
11900
12019
|
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
11901
12020
|
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
11902
|
-
versionHistoryError = enclosingRepoDir ? `This book was added to ${
|
|
12021
|
+
versionHistoryError = enclosingRepoDir ? `This book was added to ${path20.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
|
|
11903
12022
|
}
|
|
11904
12023
|
}
|
|
11905
12024
|
const result = {
|
|
@@ -11919,7 +12038,7 @@ function prettifyFolderName(base) {
|
|
|
11919
12038
|
}
|
|
11920
12039
|
async function adoptFolder(options) {
|
|
11921
12040
|
const dir = options.dir;
|
|
11922
|
-
if (typeof dir !== "string" || !
|
|
12041
|
+
if (typeof dir !== "string" || !path20.isAbsolute(dir)) {
|
|
11923
12042
|
throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
|
|
11924
12043
|
}
|
|
11925
12044
|
let st;
|
|
@@ -11931,11 +12050,11 @@ async function adoptFolder(options) {
|
|
|
11931
12050
|
if (!st.isDirectory()) {
|
|
11932
12051
|
throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
|
|
11933
12052
|
}
|
|
11934
|
-
if (MANIFEST_FILENAMES.some((name) =>
|
|
12053
|
+
if (MANIFEST_FILENAMES.some((name) => existsSync14(path20.join(dir, name)))) {
|
|
11935
12054
|
throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress book.");
|
|
11936
12055
|
}
|
|
11937
12056
|
const template = options.template ?? "book";
|
|
11938
|
-
const title = (options.title ?? "").trim() || prettifyFolderName(
|
|
12057
|
+
const title = (options.title ?? "").trim() || prettifyFolderName(path20.basename(dir));
|
|
11939
12058
|
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11940
12059
|
let mdFiles;
|
|
11941
12060
|
try {
|
|
@@ -11943,14 +12062,14 @@ async function adoptFolder(options) {
|
|
|
11943
12062
|
mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
11944
12063
|
if (mdFiles.length === 0) {
|
|
11945
12064
|
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11946
|
-
await copyFile(tplChapter,
|
|
11947
|
-
await fillTemplateFile(
|
|
12065
|
+
await copyFile(tplChapter, path20.join(dir, "chapter-01.md"));
|
|
12066
|
+
await fillTemplateFile(path20.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
|
|
11948
12067
|
mdFiles = ["chapter-01.md"];
|
|
11949
12068
|
}
|
|
11950
|
-
await
|
|
11951
|
-
const bookCssPath =
|
|
11952
|
-
if (!
|
|
11953
|
-
await
|
|
12069
|
+
await mkdir9(path20.join(dir, "styles"), { recursive: true });
|
|
12070
|
+
const bookCssPath = path20.join(dir, "styles", "book.css");
|
|
12071
|
+
if (!existsSync14(bookCssPath)) {
|
|
12072
|
+
await writeFile9(bookCssPath, starterOverridesCss(), "utf8");
|
|
11954
12073
|
}
|
|
11955
12074
|
const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
|
|
11956
12075
|
`);
|
|
@@ -11966,8 +12085,8 @@ ${filesYaml}
|
|
|
11966
12085
|
styles:
|
|
11967
12086
|
- styles/book.css
|
|
11968
12087
|
`;
|
|
11969
|
-
await
|
|
11970
|
-
await
|
|
12088
|
+
await writeFile9(path20.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
|
|
12089
|
+
await mkdir9(path20.join(dir, "assets"), { recursive: true });
|
|
11971
12090
|
await ensureProjectGitignore(dir);
|
|
11972
12091
|
} catch (e) {
|
|
11973
12092
|
if (e instanceof CreateProjectErrorImpl)
|
|
@@ -11994,8 +12113,8 @@ styles:
|
|
|
11994
12113
|
}
|
|
11995
12114
|
const result = {
|
|
11996
12115
|
projectDir: dir,
|
|
11997
|
-
manifestPath:
|
|
11998
|
-
openFile:
|
|
12116
|
+
manifestPath: path20.join(dir, MANIFEST_FILENAMES[0]),
|
|
12117
|
+
openFile: path20.join(dir, mdFiles[0]),
|
|
11999
12118
|
versionHistory
|
|
12000
12119
|
};
|
|
12001
12120
|
if (versionHistoryError !== undefined)
|
|
@@ -12005,7 +12124,7 @@ styles:
|
|
|
12005
12124
|
async function firstSourceFile(manifestPath) {
|
|
12006
12125
|
let text;
|
|
12007
12126
|
try {
|
|
12008
|
-
text = await
|
|
12127
|
+
text = await readFile30(manifestPath, "utf8");
|
|
12009
12128
|
} catch {
|
|
12010
12129
|
return;
|
|
12011
12130
|
}
|
|
@@ -12017,16 +12136,16 @@ async function firstSourceFile(manifestPath) {
|
|
|
12017
12136
|
return m ? m[1].trim() : undefined;
|
|
12018
12137
|
}
|
|
12019
12138
|
async function fillTemplateFile(filePath, substitutions) {
|
|
12020
|
-
let text = await
|
|
12139
|
+
let text = await readFile30(filePath, "utf8");
|
|
12021
12140
|
for (const [token, value] of Object.entries(substitutions)) {
|
|
12022
12141
|
text = text.split(token).join(value);
|
|
12023
12142
|
}
|
|
12024
|
-
await
|
|
12143
|
+
await writeFile9(filePath, text, "utf8");
|
|
12025
12144
|
}
|
|
12026
12145
|
// src/lib/extension-scaffold.ts
|
|
12027
|
-
import { access as access2, mkdir as
|
|
12146
|
+
import { access as access2, mkdir as mkdir10, readFile as readFile31, writeFile as writeFile10 } from "node:fs/promises";
|
|
12028
12147
|
import { constants as FS2 } from "node:fs";
|
|
12029
|
-
import
|
|
12148
|
+
import path21 from "node:path";
|
|
12030
12149
|
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
12031
12150
|
var RESERVED_PREFIX = "gp-";
|
|
12032
12151
|
var TEMPLATE_FILES = {
|
|
@@ -12104,7 +12223,7 @@ async function scaffoldExtension(options) {
|
|
|
12104
12223
|
}
|
|
12105
12224
|
const prefix = resolveExtensionPrefix(slug, options.prefix);
|
|
12106
12225
|
const parentDir = options.parentDir;
|
|
12107
|
-
if (!parentDir || !
|
|
12226
|
+
if (!parentDir || !path21.isAbsolute(parentDir)) {
|
|
12108
12227
|
throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
|
|
12109
12228
|
}
|
|
12110
12229
|
try {
|
|
@@ -12112,7 +12231,7 @@ async function scaffoldExtension(options) {
|
|
|
12112
12231
|
} catch {
|
|
12113
12232
|
throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
12114
12233
|
}
|
|
12115
|
-
const extensionDir =
|
|
12234
|
+
const extensionDir = path21.join(parentDir, slug);
|
|
12116
12235
|
let targetExists = true;
|
|
12117
12236
|
try {
|
|
12118
12237
|
await access2(extensionDir, FS2.F_OK);
|
|
@@ -12136,14 +12255,14 @@ async function scaffoldExtension(options) {
|
|
|
12136
12255
|
const entries = TEMPLATE_FILES[kind];
|
|
12137
12256
|
const written = [];
|
|
12138
12257
|
try {
|
|
12139
|
-
await
|
|
12258
|
+
await mkdir10(extensionDir, { recursive: true });
|
|
12140
12259
|
for (const [assetName, relDest] of entries) {
|
|
12141
12260
|
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
12142
|
-
const dest =
|
|
12143
|
-
await
|
|
12144
|
-
const text = await
|
|
12261
|
+
const dest = path21.join(extensionDir, relDest);
|
|
12262
|
+
await mkdir10(path21.dirname(dest), { recursive: true });
|
|
12263
|
+
const text = await readFile31(src, "utf8");
|
|
12145
12264
|
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
12146
|
-
await
|
|
12265
|
+
await writeFile10(dest, substitute(text, table), "utf8");
|
|
12147
12266
|
written.push(relDest);
|
|
12148
12267
|
}
|
|
12149
12268
|
} catch (e) {
|
|
@@ -12151,11 +12270,11 @@ async function scaffoldExtension(options) {
|
|
|
12151
12270
|
}
|
|
12152
12271
|
return {
|
|
12153
12272
|
extensionDir,
|
|
12154
|
-
manifestPath:
|
|
12273
|
+
manifestPath: path21.join(extensionDir, "package.json"),
|
|
12155
12274
|
kind,
|
|
12156
12275
|
slug,
|
|
12157
12276
|
prefix,
|
|
12158
|
-
openFile:
|
|
12277
|
+
openFile: path21.join(extensionDir, OPEN_FILE[kind]),
|
|
12159
12278
|
files: written
|
|
12160
12279
|
};
|
|
12161
12280
|
}
|
|
@@ -12167,10 +12286,10 @@ function substitute(text, table) {
|
|
|
12167
12286
|
return out;
|
|
12168
12287
|
}
|
|
12169
12288
|
// src/lib/project-templates.ts
|
|
12170
|
-
import { cp as cp3, mkdir as
|
|
12171
|
-
import { constants as FS3, existsSync as
|
|
12289
|
+
import { cp as cp3, mkdir as mkdir11, readFile as readFile32, readdir as readdir7, stat as stat7, writeFile as writeFile11 } from "node:fs/promises";
|
|
12290
|
+
import { constants as FS3, existsSync as existsSync15 } from "node:fs";
|
|
12172
12291
|
import { access as access3 } from "node:fs/promises";
|
|
12173
|
-
import
|
|
12292
|
+
import path22 from "node:path";
|
|
12174
12293
|
import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
|
|
12175
12294
|
var BUILT_IN_TEMPLATE_IDS = [
|
|
12176
12295
|
"book",
|
|
@@ -12179,7 +12298,7 @@ var BUILT_IN_TEMPLATE_IDS = [
|
|
|
12179
12298
|
];
|
|
12180
12299
|
async function readTemplateChoices(manifestPath) {
|
|
12181
12300
|
try {
|
|
12182
|
-
const text = await
|
|
12301
|
+
const text = await readFile32(manifestPath, "utf8");
|
|
12183
12302
|
const doc = parseDocument2(text);
|
|
12184
12303
|
const out = {};
|
|
12185
12304
|
const preset = doc.get("preset");
|
|
@@ -12255,7 +12374,7 @@ function rewriteEntry(node, newPath) {
|
|
|
12255
12374
|
node.value = newPath;
|
|
12256
12375
|
}
|
|
12257
12376
|
function commonAncestorDir(absFiles) {
|
|
12258
|
-
const dirSegs = absFiles.map((f) =>
|
|
12377
|
+
const dirSegs = absFiles.map((f) => path22.resolve(f).split(path22.sep).slice(0, -1));
|
|
12259
12378
|
if (dirSegs.length === 0)
|
|
12260
12379
|
return "";
|
|
12261
12380
|
let common = dirSegs[0];
|
|
@@ -12265,22 +12384,22 @@ function commonAncestorDir(absFiles) {
|
|
|
12265
12384
|
i++;
|
|
12266
12385
|
common = common.slice(0, i);
|
|
12267
12386
|
}
|
|
12268
|
-
return common.join(
|
|
12387
|
+
return common.join(path22.sep) || path22.sep;
|
|
12269
12388
|
}
|
|
12270
12389
|
function pickVendorDir(templateDir, base) {
|
|
12271
|
-
const preferred =
|
|
12272
|
-
if (!
|
|
12390
|
+
const preferred = path22.basename(base) || VENDOR_DIR;
|
|
12391
|
+
if (!existsSync15(path22.join(templateDir, preferred)))
|
|
12273
12392
|
return preferred;
|
|
12274
12393
|
for (let n = 1;; n++) {
|
|
12275
12394
|
const candidate = `${preferred}-${n}`;
|
|
12276
|
-
if (!
|
|
12395
|
+
if (!existsSync15(path22.join(templateDir, candidate)))
|
|
12277
12396
|
return candidate;
|
|
12278
12397
|
}
|
|
12279
12398
|
}
|
|
12280
12399
|
var TEMPLATE_META_FILE = ".gutterpress-template.json";
|
|
12281
12400
|
async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
12282
12401
|
const { doc, file } = await loadManifestDoc(templateDir);
|
|
12283
|
-
const escapes = (p) => escapesProjectRoot(sourceProjectDir,
|
|
12402
|
+
const escapes = (p) => escapesProjectRoot(sourceProjectDir, path22.resolve(sourceProjectDir, p));
|
|
12284
12403
|
const stylesSeq = doc.get("styles", true);
|
|
12285
12404
|
const extensionsSeq = doc.get("extensions", true);
|
|
12286
12405
|
const escapingStyles = collectEscaping(isSeq3(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
|
|
@@ -12296,16 +12415,16 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
|
12296
12415
|
return { vendoredRefs: [], excludedRefs };
|
|
12297
12416
|
}
|
|
12298
12417
|
const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
|
|
12299
|
-
const extensionFiles = escapingExtensions.map((p) =>
|
|
12418
|
+
const extensionFiles = escapingExtensions.map((p) => path22.resolve(sourceProjectDir, p.value));
|
|
12300
12419
|
const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
|
|
12301
12420
|
const base = commonAncestorDir(externalFiles);
|
|
12302
12421
|
const vendorDir = pickVendorDir(templateDir, base);
|
|
12303
|
-
await Promise.all(externalFiles.filter((abs) =>
|
|
12304
|
-
const dest =
|
|
12305
|
-
await
|
|
12422
|
+
await Promise.all(externalFiles.filter((abs) => existsSync15(abs)).map(async (abs) => {
|
|
12423
|
+
const dest = path22.join(templateDir, vendorDir, path22.relative(base, abs));
|
|
12424
|
+
await mkdir11(path22.dirname(dest), { recursive: true });
|
|
12306
12425
|
await cp3(abs, dest, { recursive: true });
|
|
12307
12426
|
}));
|
|
12308
|
-
const bookLocal = (value) => toPosixPath(
|
|
12427
|
+
const bookLocal = (value) => toPosixPath(path22.join(vendorDir, path22.relative(base, path22.resolve(sourceProjectDir, value))));
|
|
12309
12428
|
const vendoredRefs = [];
|
|
12310
12429
|
for (const { node, value } of escapingStyles) {
|
|
12311
12430
|
const rewritten = bookLocal(value);
|
|
@@ -12325,7 +12444,7 @@ async function saveProjectAsTemplate(options) {
|
|
|
12325
12444
|
const id = slugify(name);
|
|
12326
12445
|
if (!id)
|
|
12327
12446
|
throw new Error(`Could not derive a template id from "${name}".`);
|
|
12328
|
-
const dir =
|
|
12447
|
+
const dir = path22.join(templatesRoot, id);
|
|
12329
12448
|
let exists = true;
|
|
12330
12449
|
try {
|
|
12331
12450
|
await access3(dir, FS3.F_OK);
|
|
@@ -12335,20 +12454,20 @@ async function saveProjectAsTemplate(options) {
|
|
|
12335
12454
|
if (exists) {
|
|
12336
12455
|
throw new Error(`A template named "${name}" already exists. Choose a different name.`);
|
|
12337
12456
|
}
|
|
12338
|
-
await
|
|
12457
|
+
await mkdir11(dir, { recursive: true });
|
|
12339
12458
|
const entries = await readdir7(projectDir, { withFileTypes: true });
|
|
12340
12459
|
for (const entry of entries) {
|
|
12341
12460
|
if (SKIP_ENTRIES.has(entry.name))
|
|
12342
12461
|
continue;
|
|
12343
12462
|
if (entry.name === TEMPLATE_META_FILE)
|
|
12344
12463
|
continue;
|
|
12345
|
-
await cp3(
|
|
12464
|
+
await cp3(path22.join(projectDir, entry.name), path22.join(dir, entry.name), { recursive: true });
|
|
12346
12465
|
}
|
|
12347
12466
|
const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
|
|
12348
12467
|
const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
|
|
12349
12468
|
if (manifestName)
|
|
12350
|
-
await retokeniseManifest(
|
|
12351
|
-
await
|
|
12469
|
+
await retokeniseManifest(path22.join(dir, manifestName));
|
|
12470
|
+
await writeFile11(path22.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
|
|
12352
12471
|
return {
|
|
12353
12472
|
id,
|
|
12354
12473
|
label: name,
|
|
@@ -12361,12 +12480,12 @@ async function saveProjectAsTemplate(options) {
|
|
|
12361
12480
|
async function retokeniseManifest(manifestPath) {
|
|
12362
12481
|
let text;
|
|
12363
12482
|
try {
|
|
12364
|
-
text = await
|
|
12483
|
+
text = await readFile32(manifestPath, "utf8");
|
|
12365
12484
|
} catch {
|
|
12366
12485
|
return;
|
|
12367
12486
|
}
|
|
12368
12487
|
text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
|
|
12369
|
-
await
|
|
12488
|
+
await writeFile11(manifestPath, text, "utf8");
|
|
12370
12489
|
}
|
|
12371
12490
|
async function listCustomTemplates(templatesRoot) {
|
|
12372
12491
|
let entries;
|
|
@@ -12379,10 +12498,10 @@ async function listCustomTemplates(templatesRoot) {
|
|
|
12379
12498
|
for (const entry of entries) {
|
|
12380
12499
|
if (!entry.isDirectory())
|
|
12381
12500
|
continue;
|
|
12382
|
-
const dir =
|
|
12501
|
+
const dir = path22.join(templatesRoot, entry.name);
|
|
12383
12502
|
let label = prettify(entry.name);
|
|
12384
12503
|
try {
|
|
12385
|
-
const meta = JSON.parse(await
|
|
12504
|
+
const meta = JSON.parse(await readFile32(path22.join(dir, TEMPLATE_META_FILE), "utf8"));
|
|
12386
12505
|
if (typeof meta.label === "string" && meta.label)
|
|
12387
12506
|
label = meta.label;
|
|
12388
12507
|
} catch {}
|
|
@@ -12392,7 +12511,7 @@ async function listCustomTemplates(templatesRoot) {
|
|
|
12392
12511
|
description: "Your saved template.",
|
|
12393
12512
|
kind: "custom",
|
|
12394
12513
|
dir,
|
|
12395
|
-
...await readTemplateChoices(
|
|
12514
|
+
...await readTemplateChoices(path22.join(dir, MANIFEST_FILENAMES[0]))
|
|
12396
12515
|
});
|
|
12397
12516
|
}
|
|
12398
12517
|
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
@@ -12404,12 +12523,12 @@ async function importTemplateFromFolder(options) {
|
|
|
12404
12523
|
if (!srcStat.isDirectory()) {
|
|
12405
12524
|
throw new Error("The chosen path is not a folder.");
|
|
12406
12525
|
}
|
|
12407
|
-
const name = options.name?.trim() || prettify(
|
|
12526
|
+
const name = options.name?.trim() || prettify(path22.basename(sourceDir));
|
|
12408
12527
|
return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
|
|
12409
12528
|
}
|
|
12410
12529
|
// src/lib/snippets.ts
|
|
12411
|
-
import { readdir as readdir8, readFile as
|
|
12412
|
-
import
|
|
12530
|
+
import { readdir as readdir8, readFile as readFile33, writeFile as writeFile12, mkdir as mkdir12, rm as rm4 } from "node:fs/promises";
|
|
12531
|
+
import path23 from "node:path";
|
|
12413
12532
|
var SNIPPETS_DIR = "snippets";
|
|
12414
12533
|
var LEVEL_RANK = { project: 0, extension: 1, core: 2 };
|
|
12415
12534
|
var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
@@ -12429,9 +12548,9 @@ function substituteVariables(template, values) {
|
|
|
12429
12548
|
return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
|
|
12430
12549
|
}
|
|
12431
12550
|
function resolveSnippetPath(projectDir, fileName) {
|
|
12432
|
-
const dir =
|
|
12433
|
-
const full =
|
|
12434
|
-
if (full !==
|
|
12551
|
+
const dir = path23.resolve(projectDir, SNIPPETS_DIR);
|
|
12552
|
+
const full = path23.resolve(dir, fileName);
|
|
12553
|
+
if (full !== path23.join(dir, path23.basename(fileName)) || path23.dirname(full) !== dir) {
|
|
12435
12554
|
throw new Error(`Unsafe snippet filename: ${fileName}`);
|
|
12436
12555
|
}
|
|
12437
12556
|
return full;
|
|
@@ -12449,7 +12568,7 @@ async function scanSnippetFiles(dir) {
|
|
|
12449
12568
|
continue;
|
|
12450
12569
|
let body = "";
|
|
12451
12570
|
try {
|
|
12452
|
-
body = await
|
|
12571
|
+
body = await readFile33(path23.join(dir, fileName), "utf8");
|
|
12453
12572
|
} catch {
|
|
12454
12573
|
continue;
|
|
12455
12574
|
}
|
|
@@ -12464,7 +12583,7 @@ async function scanSnippetFiles(dir) {
|
|
|
12464
12583
|
return entries;
|
|
12465
12584
|
}
|
|
12466
12585
|
async function listSnippets(projectDir) {
|
|
12467
|
-
const files = await scanSnippetFiles(
|
|
12586
|
+
const files = await scanSnippetFiles(path23.join(projectDir, SNIPPETS_DIR));
|
|
12468
12587
|
return files.map((file) => ({ ...file, source: { kind: "project" } }));
|
|
12469
12588
|
}
|
|
12470
12589
|
async function saveSnippet(projectDir, name, body) {
|
|
@@ -12472,9 +12591,9 @@ async function saveSnippet(projectDir, name, body) {
|
|
|
12472
12591
|
if (!stem)
|
|
12473
12592
|
throw new Error(`Could not derive a filename from "${name}".`);
|
|
12474
12593
|
const fileName = `${stem}.md`;
|
|
12475
|
-
const dir =
|
|
12476
|
-
await
|
|
12477
|
-
await
|
|
12594
|
+
const dir = path23.join(projectDir, SNIPPETS_DIR);
|
|
12595
|
+
await mkdir12(dir, { recursive: true });
|
|
12596
|
+
await writeFile12(resolveSnippetPath(projectDir, fileName), body, "utf8");
|
|
12478
12597
|
return { name, fileName, variables: extractVariables(body), body, source: { kind: "project" } };
|
|
12479
12598
|
}
|
|
12480
12599
|
async function deleteSnippet(projectDir, fileName) {
|
|
@@ -12504,7 +12623,7 @@ async function collectLibrary(projectDir) {
|
|
|
12504
12623
|
continue;
|
|
12505
12624
|
const source = { kind: "extension", ref: entry.use, name: entry.label };
|
|
12506
12625
|
const folder = await declaredSnippetsFolder(entry);
|
|
12507
|
-
const snippets2 = folder ? (await scanSnippetFiles(
|
|
12626
|
+
const snippets2 = folder ? (await scanSnippetFiles(path23.join(entry.dir, folder))).map((file) => ({ ...file, source })) : [];
|
|
12508
12627
|
loaded.push({ entry, source, folder, snippets: snippets2, markers: await declaredMarkers(entry, projectDir) });
|
|
12509
12628
|
}
|
|
12510
12629
|
let registry;
|
|
@@ -12518,24 +12637,29 @@ async function collectLibrary(projectDir) {
|
|
|
12518
12637
|
const resolved = registry.get(name);
|
|
12519
12638
|
if (!resolved || resolved.deprecated !== undefined)
|
|
12520
12639
|
continue;
|
|
12521
|
-
components.push({
|
|
12640
|
+
components.push({
|
|
12641
|
+
name,
|
|
12642
|
+
source,
|
|
12643
|
+
variants: Object.keys(resolved.variants ?? {}),
|
|
12644
|
+
...resolved.selfClosing ? { selfClosing: true } : {}
|
|
12645
|
+
});
|
|
12522
12646
|
if (!entry.dir)
|
|
12523
12647
|
continue;
|
|
12524
12648
|
const own = typeof decl.snippet === "string" && decl.snippet.trim() ? decl.snippet.trim() : null;
|
|
12525
|
-
const rel = own ??
|
|
12649
|
+
const rel = own ?? path23.join(folder ?? SNIPPETS_DIR, `${name}.md`);
|
|
12526
12650
|
if (pathEscapesFolder(rel))
|
|
12527
12651
|
continue;
|
|
12528
|
-
const listed = folder &&
|
|
12652
|
+
const listed = folder && path23.dirname(path23.join(entry.dir, rel)) === path23.join(entry.dir, folder) ? snippets2.find((s) => s.fileName === path23.basename(rel)) : undefined;
|
|
12529
12653
|
if (listed) {
|
|
12530
12654
|
listed.component = name;
|
|
12531
12655
|
continue;
|
|
12532
12656
|
}
|
|
12533
|
-
const body = await
|
|
12657
|
+
const body = await readFile33(path23.join(entry.dir, rel), "utf8").catch(() => null);
|
|
12534
12658
|
if (body === null)
|
|
12535
12659
|
continue;
|
|
12536
12660
|
snippets2.push({
|
|
12537
|
-
name: prettify(
|
|
12538
|
-
fileName: rel.split(
|
|
12661
|
+
name: prettify(path23.basename(rel).replace(/\.md$/i, "")),
|
|
12662
|
+
fileName: rel.split(path23.sep).join("/"),
|
|
12539
12663
|
variables: extractVariables(body),
|
|
12540
12664
|
body,
|
|
12541
12665
|
source,
|
|
@@ -12579,10 +12703,10 @@ async function listMarkerComponents(projectDir) {
|
|
|
12579
12703
|
return (await collectLibrary(projectDir)).components;
|
|
12580
12704
|
}
|
|
12581
12705
|
// src/lib/extension-import.ts
|
|
12582
|
-
import { existsSync as
|
|
12583
|
-
import { cp as cp4, mkdir as
|
|
12706
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
12707
|
+
import { cp as cp4, mkdir as mkdir13, mkdtemp as mkdtemp2, readFile as readFile34, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
|
|
12584
12708
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
12585
|
-
import
|
|
12709
|
+
import path24 from "node:path";
|
|
12586
12710
|
import { unzipSync } from "fflate";
|
|
12587
12711
|
var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
12588
12712
|
var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
|
|
@@ -12649,11 +12773,11 @@ function mb2(n) {
|
|
|
12649
12773
|
return `${Math.round(n / (1024 * 1024))}MB`;
|
|
12650
12774
|
}
|
|
12651
12775
|
async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallbackName) {
|
|
12652
|
-
const cssPath =
|
|
12653
|
-
if (!
|
|
12776
|
+
const cssPath = path24.join(sourceDir, "theme.css");
|
|
12777
|
+
if (!existsSync16(cssPath)) {
|
|
12654
12778
|
throw new Error("A theme must contain a theme.css file.");
|
|
12655
12779
|
}
|
|
12656
|
-
const css = await
|
|
12780
|
+
const css = await readFile34(cssPath, "utf8");
|
|
12657
12781
|
const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
|
|
12658
12782
|
if (reject) {
|
|
12659
12783
|
throw new Error(`theme.css could not be parsed — ${reject.message}`);
|
|
@@ -12662,7 +12786,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12662
12786
|
for (const w of printFindings) {
|
|
12663
12787
|
warnings.push({ code: "print-safety", message: w.message });
|
|
12664
12788
|
}
|
|
12665
|
-
const metaExists =
|
|
12789
|
+
const metaExists = existsSync16(path24.join(sourceDir, EXTENSION_MANIFEST_FILENAME));
|
|
12666
12790
|
const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
|
|
12667
12791
|
if (!metaExists) {
|
|
12668
12792
|
warnings.push({
|
|
@@ -12681,7 +12805,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12681
12805
|
for (let i = 0;i < extraSheets.length; i++) {
|
|
12682
12806
|
const rel = extraSheets[i];
|
|
12683
12807
|
const sheetPath = extraSheetPaths[i];
|
|
12684
|
-
const sheetCss = await
|
|
12808
|
+
const sheetCss = await readFile34(sheetPath, "utf8");
|
|
12685
12809
|
const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
|
|
12686
12810
|
if (sheetReject) {
|
|
12687
12811
|
throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
|
|
@@ -12700,8 +12824,8 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12700
12824
|
});
|
|
12701
12825
|
}
|
|
12702
12826
|
const id = await uniqueExtensionId(projectDir, meta.name?.trim() || fallbackName);
|
|
12703
|
-
const dest =
|
|
12704
|
-
await
|
|
12827
|
+
const dest = path24.join(projectDir, EXTENSIONS_DIR, id);
|
|
12828
|
+
await mkdir13(path24.dirname(dest), { recursive: true });
|
|
12705
12829
|
await cp4(sourceDir, dest, { recursive: true });
|
|
12706
12830
|
await completeLandedManifest(dest, id, meta, metaExists);
|
|
12707
12831
|
const entry = await addExtension(projectDir, `./${EXTENSIONS_DIR}/${id}`);
|
|
@@ -12710,18 +12834,18 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12710
12834
|
async function completeLandedManifest(dest, id, meta, metaExists) {
|
|
12711
12835
|
if (metaExists && extensionStyleList(meta).length > 0)
|
|
12712
12836
|
return;
|
|
12713
|
-
const file =
|
|
12837
|
+
const file = path24.join(dest, EXTENSION_MANIFEST_FILENAME);
|
|
12714
12838
|
let pkg = {};
|
|
12715
12839
|
if (metaExists) {
|
|
12716
12840
|
try {
|
|
12717
|
-
const parsed = JSON.parse(await
|
|
12841
|
+
const parsed = JSON.parse(await readFile34(file, "utf8"));
|
|
12718
12842
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12719
12843
|
pkg = parsed;
|
|
12720
12844
|
}
|
|
12721
12845
|
} catch {}
|
|
12722
12846
|
}
|
|
12723
12847
|
const gutterpress = pkg.gutterpress && typeof pkg.gutterpress === "object" && !Array.isArray(pkg.gutterpress) ? pkg.gutterpress : {};
|
|
12724
|
-
await
|
|
12848
|
+
await writeFile13(file, JSON.stringify({
|
|
12725
12849
|
...pkg,
|
|
12726
12850
|
name: typeof pkg.name === "string" && pkg.name.trim() ? pkg.name : id,
|
|
12727
12851
|
gutterpress: { ...gutterpress, styles: ["theme.css"] }
|
|
@@ -12768,7 +12892,7 @@ async function importExtensionFromZip(projectDir, archive) {
|
|
|
12768
12892
|
throw new Error("No theme.css found in the package. A theme needs a theme.css at the package root or inside a single folder.");
|
|
12769
12893
|
}
|
|
12770
12894
|
const prefix = root === "" ? "" : `${root}/`;
|
|
12771
|
-
const tmp = await mkdtemp2(
|
|
12895
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-theme-"));
|
|
12772
12896
|
try {
|
|
12773
12897
|
const relPaths = [];
|
|
12774
12898
|
for (const [name, data] of Object.entries(files)) {
|
|
@@ -12780,13 +12904,13 @@ async function importExtensionFromZip(projectDir, archive) {
|
|
|
12780
12904
|
const rel = prefix ? norm.slice(prefix.length) : norm;
|
|
12781
12905
|
if (!rel || rel.split("/").some((seg) => seg === ".."))
|
|
12782
12906
|
continue;
|
|
12783
|
-
const dest =
|
|
12784
|
-
if (dest !== tmp && !dest.startsWith(tmp +
|
|
12907
|
+
const dest = path24.join(tmp, rel);
|
|
12908
|
+
if (dest !== tmp && !dest.startsWith(tmp + path24.sep)) {
|
|
12785
12909
|
throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
|
|
12786
12910
|
}
|
|
12787
12911
|
relPaths.push(rel);
|
|
12788
|
-
await
|
|
12789
|
-
await
|
|
12912
|
+
await mkdir13(path24.dirname(dest), { recursive: true });
|
|
12913
|
+
await writeFile13(dest, data);
|
|
12790
12914
|
}
|
|
12791
12915
|
return await finalizeExtensionImport(projectDir, tmp, relPaths, root || "imported-look");
|
|
12792
12916
|
} finally {
|
|
@@ -12799,10 +12923,10 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
12799
12923
|
throw new Error(`The CSS could not be parsed — ${reject.message}`);
|
|
12800
12924
|
}
|
|
12801
12925
|
const displayName = name.trim() || "Imported theme";
|
|
12802
|
-
const tmp = await mkdtemp2(
|
|
12926
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-theme-"));
|
|
12803
12927
|
try {
|
|
12804
|
-
await
|
|
12805
|
-
await
|
|
12928
|
+
await writeFile13(path24.join(tmp, "theme.css"), css, "utf8");
|
|
12929
|
+
await writeFile13(path24.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify({ name: slugify(displayName, "look"), gutterpress: { styles: ["theme.css"] } }, null, 2), "utf8");
|
|
12806
12930
|
const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], displayName);
|
|
12807
12931
|
result.warnings.unshift({
|
|
12808
12932
|
code: "no-theme-json",
|
|
@@ -12814,14 +12938,14 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
12814
12938
|
}
|
|
12815
12939
|
}
|
|
12816
12940
|
async function importExtensionFromFile(projectDir, filePath) {
|
|
12817
|
-
const ext =
|
|
12941
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
12818
12942
|
if (ext === ".zip") {
|
|
12819
|
-
const buf = await
|
|
12943
|
+
const buf = await readFile34(filePath);
|
|
12820
12944
|
return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
12821
12945
|
}
|
|
12822
12946
|
if (ext === ".css") {
|
|
12823
|
-
const css = await
|
|
12824
|
-
return importExtensionFromCssText(projectDir, css, prettify(
|
|
12947
|
+
const css = await readFile34(filePath, "utf8");
|
|
12948
|
+
return importExtensionFromCssText(projectDir, css, prettify(path24.basename(filePath, ".css")));
|
|
12825
12949
|
}
|
|
12826
12950
|
throw new Error("Choose a .zip extension package or a .css stylesheet.");
|
|
12827
12951
|
}
|
|
@@ -12869,7 +12993,7 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12869
12993
|
let baseName;
|
|
12870
12994
|
if (looksLikeCssUrl(trimmed)) {
|
|
12871
12995
|
css = await fetchText(trimmed);
|
|
12872
|
-
baseName =
|
|
12996
|
+
baseName = path24.basename(new URL(trimmed, "https://x/").pathname, ".css") || "look";
|
|
12873
12997
|
} else {
|
|
12874
12998
|
const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
12875
12999
|
css = await fetchText(`${base}theme.css`);
|
|
@@ -12878,14 +13002,14 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12878
13002
|
if (parsed && typeof parsed === "object")
|
|
12879
13003
|
meta = parsed;
|
|
12880
13004
|
} catch {}
|
|
12881
|
-
baseName = typeof meta.name === "string" && meta.name ||
|
|
13005
|
+
baseName = typeof meta.name === "string" && meta.name || path24.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "look";
|
|
12882
13006
|
}
|
|
12883
13007
|
if (!css.trim())
|
|
12884
13008
|
throw new Error(`Fetched CSS from ${trimmed} was empty.`);
|
|
12885
13009
|
assertLooksLikeCss(css, trimmed);
|
|
12886
|
-
const tmp = await mkdtemp2(
|
|
13010
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-ext-"));
|
|
12887
13011
|
try {
|
|
12888
|
-
await
|
|
13012
|
+
await writeFile13(path24.join(tmp, "theme.css"), css, "utf8");
|
|
12889
13013
|
const gp = meta.gutterpress && typeof meta.gutterpress === "object" && !Array.isArray(meta.gutterpress) ? meta.gutterpress : {};
|
|
12890
13014
|
const finalMeta = {
|
|
12891
13015
|
name: typeof meta.name === "string" && meta.name || slugify(baseName, "look"),
|
|
@@ -12897,7 +13021,7 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12897
13021
|
preview: typeof gp.preview === "string" ? gp.preview : null
|
|
12898
13022
|
}
|
|
12899
13023
|
};
|
|
12900
|
-
await
|
|
13024
|
+
await writeFile13(path24.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify(finalMeta, null, 2), "utf8");
|
|
12901
13025
|
return await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], baseName);
|
|
12902
13026
|
} finally {
|
|
12903
13027
|
await rm5(tmp, { recursive: true, force: true });
|
|
@@ -13018,7 +13142,7 @@ async function searchNpmExtensions(query, options = {}) {
|
|
|
13018
13142
|
return { matches, total: pages.reduce((sum, page) => sum + page.total, 0) };
|
|
13019
13143
|
}
|
|
13020
13144
|
// src/lib/manifest-config.ts
|
|
13021
|
-
import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as
|
|
13145
|
+
import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
|
|
13022
13146
|
function hasKey(obj, key) {
|
|
13023
13147
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
13024
13148
|
}
|
|
@@ -13101,7 +13225,7 @@ async function setActiveStyles(projectDir, paths) {
|
|
|
13101
13225
|
} else {
|
|
13102
13226
|
const seq = new YAMLSeq3(doc.schema);
|
|
13103
13227
|
for (const p of paths)
|
|
13104
|
-
seq.add(new
|
|
13228
|
+
seq.add(new Scalar3(p));
|
|
13105
13229
|
doc.set("styles", seq);
|
|
13106
13230
|
}
|
|
13107
13231
|
await writeManifestDoc(file, doc);
|
|
@@ -13643,7 +13767,8 @@ async function listGitHubRepositories(credential, options = {}) {
|
|
|
13643
13767
|
fullName: r.full_name,
|
|
13644
13768
|
private: r.private,
|
|
13645
13769
|
defaultBranch: r.default_branch,
|
|
13646
|
-
htmlUrl: r.html_url
|
|
13770
|
+
htmlUrl: r.html_url,
|
|
13771
|
+
...r.size === 0 && !r.archived && r.permissions?.push !== false ? { maybeEmpty: true } : {}
|
|
13647
13772
|
});
|
|
13648
13773
|
}
|
|
13649
13774
|
if (body.length < PER_PAGE)
|
|
@@ -13708,10 +13833,12 @@ async function listGitHubBranches(credential, owner, repo, options = {}) {
|
|
|
13708
13833
|
}
|
|
13709
13834
|
return branches;
|
|
13710
13835
|
}
|
|
13711
|
-
// src/lib/remote-auth/
|
|
13712
|
-
import
|
|
13713
|
-
|
|
13714
|
-
|
|
13836
|
+
// src/lib/remote-auth/online-backup.ts
|
|
13837
|
+
import git5 from "isomorphic-git";
|
|
13838
|
+
|
|
13839
|
+
// src/lib/remote-auth/sync.ts
|
|
13840
|
+
import git4 from "isomorphic-git";
|
|
13841
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
13715
13842
|
|
|
13716
13843
|
// src/lib/remote-auth/git-http.ts
|
|
13717
13844
|
import httpNode from "isomorphic-git/http/node";
|
|
@@ -13804,634 +13931,85 @@ function withIdleTimeout(http3, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
|
|
|
13804
13931
|
}
|
|
13805
13932
|
var defaultGitHttp = withIdleTimeout(httpNode);
|
|
13806
13933
|
|
|
13807
|
-
// src/lib/remote-auth/
|
|
13934
|
+
// src/lib/remote-auth/converge-merge.ts
|
|
13935
|
+
import { mkdir as mkdir14, unlink as unlink3, writeFile as writeFile14 } from "node:fs/promises";
|
|
13936
|
+
import path25 from "node:path";
|
|
13808
13937
|
import git2 from "isomorphic-git";
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
return gitScopeFor(source);
|
|
13813
|
-
return projectDir;
|
|
13938
|
+
import diff3Merge from "diff3";
|
|
13939
|
+
function isMergeConflictError(e) {
|
|
13940
|
+
return e?.code === "MergeConflictError";
|
|
13814
13941
|
}
|
|
13815
|
-
function
|
|
13816
|
-
|
|
13817
|
-
const u = new URL(url);
|
|
13818
|
-
if (u.protocol === "https:")
|
|
13819
|
-
return true;
|
|
13820
|
-
if (u.protocol === "http:") {
|
|
13821
|
-
const host = u.hostname.toLowerCase();
|
|
13822
|
-
return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
|
|
13823
|
-
}
|
|
13824
|
-
return false;
|
|
13825
|
-
} catch {
|
|
13826
|
-
return false;
|
|
13827
|
-
}
|
|
13942
|
+
function isCheckoutConflict(e) {
|
|
13943
|
+
return e?.code === "CheckoutConflictError";
|
|
13828
13944
|
}
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13945
|
+
var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
|
|
13946
|
+
var CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
|
|
13947
|
+
var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
|
|
13948
|
+
var MARKER_UNSAFE_EXT = /\.svg$/i;
|
|
13949
|
+
var OUR_LABEL = "your version";
|
|
13950
|
+
var THEIR_LABEL = "online version";
|
|
13951
|
+
function stringLooksBinary(s) {
|
|
13952
|
+
return s.includes("\x00");
|
|
13953
|
+
}
|
|
13954
|
+
var LINEBREAKS = /^.*(\r?\n|$)/gm;
|
|
13955
|
+
function mergeWithMarkers(baseContent, ourContent, theirContent) {
|
|
13956
|
+
const result = diff3Merge(ourContent.match(LINEBREAKS) ?? [], baseContent.match(LINEBREAKS) ?? [], theirContent.match(LINEBREAKS) ?? []);
|
|
13957
|
+
let merged = "";
|
|
13958
|
+
for (const item of result) {
|
|
13959
|
+
if (item.ok)
|
|
13960
|
+
merged += item.ok.join("");
|
|
13961
|
+
if (item.conflict) {
|
|
13962
|
+
merged += `<<<<<<< ${OUR_LABEL}
|
|
13963
|
+
`;
|
|
13964
|
+
merged += item.conflict.a.join("");
|
|
13965
|
+
merged += `=======
|
|
13966
|
+
`;
|
|
13967
|
+
merged += item.conflict.b.join("");
|
|
13968
|
+
merged += `>>>>>>> ${THEIR_LABEL}
|
|
13969
|
+
`;
|
|
13970
|
+
}
|
|
13835
13971
|
}
|
|
13972
|
+
return merged;
|
|
13836
13973
|
}
|
|
13837
|
-
function
|
|
13838
|
-
return
|
|
13974
|
+
async function readBlob(dir, cache2, oid, filepath) {
|
|
13975
|
+
return git2.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
|
|
13839
13976
|
}
|
|
13840
|
-
function
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
onAuth: (url) => {
|
|
13845
|
-
if (!isCredentialTransmissionSafe(url))
|
|
13846
|
-
throw new InsecureTransportError;
|
|
13847
|
-
return {
|
|
13848
|
-
username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
|
|
13849
|
-
password: credential.token
|
|
13850
|
-
};
|
|
13851
|
-
}
|
|
13852
|
-
};
|
|
13977
|
+
async function writeTreeFile(dir, filepath, content) {
|
|
13978
|
+
const abs = path25.join(dir, filepath);
|
|
13979
|
+
await mkdir14(path25.dirname(abs), { recursive: true });
|
|
13980
|
+
await writeFile14(abs, content);
|
|
13853
13981
|
}
|
|
13854
|
-
async function
|
|
13855
|
-
const
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13982
|
+
async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
|
|
13983
|
+
const bases = await git2.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
|
|
13984
|
+
const base = bases[0];
|
|
13985
|
+
if (!base && !allowUnrelated)
|
|
13986
|
+
return null;
|
|
13987
|
+
const out = [];
|
|
13988
|
+
await git2.walk({
|
|
13989
|
+
fs: gitFs,
|
|
13990
|
+
dir,
|
|
13991
|
+
cache: cache2,
|
|
13992
|
+
trees: [
|
|
13993
|
+
git2.TREE({ ref: ourTip }),
|
|
13994
|
+
git2.TREE({ ref: theirTip }),
|
|
13995
|
+
...base ? [git2.TREE({ ref: base })] : []
|
|
13996
|
+
],
|
|
13997
|
+
map: async (filepath, entries) => {
|
|
13998
|
+
if (filepath === ".")
|
|
13999
|
+
return true;
|
|
14000
|
+
const [ours, theirs, inBase] = entries;
|
|
14001
|
+
const types = await Promise.all([ours, theirs, inBase].map((e) => e ? e.type() : Promise.resolve(undefined)));
|
|
14002
|
+
if (types.some((t) => t === "tree"))
|
|
14003
|
+
return true;
|
|
14004
|
+
if (!ours || !theirs || inBase)
|
|
14005
|
+
return null;
|
|
14006
|
+
const [a, b] = await Promise.all([ours.oid(), theirs.oid()]);
|
|
14007
|
+
if (a !== b)
|
|
14008
|
+
out.push(filepath);
|
|
14009
|
+
return null;
|
|
13874
14010
|
}
|
|
13875
|
-
}
|
|
13876
|
-
|
|
13877
|
-
credential = urlCredential;
|
|
13878
|
-
return {
|
|
13879
|
-
remote: origin.remote,
|
|
13880
|
-
url: cleanUrl,
|
|
13881
|
-
host,
|
|
13882
|
-
...credential ? { credential } : {}
|
|
13883
|
-
};
|
|
13884
|
-
}
|
|
13885
|
-
function classifyTransportFailure(e) {
|
|
13886
|
-
if (isInsecureTransportError(e))
|
|
13887
|
-
return "insecure_transport";
|
|
13888
|
-
const err = e;
|
|
13889
|
-
if (err?.code === "HttpError") {
|
|
13890
|
-
const status = err.data?.statusCode;
|
|
13891
|
-
if (status === 401 || status === 403 || status === 404)
|
|
13892
|
-
return "auth_required";
|
|
13893
|
-
}
|
|
13894
|
-
const msg = `${err?.message ?? String(e)} ${err?.data?.prettyDetails ?? ""}`;
|
|
13895
|
-
if (/\b401\b|\b403\b|\b404\b|unauthorized|authentication|not authorized|permission denied|forbidden|access denied|not allowed to push|pre-receive hook declined|hook declined/i.test(msg)) {
|
|
13896
|
-
return "auth_required";
|
|
13897
|
-
}
|
|
13898
|
-
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network|fetch failed|couldn't reach|socket hang ?up/i.test(msg)) {
|
|
13899
|
-
return "network_unavailable";
|
|
13900
|
-
}
|
|
13901
|
-
return null;
|
|
13902
|
-
}
|
|
13903
|
-
function failureOutcome(e, snapshotId) {
|
|
13904
|
-
const kind = classifyTransportFailure(e);
|
|
13905
|
-
const base = snapshotId ? { snapshotId } : {};
|
|
13906
|
-
if (kind === "auth_required")
|
|
13907
|
-
return { status: "auth", message: MSG_AUTH, ...base };
|
|
13908
|
-
if (kind === "network_unavailable")
|
|
13909
|
-
return { status: "offline", message: MSG_OFFLINE, ...base };
|
|
13910
|
-
if (kind === "insecure_transport") {
|
|
13911
|
-
return { status: "error", message: MSG_INSECURE_TRANSPORT, ...base };
|
|
13912
|
-
}
|
|
13913
|
-
return {
|
|
13914
|
-
status: "error",
|
|
13915
|
-
message: "Syncing didn't complete. Your work is saved on this computer — please try again.",
|
|
13916
|
-
...base
|
|
13917
|
-
};
|
|
13918
|
-
}
|
|
13919
|
-
function setupErrorMessage(e) {
|
|
13920
|
-
if (e instanceof Error && (e.message === MSG_NO_REMOTE || e.message === MSG_SSH_REMOTE || e.message === MSG_NO_BRANCH)) {
|
|
13921
|
-
return e.message;
|
|
13922
|
-
}
|
|
13923
|
-
return null;
|
|
13924
|
-
}
|
|
13925
|
-
async function snapshotBeforeAction(args) {
|
|
13926
|
-
const { projectDir, dir, cache: cache2 } = args;
|
|
13927
|
-
const hasChanges = await hasPendingChanges(dir, cache2);
|
|
13928
|
-
const staleStaging = gitFs.existsSync(snapshotStagingMarkerPath(dir));
|
|
13929
|
-
if (!hasChanges && !staleStaging)
|
|
13930
|
-
return;
|
|
13931
|
-
const snap = await snapshotWorkingTreeUnlocked({
|
|
13932
|
-
projectDir,
|
|
13933
|
-
repoRoot: dir,
|
|
13934
|
-
message: args.message?.trim() || SYNC_SNAPSHOT_MESSAGE,
|
|
13935
|
-
authorName: args.authorName,
|
|
13936
|
-
authorEmail: args.authorEmail,
|
|
13937
|
-
cache: cache2
|
|
13938
|
-
});
|
|
13939
|
-
return snap.id;
|
|
13940
|
-
}
|
|
13941
|
-
async function currentBranchOrThrow(dir) {
|
|
13942
|
-
const branch = await git2.currentBranch({ fs: gitFs, dir });
|
|
13943
|
-
if (!branch)
|
|
13944
|
-
throw new Error(MSG_NO_BRANCH);
|
|
13945
|
-
return branch;
|
|
13946
|
-
}
|
|
13947
|
-
async function resolveRefOrNull(dir, ref) {
|
|
13948
|
-
try {
|
|
13949
|
-
return await git2.resolveRef({ fs: gitFs, dir, ref });
|
|
13950
|
-
} catch {
|
|
13951
|
-
return null;
|
|
13952
|
-
}
|
|
13953
|
-
}
|
|
13954
|
-
async function guardTrackingRef(dir, ref, cache2, fn) {
|
|
13955
|
-
const before = await resolveRefOrNull(dir, ref);
|
|
13956
|
-
try {
|
|
13957
|
-
return await fn();
|
|
13958
|
-
} catch (e) {
|
|
13959
|
-
try {
|
|
13960
|
-
const after = await resolveRefOrNull(dir, ref);
|
|
13961
|
-
if (after && after !== before) {
|
|
13962
|
-
const landed = await git2.readObject({ fs: gitFs, dir, oid: after, cache: cache2 }).then(() => true, () => false);
|
|
13963
|
-
if (!landed) {
|
|
13964
|
-
if (before)
|
|
13965
|
-
await git2.writeRef({ fs: gitFs, dir, ref, value: before, force: true });
|
|
13966
|
-
else
|
|
13967
|
-
await git2.deleteRef({ fs: gitFs, dir, ref });
|
|
13968
|
-
}
|
|
13969
|
-
}
|
|
13970
|
-
} catch {}
|
|
13971
|
-
throw e;
|
|
13972
|
-
}
|
|
13973
|
-
}
|
|
13974
|
-
async function fetchRemoteTip(dir, branch, transport, http3, cache2) {
|
|
13975
|
-
try {
|
|
13976
|
-
const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
|
|
13977
|
-
const result = await guardTrackingRef(dir, trackingRef, cache2, () => git2.fetch({
|
|
13978
|
-
fs: gitFs,
|
|
13979
|
-
http: http3,
|
|
13980
|
-
dir,
|
|
13981
|
-
cache: cache2,
|
|
13982
|
-
remote: transport.remote,
|
|
13983
|
-
ref: trackingRef,
|
|
13984
|
-
remoteRef: branch,
|
|
13985
|
-
singleBranch: true,
|
|
13986
|
-
tags: false,
|
|
13987
|
-
...onAuthFor(transport.credential)
|
|
13988
|
-
}));
|
|
13989
|
-
return result.fetchHead ?? null;
|
|
13990
|
-
} catch (e) {
|
|
13991
|
-
const code = e?.code;
|
|
13992
|
-
if (code === "NoRefspecError")
|
|
13993
|
-
return null;
|
|
13994
|
-
if (code === "NotFoundError" || e?.data?.statusCode === 404) {
|
|
13995
|
-
const err = new Error("The online repository couldn't be accessed with the saved connection. Reconnect and try again.", { cause: e });
|
|
13996
|
-
err.code = "HttpError";
|
|
13997
|
-
err.data = { statusCode: 401 };
|
|
13998
|
-
throw err;
|
|
13999
|
-
}
|
|
14000
|
-
throw e;
|
|
14001
|
-
}
|
|
14002
|
-
}
|
|
14003
|
-
function short(oid) {
|
|
14004
|
-
return oid ? oid.slice(0, 8) : "none";
|
|
14005
|
-
}
|
|
14006
|
-
|
|
14007
|
-
// src/lib/remote-auth/clone.ts
|
|
14008
|
-
function sanitizeCloneFolderName(name) {
|
|
14009
|
-
return String(name ?? "").replace(/[\\/]/g, "-").replace(/^\.+/, "").trim();
|
|
14010
|
-
}
|
|
14011
|
-
async function assertCloneTarget(dir) {
|
|
14012
|
-
try {
|
|
14013
|
-
const entries = await readdir9(dir);
|
|
14014
|
-
if (entries.length > 0) {
|
|
14015
|
-
throw new Error("That folder already has files in it. Choose an empty folder for the book.");
|
|
14016
|
-
}
|
|
14017
|
-
} catch (e) {
|
|
14018
|
-
if (e.code === "ENOENT")
|
|
14019
|
-
return;
|
|
14020
|
-
throw e;
|
|
14021
|
-
}
|
|
14022
|
-
}
|
|
14023
|
-
function friendlyCloneError(e) {
|
|
14024
|
-
if (isInsecureTransportError(e)) {
|
|
14025
|
-
return new Error("That repository address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Use a secure address (starting with https), or a local loopback address for a server on this computer.", { cause: e });
|
|
14026
|
-
}
|
|
14027
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
14028
|
-
if (/401|403|auth|credential/i.test(msg)) {
|
|
14029
|
-
return new Error("GitHub didn't accept the connection for this repository. Reconnect GitHub and try again.", { cause: e });
|
|
14030
|
-
}
|
|
14031
|
-
if (/404|not found/i.test(msg)) {
|
|
14032
|
-
return new Error("That repository couldn't be found. It may have been renamed or you may no longer have access.", { cause: e });
|
|
14033
|
-
}
|
|
14034
|
-
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
|
|
14035
|
-
return new Error(OFFLINE_MESSAGE, { cause: e });
|
|
14036
|
-
}
|
|
14037
|
-
if (e instanceof Error && /already has files/i.test(e.message)) {
|
|
14038
|
-
return e;
|
|
14039
|
-
}
|
|
14040
|
-
return new Error("The book couldn't be downloaded from GitHub. Please try again.", { cause: e });
|
|
14041
|
-
}
|
|
14042
|
-
async function cloneRepository(options) {
|
|
14043
|
-
const { dir, onProgress, tokenStore } = options;
|
|
14044
|
-
if (!path24.isAbsolute(dir)) {
|
|
14045
|
-
throw new Error("cloneRepository requires an absolute destination path.");
|
|
14046
|
-
}
|
|
14047
|
-
let parsed;
|
|
14048
|
-
try {
|
|
14049
|
-
parsed = new URL(options.url);
|
|
14050
|
-
} catch {
|
|
14051
|
-
throw new Error("That repository address is not a valid web URL.");
|
|
14052
|
-
}
|
|
14053
|
-
if (!/^https?:$/.test(parsed.protocol)) {
|
|
14054
|
-
throw new Error("Only HTTPS repository addresses are supported. SSH addresses (git@…) can't be used here.");
|
|
14055
|
-
}
|
|
14056
|
-
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
14057
|
-
const credential = options.credential ?? urlCredential;
|
|
14058
|
-
if (urlCredential && tokenStore && !await tokenStore.get(urlCredential.host)) {
|
|
14059
|
-
await tokenStore.set(urlCredential.host, urlCredential);
|
|
14060
|
-
}
|
|
14061
|
-
return withRepoLock(dir, async () => {
|
|
14062
|
-
await assertCloneTarget(dir);
|
|
14063
|
-
const dirExistedBefore = gitFs.existsSync(dir);
|
|
14064
|
-
await mkdir13(dir, { recursive: true });
|
|
14065
|
-
try {
|
|
14066
|
-
await git3.clone({
|
|
14067
|
-
fs: gitFs,
|
|
14068
|
-
http: options.httpClient ?? defaultGitHttp,
|
|
14069
|
-
dir,
|
|
14070
|
-
url: cleanUrl,
|
|
14071
|
-
singleBranch: true,
|
|
14072
|
-
...options.branch ? { ref: options.branch } : {},
|
|
14073
|
-
...options.depth ? { depth: options.depth } : {},
|
|
14074
|
-
...onAuthFor(credential),
|
|
14075
|
-
...onProgress ? {
|
|
14076
|
-
onProgress: (p) => onProgress({
|
|
14077
|
-
phase: p.phase,
|
|
14078
|
-
loaded: p.loaded,
|
|
14079
|
-
...p.total ? { total: p.total } : {}
|
|
14080
|
-
})
|
|
14081
|
-
} : {}
|
|
14082
|
-
});
|
|
14083
|
-
} catch (e) {
|
|
14084
|
-
const friendly = friendlyCloneError(e);
|
|
14085
|
-
if (!dirExistedBefore) {
|
|
14086
|
-
try {
|
|
14087
|
-
await rm6(dir, { recursive: true, force: true });
|
|
14088
|
-
} catch {
|
|
14089
|
-
throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
|
|
14090
|
-
}
|
|
14091
|
-
}
|
|
14092
|
-
throw friendly;
|
|
14093
|
-
}
|
|
14094
|
-
let branch;
|
|
14095
|
-
try {
|
|
14096
|
-
branch = await git3.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14097
|
-
} catch {
|
|
14098
|
-
branch = undefined;
|
|
14099
|
-
}
|
|
14100
|
-
return { projectDir: dir, ...branch ? { branch } : {} };
|
|
14101
|
-
});
|
|
14102
|
-
}
|
|
14103
|
-
// src/lib/remote-auth/test-access.ts
|
|
14104
|
-
import git4 from "isomorphic-git";
|
|
14105
|
-
import httpNode2 from "isomorphic-git/http/node";
|
|
14106
|
-
function isSshRemoteUrl(url) {
|
|
14107
|
-
const trimmed = url.trim();
|
|
14108
|
-
if (/^ssh:\/\//i.test(trimmed))
|
|
14109
|
-
return true;
|
|
14110
|
-
return /^[\w.-]+@[\w.][\w.-]*:/.test(trimmed) || /^[\w.-]+@\[/.test(trimmed);
|
|
14111
|
-
}
|
|
14112
|
-
var FAILURE_MESSAGES = {
|
|
14113
|
-
auth: "The Git server didn't accept the saved connection. Reconnect to this server with a current access token and try again.",
|
|
14114
|
-
"not-found": "The server answered, but no repository was found at that address. It may have been moved, renamed, or you may no longer have access.",
|
|
14115
|
-
unreachable: "Couldn't reach the Git server. Check your internet connection (and VPN, if this is a private server), then try again.",
|
|
14116
|
-
"ssh-unsupported": "This book's online address uses SSH (git@…), which gutterpress can't check or sync with. Everything on this computer still works — sync with your usual Git tool.",
|
|
14117
|
-
"insecure-transport": "This address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to use a saved connection.",
|
|
14118
|
-
tls: "The server's security certificate couldn't be verified. If this is a private server with its own certificate, ask its administrator about trusting it (NODE_EXTRA_CA_CERTS).",
|
|
14119
|
-
unknown: "The connection test failed unexpectedly. The server may not be a Git server, or it may be temporarily unavailable."
|
|
14120
|
-
};
|
|
14121
|
-
function failure(reason) {
|
|
14122
|
-
return { ok: false, reason, message: FAILURE_MESSAGES[reason] };
|
|
14123
|
-
}
|
|
14124
|
-
function classifyRemoteAccessError(e) {
|
|
14125
|
-
if (isInsecureTransportError(e))
|
|
14126
|
-
return failure("insecure-transport");
|
|
14127
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
14128
|
-
if (/401|403|auth|credential|denied|cancel/i.test(msg))
|
|
14129
|
-
return failure("auth");
|
|
14130
|
-
if (/404|not found/i.test(msg))
|
|
14131
|
-
return failure("not-found");
|
|
14132
|
-
if (/certificate|self.signed|unable_to_verify|cert_|tls|ssl/i.test(msg) && !/fetch failed/i.test(msg)) {
|
|
14133
|
-
return failure("tls");
|
|
14134
|
-
}
|
|
14135
|
-
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|EHOSTUNREACH|network|fetch failed|timed out/i.test(msg)) {
|
|
14136
|
-
return failure("unreachable");
|
|
14137
|
-
}
|
|
14138
|
-
return failure("unknown");
|
|
14139
|
-
}
|
|
14140
|
-
async function testRemoteAccess(options) {
|
|
14141
|
-
if (isSshRemoteUrl(options.url))
|
|
14142
|
-
return failure("ssh-unsupported");
|
|
14143
|
-
let parsed;
|
|
14144
|
-
try {
|
|
14145
|
-
parsed = new URL(options.url);
|
|
14146
|
-
} catch {
|
|
14147
|
-
return failure("unknown");
|
|
14148
|
-
}
|
|
14149
|
-
if (!/^https?:$/.test(parsed.protocol))
|
|
14150
|
-
return failure("ssh-unsupported");
|
|
14151
|
-
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
14152
|
-
const credential = options.credential ?? urlCredential;
|
|
14153
|
-
const timeoutMs = options.timeoutMs ?? 15000;
|
|
14154
|
-
let timer;
|
|
14155
|
-
const timeout = new Promise((resolve9) => {
|
|
14156
|
-
timer = setTimeout(() => resolve9(failure("unreachable")), timeoutMs);
|
|
14157
|
-
});
|
|
14158
|
-
const probe = (async () => {
|
|
14159
|
-
try {
|
|
14160
|
-
const refs = await git4.listServerRefs({
|
|
14161
|
-
http: options.httpClient ?? httpNode2,
|
|
14162
|
-
url: cleanUrl,
|
|
14163
|
-
protocolVersion: 1,
|
|
14164
|
-
symrefs: true,
|
|
14165
|
-
...onAuthFor(credential),
|
|
14166
|
-
...credential ? { onAuthFailure: () => ({ cancel: true }) } : {}
|
|
14167
|
-
});
|
|
14168
|
-
const head = refs.find((r) => r.ref === "HEAD" && r.target);
|
|
14169
|
-
const defaultBranch = head?.target?.replace(/^refs\/heads\//, "");
|
|
14170
|
-
return {
|
|
14171
|
-
ok: true,
|
|
14172
|
-
refCount: refs.filter((r) => r.ref !== "HEAD").length,
|
|
14173
|
-
...defaultBranch ? { defaultBranch } : {}
|
|
14174
|
-
};
|
|
14175
|
-
} catch (e) {
|
|
14176
|
-
return classifyRemoteAccessError(e);
|
|
14177
|
-
}
|
|
14178
|
-
})();
|
|
14179
|
-
try {
|
|
14180
|
-
return await Promise.race([probe, timeout]);
|
|
14181
|
-
} finally {
|
|
14182
|
-
clearTimeout(timer);
|
|
14183
|
-
}
|
|
14184
|
-
}
|
|
14185
|
-
// src/lib/remote-auth/generic-auth.ts
|
|
14186
|
-
function normalizeForgeHost(input) {
|
|
14187
|
-
return credentialHostKey(input);
|
|
14188
|
-
}
|
|
14189
|
-
function knownForgeTokenUrl(host) {
|
|
14190
|
-
const h = normalizeForgeHost(host);
|
|
14191
|
-
if (!h)
|
|
14192
|
-
return null;
|
|
14193
|
-
const name = h.split(":")[0];
|
|
14194
|
-
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
14195
|
-
return null;
|
|
14196
|
-
if (name === "bitbucket.org") {
|
|
14197
|
-
return "https://bitbucket.org/account/settings/app-passwords/";
|
|
14198
|
-
}
|
|
14199
|
-
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com")) {
|
|
14200
|
-
return "https://dev.azure.com/_usersSettings/tokens";
|
|
14201
|
-
}
|
|
14202
|
-
if (name.includes("gitlab")) {
|
|
14203
|
-
return `https://${h}/-/user_settings/personal_access_tokens`;
|
|
14204
|
-
}
|
|
14205
|
-
if (name.includes("gitea") || name.includes("forgejo")) {
|
|
14206
|
-
return `https://${h}/user/settings/applications`;
|
|
14207
|
-
}
|
|
14208
|
-
return null;
|
|
14209
|
-
}
|
|
14210
|
-
function connectError(result, host) {
|
|
14211
|
-
switch (result.reason) {
|
|
14212
|
-
case "auth":
|
|
14213
|
-
return new Error("The Git server didn't accept that access token. Check the token (and username, if your server needs one) and try again.");
|
|
14214
|
-
case "ssh-unsupported":
|
|
14215
|
-
return new Error("Enter the server's web address (starting with https://), not an SSH address (git@…).");
|
|
14216
|
-
case "unreachable":
|
|
14217
|
-
return new Error(`Couldn't reach ${host}. Check the server address and your connection, then try again.`);
|
|
14218
|
-
case "tls":
|
|
14219
|
-
return new Error(result.message);
|
|
14220
|
-
case "not-found":
|
|
14221
|
-
return new Error("The server answered, but that repository wasn't found. Check the repository address and that the token has access to it.");
|
|
14222
|
-
default:
|
|
14223
|
-
return new Error(`Couldn't verify the connection to ${host}. Check the server address and try again.`);
|
|
14224
|
-
}
|
|
14225
|
-
}
|
|
14226
|
-
async function connectGenericHost(input, options = {}) {
|
|
14227
|
-
const host = normalizeForgeHost(input.host);
|
|
14228
|
-
if (!host) {
|
|
14229
|
-
throw new Error("Enter your Git server's web address (like https://git.example.com).");
|
|
14230
|
-
}
|
|
14231
|
-
const token = input.token?.trim();
|
|
14232
|
-
if (!token) {
|
|
14233
|
-
throw new Error("Paste an access token from your Git server to connect.");
|
|
14234
|
-
}
|
|
14235
|
-
const username = input.username?.trim() || undefined;
|
|
14236
|
-
const candidate = {
|
|
14237
|
-
host,
|
|
14238
|
-
kind: "token",
|
|
14239
|
-
token,
|
|
14240
|
-
...username ? { username } : {},
|
|
14241
|
-
label: username ? `${host} — ${username}` : host,
|
|
14242
|
-
createdAt: Date.now()
|
|
14243
|
-
};
|
|
14244
|
-
const probeUrl = input.repoUrl?.trim() || `https://${host}/`;
|
|
14245
|
-
const result = await testRemoteAccess({
|
|
14246
|
-
url: probeUrl,
|
|
14247
|
-
credential: candidate,
|
|
14248
|
-
...options.httpClient ? { httpClient: options.httpClient } : {},
|
|
14249
|
-
...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
|
|
14250
|
-
});
|
|
14251
|
-
if (!result.ok) {
|
|
14252
|
-
const rootProbe = !input.repoUrl?.trim();
|
|
14253
|
-
const inconclusiveButReachable = rootProbe && (result.reason === "not-found" || result.reason === "unknown");
|
|
14254
|
-
if (!inconclusiveButReachable)
|
|
14255
|
-
throw connectError(result, host);
|
|
14256
|
-
}
|
|
14257
|
-
return candidate;
|
|
14258
|
-
}
|
|
14259
|
-
// src/lib/remote-auth/diagnose.ts
|
|
14260
|
-
function parseRemoteOrigin(url) {
|
|
14261
|
-
const trimmed = url.trim();
|
|
14262
|
-
if (!trimmed)
|
|
14263
|
-
return { protocol: "none" };
|
|
14264
|
-
const scp = /^[\w.-]+@([\w.][\w.-]*):/.exec(trimmed);
|
|
14265
|
-
if (scp)
|
|
14266
|
-
return { protocol: "ssh", host: credentialHostKey(trimmed) };
|
|
14267
|
-
try {
|
|
14268
|
-
const parsed = new URL(trimmed);
|
|
14269
|
-
const host = credentialHostKey(trimmed);
|
|
14270
|
-
if (/^https?:$/.test(parsed.protocol))
|
|
14271
|
-
return { protocol: "https", host };
|
|
14272
|
-
if (/^ssh:$/.test(parsed.protocol) || isSshRemoteUrl(trimmed)) {
|
|
14273
|
-
return { protocol: "ssh", host };
|
|
14274
|
-
}
|
|
14275
|
-
return { protocol: "none" };
|
|
14276
|
-
} catch {
|
|
14277
|
-
return { protocol: "none" };
|
|
14278
|
-
}
|
|
14279
|
-
}
|
|
14280
|
-
function forgeKindForHost(host) {
|
|
14281
|
-
const name = host.toLowerCase().split(":")[0];
|
|
14282
|
-
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
14283
|
-
return "github";
|
|
14284
|
-
if (name === "bitbucket.org" || name.includes("bitbucket"))
|
|
14285
|
-
return "bitbucket";
|
|
14286
|
-
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com"))
|
|
14287
|
-
return "azure";
|
|
14288
|
-
if (name.includes("gitlab"))
|
|
14289
|
-
return "gitlab";
|
|
14290
|
-
if (name.includes("forgejo"))
|
|
14291
|
-
return "forgejo";
|
|
14292
|
-
if (name.includes("gitea"))
|
|
14293
|
-
return "gitea";
|
|
14294
|
-
return "generic";
|
|
14295
|
-
}
|
|
14296
|
-
async function diagnoseProjectRemote(projectDir, options = {}) {
|
|
14297
|
-
const classification = options.source ?? await detectProjectSource(projectDir);
|
|
14298
|
-
const rawRemoteUrl = classification.type === "local-git-folder" ? classification.remoteUrl : undefined;
|
|
14299
|
-
const branch = classification.type === "local-git-folder" ? classification.branch : undefined;
|
|
14300
|
-
if (!rawRemoteUrl) {
|
|
14301
|
-
return {
|
|
14302
|
-
classification,
|
|
14303
|
-
remoteProtocol: "none",
|
|
14304
|
-
...branch ? { branch } : {},
|
|
14305
|
-
credentialPresent: false,
|
|
14306
|
-
provider: null,
|
|
14307
|
-
tokenSettingsUrl: null,
|
|
14308
|
-
canSync: false,
|
|
14309
|
-
guidance: "local-only"
|
|
14310
|
-
};
|
|
14311
|
-
}
|
|
14312
|
-
const { protocol, host } = parseRemoteOrigin(rawRemoteUrl);
|
|
14313
|
-
const urlExtraction = protocol === "https" ? extractUrlCredential(rawRemoteUrl) : null;
|
|
14314
|
-
const remoteUrl = urlExtraction ? urlExtraction.cleanUrl : rawRemoteUrl;
|
|
14315
|
-
const sanitizedClassification = classification.type === "local-git-folder" && classification.remoteUrl ? { ...classification, remoteUrl } : classification;
|
|
14316
|
-
let credentialPresent = false;
|
|
14317
|
-
if (host && options.tokenStore) {
|
|
14318
|
-
try {
|
|
14319
|
-
credentialPresent = await options.tokenStore.get(host) !== null;
|
|
14320
|
-
} catch {
|
|
14321
|
-
credentialPresent = false;
|
|
14322
|
-
}
|
|
14323
|
-
}
|
|
14324
|
-
if (!credentialPresent && urlExtraction?.credential)
|
|
14325
|
-
credentialPresent = true;
|
|
14326
|
-
const provider = host && protocol !== "none" ? forgeKindForHost(host) : null;
|
|
14327
|
-
const canSync = protocol === "https" && credentialPresent;
|
|
14328
|
-
let guidance;
|
|
14329
|
-
if (protocol === "ssh")
|
|
14330
|
-
guidance = "ssh-use-own-tools";
|
|
14331
|
-
else if (protocol !== "https")
|
|
14332
|
-
guidance = "local-only";
|
|
14333
|
-
else if (credentialPresent)
|
|
14334
|
-
guidance = "ready-to-sync";
|
|
14335
|
-
else if (provider === "github")
|
|
14336
|
-
guidance = "connect-github-to-sync";
|
|
14337
|
-
else
|
|
14338
|
-
guidance = "https-connect-server";
|
|
14339
|
-
return {
|
|
14340
|
-
classification: sanitizedClassification,
|
|
14341
|
-
remoteUrl,
|
|
14342
|
-
...host ? { remoteHost: host } : {},
|
|
14343
|
-
remoteProtocol: protocol === "none" ? "none" : protocol,
|
|
14344
|
-
...branch ? { branch } : {},
|
|
14345
|
-
credentialPresent,
|
|
14346
|
-
provider,
|
|
14347
|
-
tokenSettingsUrl: host ? knownForgeTokenUrl(host) : null,
|
|
14348
|
-
canSync,
|
|
14349
|
-
guidance
|
|
14350
|
-
};
|
|
14351
|
-
}
|
|
14352
|
-
// src/lib/remote-auth/sync.ts
|
|
14353
|
-
import git6 from "isomorphic-git";
|
|
14354
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
14355
|
-
|
|
14356
|
-
// src/lib/remote-auth/converge-merge.ts
|
|
14357
|
-
import { mkdir as mkdir14, unlink as unlink3, writeFile as writeFile13 } from "node:fs/promises";
|
|
14358
|
-
import path25 from "node:path";
|
|
14359
|
-
import git5 from "isomorphic-git";
|
|
14360
|
-
import diff3Merge from "diff3";
|
|
14361
|
-
function isMergeConflictError(e) {
|
|
14362
|
-
return e?.code === "MergeConflictError";
|
|
14363
|
-
}
|
|
14364
|
-
function isCheckoutConflict(e) {
|
|
14365
|
-
return e?.code === "CheckoutConflictError";
|
|
14366
|
-
}
|
|
14367
|
-
var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
|
|
14368
|
-
var CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
|
|
14369
|
-
var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
|
|
14370
|
-
var MARKER_UNSAFE_EXT = /\.svg$/i;
|
|
14371
|
-
var OUR_LABEL = "your version";
|
|
14372
|
-
var THEIR_LABEL = "online version";
|
|
14373
|
-
function stringLooksBinary(s) {
|
|
14374
|
-
return s.includes("\x00");
|
|
14375
|
-
}
|
|
14376
|
-
var LINEBREAKS = /^.*(\r?\n|$)/gm;
|
|
14377
|
-
function mergeWithMarkers(baseContent, ourContent, theirContent) {
|
|
14378
|
-
const result = diff3Merge(ourContent.match(LINEBREAKS) ?? [], baseContent.match(LINEBREAKS) ?? [], theirContent.match(LINEBREAKS) ?? []);
|
|
14379
|
-
let merged = "";
|
|
14380
|
-
for (const item of result) {
|
|
14381
|
-
if (item.ok)
|
|
14382
|
-
merged += item.ok.join("");
|
|
14383
|
-
if (item.conflict) {
|
|
14384
|
-
merged += `<<<<<<< ${OUR_LABEL}
|
|
14385
|
-
`;
|
|
14386
|
-
merged += item.conflict.a.join("");
|
|
14387
|
-
merged += `=======
|
|
14388
|
-
`;
|
|
14389
|
-
merged += item.conflict.b.join("");
|
|
14390
|
-
merged += `>>>>>>> ${THEIR_LABEL}
|
|
14391
|
-
`;
|
|
14392
|
-
}
|
|
14393
|
-
}
|
|
14394
|
-
return merged;
|
|
14395
|
-
}
|
|
14396
|
-
async function readBlob(dir, cache2, oid, filepath) {
|
|
14397
|
-
return git5.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
|
|
14398
|
-
}
|
|
14399
|
-
async function writeTreeFile(dir, filepath, content) {
|
|
14400
|
-
const abs = path25.join(dir, filepath);
|
|
14401
|
-
await mkdir14(path25.dirname(abs), { recursive: true });
|
|
14402
|
-
await writeFile13(abs, content);
|
|
14403
|
-
}
|
|
14404
|
-
async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
|
|
14405
|
-
const bases = await git5.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
|
|
14406
|
-
const base = bases[0];
|
|
14407
|
-
if (!base && !allowUnrelated)
|
|
14408
|
-
return null;
|
|
14409
|
-
const out = [];
|
|
14410
|
-
await git5.walk({
|
|
14411
|
-
fs: gitFs,
|
|
14412
|
-
dir,
|
|
14413
|
-
cache: cache2,
|
|
14414
|
-
trees: [
|
|
14415
|
-
git5.TREE({ ref: ourTip }),
|
|
14416
|
-
git5.TREE({ ref: theirTip }),
|
|
14417
|
-
...base ? [git5.TREE({ ref: base })] : []
|
|
14418
|
-
],
|
|
14419
|
-
map: async (filepath, entries) => {
|
|
14420
|
-
if (filepath === ".")
|
|
14421
|
-
return true;
|
|
14422
|
-
const [ours, theirs, inBase] = entries;
|
|
14423
|
-
const types = await Promise.all([ours, theirs, inBase].map((e) => e ? e.type() : Promise.resolve(undefined)));
|
|
14424
|
-
if (types.some((t) => t === "tree"))
|
|
14425
|
-
return true;
|
|
14426
|
-
if (!ours || !theirs || inBase)
|
|
14427
|
-
return null;
|
|
14428
|
-
const [a, b] = await Promise.all([ours.oid(), theirs.oid()]);
|
|
14429
|
-
if (a !== b)
|
|
14430
|
-
out.push(filepath);
|
|
14431
|
-
return null;
|
|
14432
|
-
}
|
|
14433
|
-
});
|
|
14434
|
-
return out;
|
|
14011
|
+
});
|
|
14012
|
+
return out;
|
|
14435
14013
|
}
|
|
14436
14014
|
async function convergeMerge(params) {
|
|
14437
14015
|
const { dir, cache: cache2, branch, theirs, author } = params;
|
|
@@ -14445,14 +14023,14 @@ async function convergeMerge(params) {
|
|
|
14445
14023
|
authorEmail: params.authorEmail,
|
|
14446
14024
|
cache: cache2
|
|
14447
14025
|
});
|
|
14448
|
-
const ourTip = await
|
|
14026
|
+
const ourTip = await git2.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14449
14027
|
const combined = new Set;
|
|
14450
14028
|
const keptBothFiles = [];
|
|
14451
14029
|
let tipBeforeMerge = ourTip;
|
|
14452
14030
|
const attempt = async () => {
|
|
14453
|
-
tipBeforeMerge = await
|
|
14031
|
+
tipBeforeMerge = await git2.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14454
14032
|
step("merge", { local: tipBeforeMerge, fetched: theirs });
|
|
14455
|
-
return
|
|
14033
|
+
return git2.merge({
|
|
14456
14034
|
fs: gitFs,
|
|
14457
14035
|
dir,
|
|
14458
14036
|
cache: cache2,
|
|
@@ -14539,37 +14117,237 @@ async function convergeMerge(params) {
|
|
|
14539
14117
|
}
|
|
14540
14118
|
step("checkout");
|
|
14541
14119
|
try {
|
|
14542
|
-
await
|
|
14543
|
-
} catch (e) {
|
|
14544
|
-
if (!isCheckoutConflict(e))
|
|
14545
|
-
throw e;
|
|
14546
|
-
logger.warn("checkout", "checkout refused; restoring branch", errorLogData(e));
|
|
14547
|
-
step("rollback", { local: tipBeforeMerge });
|
|
14548
|
-
await
|
|
14120
|
+
await git2.checkout({ fs: gitFs, dir, cache: cache2, ref: branch, force: false });
|
|
14121
|
+
} catch (e) {
|
|
14122
|
+
if (!isCheckoutConflict(e))
|
|
14123
|
+
throw e;
|
|
14124
|
+
logger.warn("checkout", "checkout refused; restoring branch", errorLogData(e));
|
|
14125
|
+
step("rollback", { local: tipBeforeMerge });
|
|
14126
|
+
await git2.writeRef({
|
|
14127
|
+
fs: gitFs,
|
|
14128
|
+
dir,
|
|
14129
|
+
ref: await git2.expandRef({ fs: gitFs, dir, ref: branch }),
|
|
14130
|
+
value: tipBeforeMerge,
|
|
14131
|
+
force: true
|
|
14132
|
+
});
|
|
14133
|
+
step("checkout");
|
|
14134
|
+
throw e;
|
|
14135
|
+
}
|
|
14136
|
+
if (postRestore.length > 0) {
|
|
14137
|
+
for (const r of postRestore) {
|
|
14138
|
+
step("restore-conflict-file", { filepath: r.path });
|
|
14139
|
+
await writeTreeFile(dir, r.path, r.bytes);
|
|
14140
|
+
}
|
|
14141
|
+
step("snapshot-restored");
|
|
14142
|
+
if (await hasPendingChanges(dir))
|
|
14143
|
+
await snapshot(CONVERGE_RESTORE_MESSAGE);
|
|
14144
|
+
}
|
|
14145
|
+
step("merged-ref");
|
|
14146
|
+
return {
|
|
14147
|
+
oid: await git2.resolveRef({ fs: gitFs, dir, ref: branch }),
|
|
14148
|
+
combinedFiles: [...combined].sort(),
|
|
14149
|
+
keptBothFiles
|
|
14150
|
+
};
|
|
14151
|
+
}
|
|
14152
|
+
|
|
14153
|
+
// src/lib/remote-auth/transport.ts
|
|
14154
|
+
import git3 from "isomorphic-git";
|
|
14155
|
+
async function repoDirFor(projectDir) {
|
|
14156
|
+
const source = await detectProjectSource(projectDir);
|
|
14157
|
+
if (source.type === "local-git-folder")
|
|
14158
|
+
return gitScopeFor(source);
|
|
14159
|
+
return projectDir;
|
|
14160
|
+
}
|
|
14161
|
+
function isCredentialTransmissionSafe(url) {
|
|
14162
|
+
try {
|
|
14163
|
+
const u = new URL(url);
|
|
14164
|
+
if (u.protocol === "https:")
|
|
14165
|
+
return true;
|
|
14166
|
+
if (u.protocol === "http:") {
|
|
14167
|
+
const host = u.hostname.toLowerCase();
|
|
14168
|
+
return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
|
|
14169
|
+
}
|
|
14170
|
+
return false;
|
|
14171
|
+
} catch {
|
|
14172
|
+
return false;
|
|
14173
|
+
}
|
|
14174
|
+
}
|
|
14175
|
+
|
|
14176
|
+
class InsecureTransportError extends Error {
|
|
14177
|
+
code = "InsecureTransport";
|
|
14178
|
+
constructor() {
|
|
14179
|
+
super("This online address isn't secure, so the saved connection wasn't sent — " + "connections are never sent over an insecure address. Switch the address " + "to a secure one (starting with https) to sync with a saved connection.");
|
|
14180
|
+
this.name = "InsecureTransportError";
|
|
14181
|
+
}
|
|
14182
|
+
}
|
|
14183
|
+
function isInsecureTransportError(e) {
|
|
14184
|
+
return e?.code === "InsecureTransport";
|
|
14185
|
+
}
|
|
14186
|
+
function onAuthFor(credential) {
|
|
14187
|
+
if (!credential)
|
|
14188
|
+
return {};
|
|
14189
|
+
return {
|
|
14190
|
+
onAuth: (url) => {
|
|
14191
|
+
if (!isCredentialTransmissionSafe(url))
|
|
14192
|
+
throw new InsecureTransportError;
|
|
14193
|
+
return {
|
|
14194
|
+
username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
|
|
14195
|
+
password: credential.token
|
|
14196
|
+
};
|
|
14197
|
+
}
|
|
14198
|
+
};
|
|
14199
|
+
}
|
|
14200
|
+
async function resolveTransport(dir, options) {
|
|
14201
|
+
const origin = await syncRemoteFor(dir);
|
|
14202
|
+
if (!origin?.url)
|
|
14203
|
+
throw new Error(MSG_NO_REMOTE);
|
|
14204
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(origin.url);
|
|
14205
|
+
let parsed;
|
|
14206
|
+
try {
|
|
14207
|
+
parsed = new URL(cleanUrl);
|
|
14208
|
+
} catch {
|
|
14209
|
+
throw new Error(MSG_SSH_REMOTE);
|
|
14210
|
+
}
|
|
14211
|
+
if (!/^https?:$/.test(parsed.protocol))
|
|
14212
|
+
throw new Error(MSG_SSH_REMOTE);
|
|
14213
|
+
const host = credentialHostKey(cleanUrl);
|
|
14214
|
+
let credential = options.credential ?? undefined;
|
|
14215
|
+
if (!credential && options.tokenStore) {
|
|
14216
|
+
try {
|
|
14217
|
+
credential = await options.tokenStore.get(host) ?? undefined;
|
|
14218
|
+
} catch {
|
|
14219
|
+
credential = undefined;
|
|
14220
|
+
}
|
|
14221
|
+
}
|
|
14222
|
+
if (!credential && urlCredential)
|
|
14223
|
+
credential = urlCredential;
|
|
14224
|
+
return {
|
|
14225
|
+
remote: origin.remote,
|
|
14226
|
+
url: cleanUrl,
|
|
14227
|
+
host,
|
|
14228
|
+
...credential ? { credential } : {}
|
|
14229
|
+
};
|
|
14230
|
+
}
|
|
14231
|
+
function classifyTransportFailure(e) {
|
|
14232
|
+
if (isInsecureTransportError(e))
|
|
14233
|
+
return "insecure_transport";
|
|
14234
|
+
const err = e;
|
|
14235
|
+
if (err?.code === "HttpError") {
|
|
14236
|
+
const status = err.data?.statusCode;
|
|
14237
|
+
if (status === 401 || status === 403 || status === 404)
|
|
14238
|
+
return "auth_required";
|
|
14239
|
+
}
|
|
14240
|
+
const msg = `${err?.message ?? String(e)} ${err?.data?.prettyDetails ?? ""}`;
|
|
14241
|
+
if (/\b401\b|\b403\b|\b404\b|unauthorized|authentication|not authorized|permission denied|forbidden|access denied|not allowed to push|pre-receive hook declined|hook declined/i.test(msg)) {
|
|
14242
|
+
return "auth_required";
|
|
14243
|
+
}
|
|
14244
|
+
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network|fetch failed|couldn't reach|socket hang ?up/i.test(msg)) {
|
|
14245
|
+
return "network_unavailable";
|
|
14246
|
+
}
|
|
14247
|
+
return null;
|
|
14248
|
+
}
|
|
14249
|
+
function failureOutcome(e, snapshotId) {
|
|
14250
|
+
const kind = classifyTransportFailure(e);
|
|
14251
|
+
const base = snapshotId ? { snapshotId } : {};
|
|
14252
|
+
if (kind === "auth_required")
|
|
14253
|
+
return { status: "auth", message: MSG_AUTH, ...base };
|
|
14254
|
+
if (kind === "network_unavailable")
|
|
14255
|
+
return { status: "offline", message: MSG_OFFLINE, ...base };
|
|
14256
|
+
if (kind === "insecure_transport") {
|
|
14257
|
+
return { status: "error", message: MSG_INSECURE_TRANSPORT, ...base };
|
|
14258
|
+
}
|
|
14259
|
+
return {
|
|
14260
|
+
status: "error",
|
|
14261
|
+
message: "Syncing didn't complete. Your work is saved on this computer — please try again.",
|
|
14262
|
+
...base
|
|
14263
|
+
};
|
|
14264
|
+
}
|
|
14265
|
+
function setupErrorMessage(e) {
|
|
14266
|
+
if (e instanceof Error && (e.message === MSG_NO_REMOTE || e.message === MSG_SSH_REMOTE || e.message === MSG_NO_BRANCH)) {
|
|
14267
|
+
return e.message;
|
|
14268
|
+
}
|
|
14269
|
+
return null;
|
|
14270
|
+
}
|
|
14271
|
+
async function snapshotBeforeAction(args) {
|
|
14272
|
+
const { projectDir, dir, cache: cache2 } = args;
|
|
14273
|
+
const hasChanges = await hasPendingChanges(dir, cache2);
|
|
14274
|
+
const staleStaging = gitFs.existsSync(snapshotStagingMarkerPath(dir));
|
|
14275
|
+
if (!hasChanges && !staleStaging)
|
|
14276
|
+
return;
|
|
14277
|
+
const snap = await snapshotWorkingTreeUnlocked({
|
|
14278
|
+
projectDir,
|
|
14279
|
+
repoRoot: dir,
|
|
14280
|
+
message: args.message?.trim() || SYNC_SNAPSHOT_MESSAGE,
|
|
14281
|
+
authorName: args.authorName,
|
|
14282
|
+
authorEmail: args.authorEmail,
|
|
14283
|
+
cache: cache2
|
|
14284
|
+
});
|
|
14285
|
+
return snap.id;
|
|
14286
|
+
}
|
|
14287
|
+
async function currentBranchOrThrow(dir) {
|
|
14288
|
+
const branch = await git3.currentBranch({ fs: gitFs, dir });
|
|
14289
|
+
if (!branch)
|
|
14290
|
+
throw new Error(MSG_NO_BRANCH);
|
|
14291
|
+
return branch;
|
|
14292
|
+
}
|
|
14293
|
+
async function resolveRefOrNull(dir, ref) {
|
|
14294
|
+
try {
|
|
14295
|
+
return await git3.resolveRef({ fs: gitFs, dir, ref });
|
|
14296
|
+
} catch {
|
|
14297
|
+
return null;
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
14300
|
+
async function guardTrackingRef(dir, ref, cache2, fn) {
|
|
14301
|
+
const before = await resolveRefOrNull(dir, ref);
|
|
14302
|
+
try {
|
|
14303
|
+
return await fn();
|
|
14304
|
+
} catch (e) {
|
|
14305
|
+
try {
|
|
14306
|
+
const after = await resolveRefOrNull(dir, ref);
|
|
14307
|
+
if (after && after !== before) {
|
|
14308
|
+
const landed = await git3.readObject({ fs: gitFs, dir, oid: after, cache: cache2 }).then(() => true, () => false);
|
|
14309
|
+
if (!landed) {
|
|
14310
|
+
if (before)
|
|
14311
|
+
await git3.writeRef({ fs: gitFs, dir, ref, value: before, force: true });
|
|
14312
|
+
else
|
|
14313
|
+
await git3.deleteRef({ fs: gitFs, dir, ref });
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
} catch {}
|
|
14317
|
+
throw e;
|
|
14318
|
+
}
|
|
14319
|
+
}
|
|
14320
|
+
async function fetchRemoteTip(dir, branch, transport, http3, cache2) {
|
|
14321
|
+
try {
|
|
14322
|
+
const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
|
|
14323
|
+
const result = await guardTrackingRef(dir, trackingRef, cache2, () => git3.fetch({
|
|
14549
14324
|
fs: gitFs,
|
|
14325
|
+
http: http3,
|
|
14550
14326
|
dir,
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14327
|
+
cache: cache2,
|
|
14328
|
+
remote: transport.remote,
|
|
14329
|
+
ref: trackingRef,
|
|
14330
|
+
remoteRef: branch,
|
|
14331
|
+
singleBranch: true,
|
|
14332
|
+
tags: false,
|
|
14333
|
+
...onAuthFor(transport.credential)
|
|
14334
|
+
}));
|
|
14335
|
+
return result.fetchHead ?? null;
|
|
14336
|
+
} catch (e) {
|
|
14337
|
+
const code = e?.code;
|
|
14338
|
+
if (code === "NoRefspecError")
|
|
14339
|
+
return null;
|
|
14340
|
+
if (code === "NotFoundError" || e?.data?.statusCode === 404) {
|
|
14341
|
+
const err = new Error("The online repository couldn't be accessed with the saved connection. Reconnect and try again.", { cause: e });
|
|
14342
|
+
err.code = "HttpError";
|
|
14343
|
+
err.data = { statusCode: 401 };
|
|
14344
|
+
throw err;
|
|
14562
14345
|
}
|
|
14563
|
-
|
|
14564
|
-
if (await hasPendingChanges(dir))
|
|
14565
|
-
await snapshot(CONVERGE_RESTORE_MESSAGE);
|
|
14346
|
+
throw e;
|
|
14566
14347
|
}
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
combinedFiles: [...combined].sort(),
|
|
14571
|
-
keptBothFiles
|
|
14572
|
-
};
|
|
14348
|
+
}
|
|
14349
|
+
function short(oid) {
|
|
14350
|
+
return oid ? oid.slice(0, 8) : "none";
|
|
14573
14351
|
}
|
|
14574
14352
|
// src/lib/remote-auth/sync.ts
|
|
14575
14353
|
function isPushRejected(e) {
|
|
@@ -14594,10 +14372,10 @@ function isDamagedObjectError(e) {
|
|
|
14594
14372
|
}
|
|
14595
14373
|
async function historyUnreadable(dir) {
|
|
14596
14374
|
try {
|
|
14597
|
-
const oid = await
|
|
14598
|
-
await
|
|
14599
|
-
await
|
|
14600
|
-
await
|
|
14375
|
+
const oid = await git4.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
|
|
14376
|
+
await git4.readCommit({ fs: gitFs, dir, oid });
|
|
14377
|
+
await git4.listFiles({ fs: gitFs, dir });
|
|
14378
|
+
await git4.listFiles({ fs: gitFs, dir, ref: "HEAD" });
|
|
14601
14379
|
return false;
|
|
14602
14380
|
} catch {
|
|
14603
14381
|
return true;
|
|
@@ -14692,7 +14470,7 @@ async function syncProject(options) {
|
|
|
14692
14470
|
step("fetch", { attempt: attempt + 1, branch, remote: transport.remote });
|
|
14693
14471
|
const remoteTip = await fetchRemoteTip(dir, branch, transport, http3, cache2);
|
|
14694
14472
|
step("local-ref", { fetched: remoteTip ?? "none" });
|
|
14695
|
-
let localTip = await
|
|
14473
|
+
let localTip = await git4.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14696
14474
|
if (push || remoteTip !== null && remoteTip !== localTip) {
|
|
14697
14475
|
step("snapshot-after-fetch");
|
|
14698
14476
|
const lateSnapshot = await snapshotBeforeAction({
|
|
@@ -14705,7 +14483,7 @@ async function syncProject(options) {
|
|
|
14705
14483
|
});
|
|
14706
14484
|
if (lateSnapshot) {
|
|
14707
14485
|
snapshotId = lateSnapshot;
|
|
14708
|
-
localTip = await
|
|
14486
|
+
localTip = await git4.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14709
14487
|
}
|
|
14710
14488
|
}
|
|
14711
14489
|
logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
|
|
@@ -14739,8 +14517,8 @@ async function syncProject(options) {
|
|
|
14739
14517
|
pulled = true;
|
|
14740
14518
|
step("compare-trees");
|
|
14741
14519
|
const [before, after] = await Promise.all([
|
|
14742
|
-
|
|
14743
|
-
|
|
14520
|
+
git4.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
|
|
14521
|
+
git4.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
|
|
14744
14522
|
]);
|
|
14745
14523
|
filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
|
|
14746
14524
|
for (const f of converge.combinedFiles)
|
|
@@ -14770,7 +14548,7 @@ async function syncProject(options) {
|
|
|
14770
14548
|
}
|
|
14771
14549
|
step("push", { branch, local: tip, fetched: remoteTip ?? "none" });
|
|
14772
14550
|
try {
|
|
14773
|
-
await
|
|
14551
|
+
await git4.push({
|
|
14774
14552
|
fs: gitFs,
|
|
14775
14553
|
http: http3,
|
|
14776
14554
|
dir,
|
|
@@ -14812,50 +14590,562 @@ async function syncProject(options) {
|
|
|
14812
14590
|
});
|
|
14813
14591
|
};
|
|
14814
14592
|
try {
|
|
14815
|
-
return finish(await run2());
|
|
14816
|
-
} catch (e) {
|
|
14817
|
-
logger.warn(stage, `${stage} failed`, errorLogData(e));
|
|
14818
|
-
return finish(failureOutcome(e));
|
|
14593
|
+
return finish(await run2());
|
|
14594
|
+
} catch (e) {
|
|
14595
|
+
logger.warn(stage, `${stage} failed`, errorLogData(e));
|
|
14596
|
+
return finish(failureOutcome(e));
|
|
14597
|
+
}
|
|
14598
|
+
}
|
|
14599
|
+
async function refreshRemoteCopies(options) {
|
|
14600
|
+
const http3 = options.httpClient ?? defaultGitHttp;
|
|
14601
|
+
try {
|
|
14602
|
+
const dir = await repoDirFor(options.projectDir);
|
|
14603
|
+
const transport = await resolveTransport(dir, {
|
|
14604
|
+
...options.credential ? { credential: options.credential } : {},
|
|
14605
|
+
...options.tokenStore ? { tokenStore: options.tokenStore } : {}
|
|
14606
|
+
});
|
|
14607
|
+
await git4.fetch({
|
|
14608
|
+
fs: gitFs,
|
|
14609
|
+
http: http3,
|
|
14610
|
+
dir,
|
|
14611
|
+
remote: transport.remote,
|
|
14612
|
+
singleBranch: false,
|
|
14613
|
+
prune: true,
|
|
14614
|
+
tags: false,
|
|
14615
|
+
...onAuthFor(transport.credential)
|
|
14616
|
+
});
|
|
14617
|
+
return { refreshed: true };
|
|
14618
|
+
} catch (e) {
|
|
14619
|
+
const message = e?.message ?? "";
|
|
14620
|
+
if (message === MSG_NO_REMOTE || message === MSG_SSH_REMOTE) {
|
|
14621
|
+
return { refreshed: false, reason: "no-remote" };
|
|
14622
|
+
}
|
|
14623
|
+
const status = e?.data?.statusCode;
|
|
14624
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
14625
|
+
return { refreshed: false, reason: "auth" };
|
|
14626
|
+
}
|
|
14627
|
+
return { refreshed: false, reason: "offline" };
|
|
14628
|
+
}
|
|
14629
|
+
}
|
|
14630
|
+
|
|
14631
|
+
// src/lib/remote-auth/online-backup.ts
|
|
14632
|
+
var API_BASE3 = "https://api.github.com";
|
|
14633
|
+
var REQUEST_TIMEOUT_MS4 = 15000;
|
|
14634
|
+
var MAX_NAME_LENGTH = 100;
|
|
14635
|
+
var MSG = {
|
|
14636
|
+
notConnected: "Connect your GitHub account first (Settings > Accounts), then set up online backup.",
|
|
14637
|
+
auth: "GitHub didn't accept the saved connection. Reconnect GitHub in Settings > Accounts and try again.",
|
|
14638
|
+
offline: "Gutterpress couldn't reach GitHub. Check your internet connection and try again. Nothing was changed.",
|
|
14639
|
+
noHistory: "Turn on version history for this book first. Online backup copies the versions Gutterpress keeps on this computer.",
|
|
14640
|
+
alreadyHasRemote: "This book already has an online copy.",
|
|
14641
|
+
noName: "Give the online copy a name.",
|
|
14642
|
+
badName: "A repository name can only use letters, numbers, hyphens (-), underscores (_) and dots (.), and be at most 100 characters.",
|
|
14643
|
+
notFound: "Gutterpress couldn't find that repository on GitHub, or your account can't see it. Choose another one.",
|
|
14644
|
+
noPermission: "Your GitHub account isn't allowed to add to that repository (it may be archived or read-only). Choose another one.",
|
|
14645
|
+
noCreatePermission: "GitHub wouldn't let Gutterpress create a repository with this connection. Create an empty repository on github.com yourself, then choose it from your existing repositories here.",
|
|
14646
|
+
notEmpty: "That repository already has files in it, and online backup needs an empty one. Choose an empty repository, or create a new one.",
|
|
14647
|
+
pushFailed: "Your book couldn't be copied to GitHub. Your writing is safe on this computer. Please try again."
|
|
14648
|
+
};
|
|
14649
|
+
var nameTaken = (name) => `You already have a repository named "${name}" on GitHub. Pick a different name, or choose that repository from your existing empty repositories.`;
|
|
14650
|
+
|
|
14651
|
+
class BackupFailure extends Error {
|
|
14652
|
+
reason;
|
|
14653
|
+
repository;
|
|
14654
|
+
constructor(reason, message, repository) {
|
|
14655
|
+
super(message);
|
|
14656
|
+
this.reason = reason;
|
|
14657
|
+
this.repository = repository;
|
|
14658
|
+
}
|
|
14659
|
+
}
|
|
14660
|
+
function isValidRepositoryName(name) {
|
|
14661
|
+
return name.length > 0 && name.length <= MAX_NAME_LENGTH && /^[A-Za-z0-9._-]+$/.test(name) && name !== "." && name !== "..";
|
|
14662
|
+
}
|
|
14663
|
+
function toRemoteRepository(r) {
|
|
14664
|
+
return {
|
|
14665
|
+
owner: r.owner.login,
|
|
14666
|
+
name: r.name,
|
|
14667
|
+
fullName: r.full_name,
|
|
14668
|
+
private: r.private,
|
|
14669
|
+
defaultBranch: r.default_branch,
|
|
14670
|
+
htmlUrl: r.html_url
|
|
14671
|
+
};
|
|
14672
|
+
}
|
|
14673
|
+
async function request(fetchImpl, method, path26, token, body) {
|
|
14674
|
+
return withFetchTimeout({ timeoutMs: REQUEST_TIMEOUT_MS4, offlineMessage: MSG.offline }, (signal) => fetchImpl(`${API_BASE3}${path26}`, {
|
|
14675
|
+
method,
|
|
14676
|
+
headers: {
|
|
14677
|
+
...githubApiHeaders(token),
|
|
14678
|
+
...body === undefined ? {} : { "Content-Type": "application/json" }
|
|
14679
|
+
},
|
|
14680
|
+
...body === undefined ? {} : { body: JSON.stringify(body) },
|
|
14681
|
+
signal
|
|
14682
|
+
})).catch((e) => {
|
|
14683
|
+
throw new BackupFailure("offline", e instanceof Error ? e.message : MSG.offline);
|
|
14684
|
+
});
|
|
14685
|
+
}
|
|
14686
|
+
async function createRepository(fetchImpl, token, name, isPrivate) {
|
|
14687
|
+
const res = await request(fetchImpl, "POST", "/user/repos", token, {
|
|
14688
|
+
name,
|
|
14689
|
+
private: isPrivate,
|
|
14690
|
+
auto_init: false,
|
|
14691
|
+
description: "Online backup made with Gutterpress"
|
|
14692
|
+
});
|
|
14693
|
+
if (res.status === 201 || res.status === 200)
|
|
14694
|
+
return await res.json();
|
|
14695
|
+
if (res.status === 401)
|
|
14696
|
+
throw new BackupFailure("auth", MSG.auth);
|
|
14697
|
+
if (res.status === 403)
|
|
14698
|
+
throw new BackupFailure("no-permission", MSG.noCreatePermission);
|
|
14699
|
+
if (res.status === 422) {
|
|
14700
|
+
const body = await res.json().catch(() => ({}));
|
|
14701
|
+
const taken = (body.errors ?? []).some((e) => /already exists/i.test(e.message ?? ""));
|
|
14702
|
+
throw new BackupFailure("name-taken", taken ? nameTaken(name) : MSG.badName);
|
|
14703
|
+
}
|
|
14704
|
+
throw new BackupFailure("push-failed", `GitHub couldn't create the repository (error ${res.status}). Please try again.`);
|
|
14705
|
+
}
|
|
14706
|
+
async function loadEmptyRepository(fetchImpl, credential, owner, name) {
|
|
14707
|
+
const path26 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
|
14708
|
+
const res = await request(fetchImpl, "GET", path26, credential.token);
|
|
14709
|
+
if (res.status === 401)
|
|
14710
|
+
throw new BackupFailure("auth", MSG.auth);
|
|
14711
|
+
if (res.status === 404)
|
|
14712
|
+
throw new BackupFailure("not-found", MSG.notFound);
|
|
14713
|
+
if (res.status === 403)
|
|
14714
|
+
throw new BackupFailure("no-permission", MSG.noPermission);
|
|
14715
|
+
if (!res.ok) {
|
|
14716
|
+
throw new BackupFailure("push-failed", `GitHub returned an unexpected error (error ${res.status}). Please try again.`);
|
|
14717
|
+
}
|
|
14718
|
+
const repo = await res.json();
|
|
14719
|
+
if (repo.archived || repo.permissions?.push === false) {
|
|
14720
|
+
throw new BackupFailure("no-permission", MSG.noPermission);
|
|
14721
|
+
}
|
|
14722
|
+
const branches = await listGitHubBranches(credential, owner, name, { fetchImpl }).catch((e) => {
|
|
14723
|
+
const msg = e instanceof Error ? e.message : "";
|
|
14724
|
+
throw new BackupFailure(/reconnect/i.test(msg) ? "auth" : /reach/i.test(msg) ? "offline" : "push-failed", msg || MSG.pushFailed);
|
|
14725
|
+
});
|
|
14726
|
+
if (branches.length > 0)
|
|
14727
|
+
throw new BackupFailure("not-empty", MSG.notEmpty, toRemoteRepository(repo));
|
|
14728
|
+
return repo;
|
|
14729
|
+
}
|
|
14730
|
+
async function run2(options) {
|
|
14731
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
14732
|
+
const credential = options.credential ?? await options.tokenStore?.get(GITHUB_HOST).catch(() => null) ?? undefined;
|
|
14733
|
+
if (!credential)
|
|
14734
|
+
throw new BackupFailure("not-connected", MSG.notConnected);
|
|
14735
|
+
const source = await detectProjectSource(options.projectDir);
|
|
14736
|
+
if (source.type !== "local-git-folder")
|
|
14737
|
+
throw new BackupFailure("no-history", MSG.noHistory);
|
|
14738
|
+
if (source.hasRemote)
|
|
14739
|
+
throw new BackupFailure("already-has-remote", MSG.alreadyHasRemote);
|
|
14740
|
+
const dir = await repoDirFor(options.projectDir);
|
|
14741
|
+
const head = await git5.resolveRef({ fs: gitFs, dir, ref: "HEAD" }).catch(() => null);
|
|
14742
|
+
if (!head)
|
|
14743
|
+
throw new BackupFailure("no-history", MSG.noHistory);
|
|
14744
|
+
const { target } = options;
|
|
14745
|
+
let repo;
|
|
14746
|
+
if (target.kind === "create") {
|
|
14747
|
+
const name = target.name.trim();
|
|
14748
|
+
if (!name)
|
|
14749
|
+
throw new BackupFailure("invalid-name", MSG.noName);
|
|
14750
|
+
if (!isValidRepositoryName(name))
|
|
14751
|
+
throw new BackupFailure("invalid-name", MSG.badName);
|
|
14752
|
+
repo = await createRepository(fetchImpl, credential.token, name, target.private ?? true);
|
|
14753
|
+
} else {
|
|
14754
|
+
repo = await loadEmptyRepository(fetchImpl, credential, target.owner, target.name);
|
|
14755
|
+
}
|
|
14756
|
+
const repository = toRemoteRepository(repo);
|
|
14757
|
+
if (!/^https?:\/\//.test(repo.clone_url)) {
|
|
14758
|
+
throw new BackupFailure("push-failed", MSG.pushFailed, repository);
|
|
14759
|
+
}
|
|
14760
|
+
await withRepoLock(dir, () => git5.addRemote({ fs: gitFs, dir, remote: "origin", url: repo.clone_url }));
|
|
14761
|
+
const sync = await syncProject({
|
|
14762
|
+
projectDir: options.projectDir,
|
|
14763
|
+
credential,
|
|
14764
|
+
...options.authorName ? { authorName: options.authorName } : {},
|
|
14765
|
+
...options.authorEmail ? { authorEmail: options.authorEmail } : {},
|
|
14766
|
+
...options.httpClient ? { httpClient: options.httpClient } : {},
|
|
14767
|
+
...options.logFile ? { logFile: options.logFile } : {}
|
|
14768
|
+
});
|
|
14769
|
+
if (sync.status === "synced" || sync.status === "up-to-date") {
|
|
14770
|
+
return { status: "connected", repository, sync };
|
|
14771
|
+
}
|
|
14772
|
+
await withRepoLock(dir, () => git5.deleteRemote({ fs: gitFs, dir, remote: "origin" })).catch(() => {});
|
|
14773
|
+
if (sync.status === "auth")
|
|
14774
|
+
throw new BackupFailure("auth", sync.message, repository);
|
|
14775
|
+
if (sync.status === "offline")
|
|
14776
|
+
throw new BackupFailure("offline", sync.message, repository);
|
|
14777
|
+
throw new BackupFailure("push-failed", sync.message || MSG.pushFailed, repository);
|
|
14778
|
+
}
|
|
14779
|
+
async function setUpOnlineBackup(options) {
|
|
14780
|
+
try {
|
|
14781
|
+
return await run2(options);
|
|
14782
|
+
} catch (e) {
|
|
14783
|
+
if (e instanceof BackupFailure) {
|
|
14784
|
+
return {
|
|
14785
|
+
status: "failed",
|
|
14786
|
+
reason: e.reason,
|
|
14787
|
+
message: e.message,
|
|
14788
|
+
...e.repository ? { repository: e.repository } : {}
|
|
14789
|
+
};
|
|
14790
|
+
}
|
|
14791
|
+
throw e;
|
|
14792
|
+
}
|
|
14793
|
+
}
|
|
14794
|
+
// src/lib/remote-auth/clone.ts
|
|
14795
|
+
import { mkdir as mkdir15, readdir as readdir9, rm as rm6 } from "node:fs/promises";
|
|
14796
|
+
import path26 from "node:path";
|
|
14797
|
+
import git6 from "isomorphic-git";
|
|
14798
|
+
function sanitizeCloneFolderName(name) {
|
|
14799
|
+
return String(name ?? "").replace(/[\\/]/g, "-").replace(/^\.+/, "").trim();
|
|
14800
|
+
}
|
|
14801
|
+
async function assertCloneTarget(dir) {
|
|
14802
|
+
try {
|
|
14803
|
+
const entries = await readdir9(dir);
|
|
14804
|
+
if (entries.length > 0) {
|
|
14805
|
+
throw new Error("That folder already has files in it. Choose an empty folder for the book.");
|
|
14806
|
+
}
|
|
14807
|
+
} catch (e) {
|
|
14808
|
+
if (e.code === "ENOENT")
|
|
14809
|
+
return;
|
|
14810
|
+
throw e;
|
|
14811
|
+
}
|
|
14812
|
+
}
|
|
14813
|
+
function friendlyCloneError(e) {
|
|
14814
|
+
if (isInsecureTransportError(e)) {
|
|
14815
|
+
return new Error("That repository address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Use a secure address (starting with https), or a local loopback address for a server on this computer.", { cause: e });
|
|
14816
|
+
}
|
|
14817
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
14818
|
+
if (/401|403|auth|credential/i.test(msg)) {
|
|
14819
|
+
return new Error("GitHub didn't accept the connection for this repository. Reconnect GitHub and try again.", { cause: e });
|
|
14820
|
+
}
|
|
14821
|
+
if (/404|not found/i.test(msg)) {
|
|
14822
|
+
return new Error("That repository couldn't be found. It may have been renamed or you may no longer have access.", { cause: e });
|
|
14823
|
+
}
|
|
14824
|
+
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
|
|
14825
|
+
return new Error(OFFLINE_MESSAGE, { cause: e });
|
|
14826
|
+
}
|
|
14827
|
+
if (e instanceof Error && /already has files/i.test(e.message)) {
|
|
14828
|
+
return e;
|
|
14829
|
+
}
|
|
14830
|
+
return new Error("The book couldn't be downloaded from GitHub. Please try again.", { cause: e });
|
|
14831
|
+
}
|
|
14832
|
+
async function cloneRepository(options) {
|
|
14833
|
+
const { dir, onProgress, tokenStore } = options;
|
|
14834
|
+
if (!path26.isAbsolute(dir)) {
|
|
14835
|
+
throw new Error("cloneRepository requires an absolute destination path.");
|
|
14836
|
+
}
|
|
14837
|
+
let parsed;
|
|
14838
|
+
try {
|
|
14839
|
+
parsed = new URL(options.url);
|
|
14840
|
+
} catch {
|
|
14841
|
+
throw new Error("That repository address is not a valid web URL.");
|
|
14842
|
+
}
|
|
14843
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
14844
|
+
throw new Error("Only HTTPS repository addresses are supported. SSH addresses (git@…) can't be used here.");
|
|
14845
|
+
}
|
|
14846
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
14847
|
+
const credential = options.credential ?? urlCredential;
|
|
14848
|
+
if (urlCredential && tokenStore && !await tokenStore.get(urlCredential.host)) {
|
|
14849
|
+
await tokenStore.set(urlCredential.host, urlCredential);
|
|
14850
|
+
}
|
|
14851
|
+
return withRepoLock(dir, async () => {
|
|
14852
|
+
await assertCloneTarget(dir);
|
|
14853
|
+
const dirExistedBefore = gitFs.existsSync(dir);
|
|
14854
|
+
await mkdir15(dir, { recursive: true });
|
|
14855
|
+
try {
|
|
14856
|
+
await git6.clone({
|
|
14857
|
+
fs: gitFs,
|
|
14858
|
+
http: options.httpClient ?? defaultGitHttp,
|
|
14859
|
+
dir,
|
|
14860
|
+
url: cleanUrl,
|
|
14861
|
+
singleBranch: true,
|
|
14862
|
+
...options.branch ? { ref: options.branch } : {},
|
|
14863
|
+
...options.depth ? { depth: options.depth } : {},
|
|
14864
|
+
...onAuthFor(credential),
|
|
14865
|
+
...onProgress ? {
|
|
14866
|
+
onProgress: (p) => onProgress({
|
|
14867
|
+
phase: p.phase,
|
|
14868
|
+
loaded: p.loaded,
|
|
14869
|
+
...p.total ? { total: p.total } : {}
|
|
14870
|
+
})
|
|
14871
|
+
} : {}
|
|
14872
|
+
});
|
|
14873
|
+
} catch (e) {
|
|
14874
|
+
const friendly = friendlyCloneError(e);
|
|
14875
|
+
if (!dirExistedBefore) {
|
|
14876
|
+
try {
|
|
14877
|
+
await rm6(dir, { recursive: true, force: true });
|
|
14878
|
+
} catch {
|
|
14879
|
+
throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
|
|
14880
|
+
}
|
|
14881
|
+
}
|
|
14882
|
+
throw friendly;
|
|
14883
|
+
}
|
|
14884
|
+
let branch;
|
|
14885
|
+
try {
|
|
14886
|
+
branch = await git6.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14887
|
+
} catch {
|
|
14888
|
+
branch = undefined;
|
|
14889
|
+
}
|
|
14890
|
+
return { projectDir: dir, ...branch ? { branch } : {} };
|
|
14891
|
+
});
|
|
14892
|
+
}
|
|
14893
|
+
// src/lib/remote-auth/test-access.ts
|
|
14894
|
+
import git7 from "isomorphic-git";
|
|
14895
|
+
import httpNode2 from "isomorphic-git/http/node";
|
|
14896
|
+
function isSshRemoteUrl(url) {
|
|
14897
|
+
const trimmed = url.trim();
|
|
14898
|
+
if (/^ssh:\/\//i.test(trimmed))
|
|
14899
|
+
return true;
|
|
14900
|
+
return /^[\w.-]+@[\w.][\w.-]*:/.test(trimmed) || /^[\w.-]+@\[/.test(trimmed);
|
|
14901
|
+
}
|
|
14902
|
+
var FAILURE_MESSAGES = {
|
|
14903
|
+
auth: "The Git server didn't accept the saved connection. Reconnect to this server with a current access token and try again.",
|
|
14904
|
+
"not-found": "The server answered, but no repository was found at that address. It may have been moved, renamed, or you may no longer have access.",
|
|
14905
|
+
unreachable: "Couldn't reach the Git server. Check your internet connection (and VPN, if this is a private server), then try again.",
|
|
14906
|
+
"ssh-unsupported": "This book's online address uses SSH (git@…), which gutterpress can't check or sync with. Everything on this computer still works — sync with your usual Git tool.",
|
|
14907
|
+
"insecure-transport": "This address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to use a saved connection.",
|
|
14908
|
+
tls: "The server's security certificate couldn't be verified. If this is a private server with its own certificate, ask its administrator about trusting it (NODE_EXTRA_CA_CERTS).",
|
|
14909
|
+
unknown: "The connection test failed unexpectedly. The server may not be a Git server, or it may be temporarily unavailable."
|
|
14910
|
+
};
|
|
14911
|
+
function failure(reason) {
|
|
14912
|
+
return { ok: false, reason, message: FAILURE_MESSAGES[reason] };
|
|
14913
|
+
}
|
|
14914
|
+
function classifyRemoteAccessError(e) {
|
|
14915
|
+
if (isInsecureTransportError(e))
|
|
14916
|
+
return failure("insecure-transport");
|
|
14917
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
14918
|
+
if (/401|403|auth|credential|denied|cancel/i.test(msg))
|
|
14919
|
+
return failure("auth");
|
|
14920
|
+
if (/404|not found/i.test(msg))
|
|
14921
|
+
return failure("not-found");
|
|
14922
|
+
if (/certificate|self.signed|unable_to_verify|cert_|tls|ssl/i.test(msg) && !/fetch failed/i.test(msg)) {
|
|
14923
|
+
return failure("tls");
|
|
14924
|
+
}
|
|
14925
|
+
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|EHOSTUNREACH|network|fetch failed|timed out/i.test(msg)) {
|
|
14926
|
+
return failure("unreachable");
|
|
14927
|
+
}
|
|
14928
|
+
return failure("unknown");
|
|
14929
|
+
}
|
|
14930
|
+
async function testRemoteAccess(options) {
|
|
14931
|
+
if (isSshRemoteUrl(options.url))
|
|
14932
|
+
return failure("ssh-unsupported");
|
|
14933
|
+
let parsed;
|
|
14934
|
+
try {
|
|
14935
|
+
parsed = new URL(options.url);
|
|
14936
|
+
} catch {
|
|
14937
|
+
return failure("unknown");
|
|
14938
|
+
}
|
|
14939
|
+
if (!/^https?:$/.test(parsed.protocol))
|
|
14940
|
+
return failure("ssh-unsupported");
|
|
14941
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
14942
|
+
const credential = options.credential ?? urlCredential;
|
|
14943
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
14944
|
+
let timer;
|
|
14945
|
+
const timeout = new Promise((resolve9) => {
|
|
14946
|
+
timer = setTimeout(() => resolve9(failure("unreachable")), timeoutMs);
|
|
14947
|
+
});
|
|
14948
|
+
const probe = (async () => {
|
|
14949
|
+
try {
|
|
14950
|
+
const refs = await git7.listServerRefs({
|
|
14951
|
+
http: options.httpClient ?? httpNode2,
|
|
14952
|
+
url: cleanUrl,
|
|
14953
|
+
protocolVersion: 1,
|
|
14954
|
+
symrefs: true,
|
|
14955
|
+
...onAuthFor(credential),
|
|
14956
|
+
...credential ? { onAuthFailure: () => ({ cancel: true }) } : {}
|
|
14957
|
+
});
|
|
14958
|
+
const head = refs.find((r) => r.ref === "HEAD" && r.target);
|
|
14959
|
+
const defaultBranch = head?.target?.replace(/^refs\/heads\//, "");
|
|
14960
|
+
return {
|
|
14961
|
+
ok: true,
|
|
14962
|
+
refCount: refs.filter((r) => r.ref !== "HEAD").length,
|
|
14963
|
+
...defaultBranch ? { defaultBranch } : {}
|
|
14964
|
+
};
|
|
14965
|
+
} catch (e) {
|
|
14966
|
+
return classifyRemoteAccessError(e);
|
|
14967
|
+
}
|
|
14968
|
+
})();
|
|
14969
|
+
try {
|
|
14970
|
+
return await Promise.race([probe, timeout]);
|
|
14971
|
+
} finally {
|
|
14972
|
+
clearTimeout(timer);
|
|
14819
14973
|
}
|
|
14820
14974
|
}
|
|
14821
|
-
|
|
14822
|
-
|
|
14975
|
+
// src/lib/remote-auth/generic-auth.ts
|
|
14976
|
+
function normalizeForgeHost(input) {
|
|
14977
|
+
return credentialHostKey(input);
|
|
14978
|
+
}
|
|
14979
|
+
function knownForgeTokenUrl(host) {
|
|
14980
|
+
const h = normalizeForgeHost(host);
|
|
14981
|
+
if (!h)
|
|
14982
|
+
return null;
|
|
14983
|
+
const name = h.split(":")[0];
|
|
14984
|
+
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
14985
|
+
return null;
|
|
14986
|
+
if (name === "bitbucket.org") {
|
|
14987
|
+
return "https://bitbucket.org/account/settings/app-passwords/";
|
|
14988
|
+
}
|
|
14989
|
+
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com")) {
|
|
14990
|
+
return "https://dev.azure.com/_usersSettings/tokens";
|
|
14991
|
+
}
|
|
14992
|
+
if (name.includes("gitlab")) {
|
|
14993
|
+
return `https://${h}/-/user_settings/personal_access_tokens`;
|
|
14994
|
+
}
|
|
14995
|
+
if (name.includes("gitea") || name.includes("forgejo")) {
|
|
14996
|
+
return `https://${h}/user/settings/applications`;
|
|
14997
|
+
}
|
|
14998
|
+
return null;
|
|
14999
|
+
}
|
|
15000
|
+
function connectError(result, host) {
|
|
15001
|
+
switch (result.reason) {
|
|
15002
|
+
case "auth":
|
|
15003
|
+
return new Error("The Git server didn't accept that access token. Check the token (and username, if your server needs one) and try again.");
|
|
15004
|
+
case "ssh-unsupported":
|
|
15005
|
+
return new Error("Enter the server's web address (starting with https://), not an SSH address (git@…).");
|
|
15006
|
+
case "unreachable":
|
|
15007
|
+
return new Error(`Couldn't reach ${host}. Check the server address and your connection, then try again.`);
|
|
15008
|
+
case "tls":
|
|
15009
|
+
return new Error(result.message);
|
|
15010
|
+
case "not-found":
|
|
15011
|
+
return new Error("The server answered, but that repository wasn't found. Check the repository address and that the token has access to it.");
|
|
15012
|
+
default:
|
|
15013
|
+
return new Error(`Couldn't verify the connection to ${host}. Check the server address and try again.`);
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
15016
|
+
async function connectGenericHost(input, options = {}) {
|
|
15017
|
+
const host = normalizeForgeHost(input.host);
|
|
15018
|
+
if (!host) {
|
|
15019
|
+
throw new Error("Enter your Git server's web address (like https://git.example.com).");
|
|
15020
|
+
}
|
|
15021
|
+
const token = input.token?.trim();
|
|
15022
|
+
if (!token) {
|
|
15023
|
+
throw new Error("Paste an access token from your Git server to connect.");
|
|
15024
|
+
}
|
|
15025
|
+
const username = input.username?.trim() || undefined;
|
|
15026
|
+
const candidate = {
|
|
15027
|
+
host,
|
|
15028
|
+
kind: "token",
|
|
15029
|
+
token,
|
|
15030
|
+
...username ? { username } : {},
|
|
15031
|
+
label: username ? `${host} — ${username}` : host,
|
|
15032
|
+
createdAt: Date.now()
|
|
15033
|
+
};
|
|
15034
|
+
const probeUrl = input.repoUrl?.trim() || `https://${host}/`;
|
|
15035
|
+
const result = await testRemoteAccess({
|
|
15036
|
+
url: probeUrl,
|
|
15037
|
+
credential: candidate,
|
|
15038
|
+
...options.httpClient ? { httpClient: options.httpClient } : {},
|
|
15039
|
+
...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
|
|
15040
|
+
});
|
|
15041
|
+
if (!result.ok) {
|
|
15042
|
+
const rootProbe = !input.repoUrl?.trim();
|
|
15043
|
+
const inconclusiveButReachable = rootProbe && (result.reason === "not-found" || result.reason === "unknown");
|
|
15044
|
+
if (!inconclusiveButReachable)
|
|
15045
|
+
throw connectError(result, host);
|
|
15046
|
+
}
|
|
15047
|
+
return candidate;
|
|
15048
|
+
}
|
|
15049
|
+
// src/lib/remote-auth/diagnose.ts
|
|
15050
|
+
function parseRemoteOrigin(url) {
|
|
15051
|
+
const trimmed = url.trim();
|
|
15052
|
+
if (!trimmed)
|
|
15053
|
+
return { protocol: "none" };
|
|
15054
|
+
const scp = /^[\w.-]+@([\w.][\w.-]*):/.exec(trimmed);
|
|
15055
|
+
if (scp)
|
|
15056
|
+
return { protocol: "ssh", host: credentialHostKey(trimmed) };
|
|
14823
15057
|
try {
|
|
14824
|
-
const
|
|
14825
|
-
const
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
fs: gitFs,
|
|
14831
|
-
http: http3,
|
|
14832
|
-
dir,
|
|
14833
|
-
remote: transport.remote,
|
|
14834
|
-
singleBranch: false,
|
|
14835
|
-
prune: true,
|
|
14836
|
-
tags: false,
|
|
14837
|
-
...onAuthFor(transport.credential)
|
|
14838
|
-
});
|
|
14839
|
-
return { refreshed: true };
|
|
14840
|
-
} catch (e) {
|
|
14841
|
-
const message = e?.message ?? "";
|
|
14842
|
-
if (message === MSG_NO_REMOTE || message === MSG_SSH_REMOTE) {
|
|
14843
|
-
return { refreshed: false, reason: "no-remote" };
|
|
15058
|
+
const parsed = new URL(trimmed);
|
|
15059
|
+
const host = credentialHostKey(trimmed);
|
|
15060
|
+
if (/^https?:$/.test(parsed.protocol))
|
|
15061
|
+
return { protocol: "https", host };
|
|
15062
|
+
if (/^ssh:$/.test(parsed.protocol) || isSshRemoteUrl(trimmed)) {
|
|
15063
|
+
return { protocol: "ssh", host };
|
|
14844
15064
|
}
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
15065
|
+
return { protocol: "none" };
|
|
15066
|
+
} catch {
|
|
15067
|
+
return { protocol: "none" };
|
|
15068
|
+
}
|
|
15069
|
+
}
|
|
15070
|
+
function forgeKindForHost(host) {
|
|
15071
|
+
const name = host.toLowerCase().split(":")[0];
|
|
15072
|
+
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
15073
|
+
return "github";
|
|
15074
|
+
if (name === "bitbucket.org" || name.includes("bitbucket"))
|
|
15075
|
+
return "bitbucket";
|
|
15076
|
+
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com"))
|
|
15077
|
+
return "azure";
|
|
15078
|
+
if (name.includes("gitlab"))
|
|
15079
|
+
return "gitlab";
|
|
15080
|
+
if (name.includes("forgejo"))
|
|
15081
|
+
return "forgejo";
|
|
15082
|
+
if (name.includes("gitea"))
|
|
15083
|
+
return "gitea";
|
|
15084
|
+
return "generic";
|
|
15085
|
+
}
|
|
15086
|
+
async function diagnoseProjectRemote(projectDir, options = {}) {
|
|
15087
|
+
const classification = options.source ?? await detectProjectSource(projectDir);
|
|
15088
|
+
const rawRemoteUrl = classification.type === "local-git-folder" ? classification.remoteUrl : undefined;
|
|
15089
|
+
const branch = classification.type === "local-git-folder" ? classification.branch : undefined;
|
|
15090
|
+
if (!rawRemoteUrl) {
|
|
15091
|
+
return {
|
|
15092
|
+
classification,
|
|
15093
|
+
remoteProtocol: "none",
|
|
15094
|
+
...branch ? { branch } : {},
|
|
15095
|
+
credentialPresent: false,
|
|
15096
|
+
provider: null,
|
|
15097
|
+
tokenSettingsUrl: null,
|
|
15098
|
+
canSync: false,
|
|
15099
|
+
guidance: "local-only"
|
|
15100
|
+
};
|
|
15101
|
+
}
|
|
15102
|
+
const { protocol, host } = parseRemoteOrigin(rawRemoteUrl);
|
|
15103
|
+
const urlExtraction = protocol === "https" ? extractUrlCredential(rawRemoteUrl) : null;
|
|
15104
|
+
const remoteUrl = urlExtraction ? urlExtraction.cleanUrl : rawRemoteUrl;
|
|
15105
|
+
const sanitizedClassification = classification.type === "local-git-folder" && classification.remoteUrl ? { ...classification, remoteUrl } : classification;
|
|
15106
|
+
let credentialPresent = false;
|
|
15107
|
+
if (host && options.tokenStore) {
|
|
15108
|
+
try {
|
|
15109
|
+
credentialPresent = await options.tokenStore.get(host) !== null;
|
|
15110
|
+
} catch {
|
|
15111
|
+
credentialPresent = false;
|
|
14848
15112
|
}
|
|
14849
|
-
return { refreshed: false, reason: "offline" };
|
|
14850
15113
|
}
|
|
15114
|
+
if (!credentialPresent && urlExtraction?.credential)
|
|
15115
|
+
credentialPresent = true;
|
|
15116
|
+
const provider = host && protocol !== "none" ? forgeKindForHost(host) : null;
|
|
15117
|
+
const canSync = protocol === "https" && credentialPresent;
|
|
15118
|
+
let guidance;
|
|
15119
|
+
if (protocol === "ssh")
|
|
15120
|
+
guidance = "ssh-use-own-tools";
|
|
15121
|
+
else if (protocol !== "https")
|
|
15122
|
+
guidance = "local-only";
|
|
15123
|
+
else if (credentialPresent)
|
|
15124
|
+
guidance = "ready-to-sync";
|
|
15125
|
+
else if (provider === "github")
|
|
15126
|
+
guidance = "connect-github-to-sync";
|
|
15127
|
+
else
|
|
15128
|
+
guidance = "https-connect-server";
|
|
15129
|
+
return {
|
|
15130
|
+
classification: sanitizedClassification,
|
|
15131
|
+
remoteUrl,
|
|
15132
|
+
...host ? { remoteHost: host } : {},
|
|
15133
|
+
remoteProtocol: protocol === "none" ? "none" : protocol,
|
|
15134
|
+
...branch ? { branch } : {},
|
|
15135
|
+
credentialPresent,
|
|
15136
|
+
provider,
|
|
15137
|
+
tokenSettingsUrl: host ? knownForgeTokenUrl(host) : null,
|
|
15138
|
+
canSync,
|
|
15139
|
+
guidance
|
|
15140
|
+
};
|
|
14851
15141
|
}
|
|
14852
15142
|
// src/lib/remote-auth/repair.ts
|
|
14853
|
-
import
|
|
14854
|
-
import { cp as cp5, mkdir as
|
|
14855
|
-
import
|
|
15143
|
+
import git8 from "isomorphic-git";
|
|
15144
|
+
import { cp as cp5, mkdir as mkdir16, rename as rename5, rm as rm7 } from "node:fs/promises";
|
|
15145
|
+
import path27 from "node:path";
|
|
14856
15146
|
var REPAIR_SNAPSHOT_MESSAGE = "Repaired online backup";
|
|
14857
15147
|
async function moveDir(from, to) {
|
|
14858
|
-
await
|
|
15148
|
+
await mkdir16(path27.dirname(to), { recursive: true });
|
|
14859
15149
|
try {
|
|
14860
15150
|
await rename5(from, to);
|
|
14861
15151
|
} catch (e) {
|
|
@@ -14868,14 +15158,14 @@ async function moveDir(from, to) {
|
|
|
14868
15158
|
async function repairOnlineBackup(options) {
|
|
14869
15159
|
const logger = resolveLogger(options.logFile, "repair");
|
|
14870
15160
|
const dir = await repoDirFor(options.projectDir);
|
|
14871
|
-
const oldGit =
|
|
14872
|
-
const movedGitTo =
|
|
14873
|
-
const freshDir =
|
|
15161
|
+
const oldGit = path27.join(dir, ".git");
|
|
15162
|
+
const movedGitTo = path27.join(options.backupDir, "git");
|
|
15163
|
+
const freshDir = path27.join(options.backupDir, "fresh-clone");
|
|
14874
15164
|
const { restoredFiles, credential } = await withRepoLock(dir, async () => {
|
|
14875
15165
|
const transport = await resolveTransport(dir, options);
|
|
14876
15166
|
let branch;
|
|
14877
15167
|
try {
|
|
14878
|
-
branch = await
|
|
15168
|
+
branch = await git8.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14879
15169
|
} catch {
|
|
14880
15170
|
branch = undefined;
|
|
14881
15171
|
}
|
|
@@ -14891,7 +15181,7 @@ async function repairOnlineBackup(options) {
|
|
|
14891
15181
|
logger.info("swap", "moving old history aside", { to: movedGitTo });
|
|
14892
15182
|
await moveDir(oldGit, movedGitTo);
|
|
14893
15183
|
try {
|
|
14894
|
-
await moveDir(
|
|
15184
|
+
await moveDir(path27.join(freshDir, ".git"), oldGit);
|
|
14895
15185
|
} catch (e) {
|
|
14896
15186
|
await rm7(oldGit, { recursive: true, force: true });
|
|
14897
15187
|
await moveDir(movedGitTo, oldGit);
|
|
@@ -14901,10 +15191,10 @@ async function repairOnlineBackup(options) {
|
|
|
14901
15191
|
const ref = fresh.branch ?? "HEAD";
|
|
14902
15192
|
let missing = [];
|
|
14903
15193
|
try {
|
|
14904
|
-
const tracked = await
|
|
14905
|
-
missing = tracked.filter((f) => !gitFs.existsSync(
|
|
15194
|
+
const tracked = await git8.listFiles({ fs: gitFs, dir, ref });
|
|
15195
|
+
missing = tracked.filter((f) => !gitFs.existsSync(path27.join(dir, f)));
|
|
14906
15196
|
if (missing.length > 0) {
|
|
14907
|
-
await
|
|
15197
|
+
await git8.checkout({ fs: gitFs, dir, ref, filepaths: missing, force: true });
|
|
14908
15198
|
logger.info("restore", "restored files missing on this computer", { files: missing });
|
|
14909
15199
|
}
|
|
14910
15200
|
} catch (e) {
|
|
@@ -14931,21 +15221,21 @@ async function repairOnlineBackup(options) {
|
|
|
14931
15221
|
return { outcome, movedGitTo, restoredFiles };
|
|
14932
15222
|
}
|
|
14933
15223
|
// src/lib/remote-auth/scorched-earth.ts
|
|
14934
|
-
import
|
|
14935
|
-
import { cp as cp6, mkdir as
|
|
14936
|
-
import
|
|
15224
|
+
import git9 from "isomorphic-git";
|
|
15225
|
+
import { cp as cp6, mkdir as mkdir17, readdir as readdir10, rm as rm8 } from "node:fs/promises";
|
|
15226
|
+
import path28 from "node:path";
|
|
14937
15227
|
async function emptyDir(dir) {
|
|
14938
15228
|
for (const entry of await readdir10(dir)) {
|
|
14939
|
-
await rm8(
|
|
15229
|
+
await rm8(path28.join(dir, entry), { recursive: true, force: true });
|
|
14940
15230
|
}
|
|
14941
15231
|
}
|
|
14942
15232
|
async function copyFilesBack(from, to) {
|
|
14943
|
-
const skip =
|
|
15233
|
+
const skip = path28.join(from, ".git");
|
|
14944
15234
|
for (const entry of await readdir10(from)) {
|
|
14945
|
-
const src =
|
|
15235
|
+
const src = path28.join(from, entry);
|
|
14946
15236
|
if (src === skip)
|
|
14947
15237
|
continue;
|
|
14948
|
-
await cp6(src,
|
|
15238
|
+
await cp6(src, path28.join(to, entry), { recursive: true, force: true });
|
|
14949
15239
|
}
|
|
14950
15240
|
}
|
|
14951
15241
|
async function scorchedEarth(options) {
|
|
@@ -14957,12 +15247,12 @@ async function scorchedEarth(options) {
|
|
|
14957
15247
|
const transport2 = await resolveTransport(dir, options);
|
|
14958
15248
|
let branch2;
|
|
14959
15249
|
try {
|
|
14960
|
-
branch2 = await
|
|
15250
|
+
branch2 = await git9.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14961
15251
|
} catch {
|
|
14962
15252
|
branch2 = undefined;
|
|
14963
15253
|
}
|
|
14964
15254
|
logger.info("backup", "copying folder", { to: backupDir });
|
|
14965
|
-
await
|
|
15255
|
+
await mkdir17(backupDir, { recursive: true });
|
|
14966
15256
|
await cp6(dir, backupDir, { recursive: true });
|
|
14967
15257
|
logger.info("delete", "emptying folder", { dir });
|
|
14968
15258
|
await emptyDir(dir);
|
|
@@ -14995,7 +15285,7 @@ async function scorchedEarth(options) {
|
|
|
14995
15285
|
}
|
|
14996
15286
|
// src/lib/publish/run-publish.ts
|
|
14997
15287
|
import { readdir as readdir12, stat as stat12 } from "node:fs/promises";
|
|
14998
|
-
import
|
|
15288
|
+
import path33 from "node:path";
|
|
14999
15289
|
|
|
15000
15290
|
// src/lib/publish/command-runner.ts
|
|
15001
15291
|
var CAPTURE_LIMIT = 64 * 1024;
|
|
@@ -15074,8 +15364,8 @@ async function resolveSwaCommand(req) {
|
|
|
15074
15364
|
const explicit = env.SWA_CLI_PATH?.trim();
|
|
15075
15365
|
if (explicit)
|
|
15076
15366
|
return explicit;
|
|
15077
|
-
const
|
|
15078
|
-
return await commandExists("swa",
|
|
15367
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15368
|
+
return await commandExists("swa", run3, req.deps.env) ? "swa" : null;
|
|
15079
15369
|
}
|
|
15080
15370
|
var SWA_INSTALL_HINT = "Install the Azure SWA CLI first: `npm install -g @azure/static-web-apps-cli` " + "(needs Node.js), or set SWA_CLI_PATH to the swa binary.";
|
|
15081
15371
|
var azureSwaProvider = {
|
|
@@ -15121,9 +15411,9 @@ var azureSwaProvider = {
|
|
|
15121
15411
|
throw new Error(SWA_INSTALL_HINT);
|
|
15122
15412
|
const cfg = req.config;
|
|
15123
15413
|
const deployEnv = cfg.env?.trim() || "production";
|
|
15124
|
-
const
|
|
15414
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15125
15415
|
req.deps.onProgress?.(`Deploying ${req.artifact.path} to Azure SWA (${deployEnv})…`);
|
|
15126
|
-
const result = await
|
|
15416
|
+
const result = await run3(swa, ["deploy", req.artifact.path, "--env", deployEnv], {
|
|
15127
15417
|
env: { SWA_CLI_DEPLOYMENT_TOKEN: resolved.credential.token },
|
|
15128
15418
|
onOutput: req.deps.onProgress
|
|
15129
15419
|
});
|
|
@@ -15144,25 +15434,25 @@ ${tail}` : ""}`);
|
|
|
15144
15434
|
};
|
|
15145
15435
|
|
|
15146
15436
|
// src/lib/publish/providers/guided.ts
|
|
15147
|
-
import { copyFile as copyFile2, mkdir as
|
|
15148
|
-
import
|
|
15437
|
+
import { copyFile as copyFile2, mkdir as mkdir18, writeFile as writeFile15 } from "node:fs/promises";
|
|
15438
|
+
import path29 from "node:path";
|
|
15149
15439
|
async function stageGuidedPackage(req, providerId, listingLines) {
|
|
15150
|
-
const packageDir =
|
|
15151
|
-
await
|
|
15152
|
-
await copyFile2(req.artifact.path,
|
|
15440
|
+
const packageDir = path29.join(path29.dirname(req.artifact.path), "publish", providerId);
|
|
15441
|
+
await mkdir18(packageDir, { recursive: true });
|
|
15442
|
+
await copyFile2(req.artifact.path, path29.join(packageDir, path29.basename(req.artifact.path)));
|
|
15153
15443
|
const { title, authors } = req.project;
|
|
15154
15444
|
const sheet = [
|
|
15155
15445
|
`# ${title || "Untitled"}`,
|
|
15156
15446
|
"",
|
|
15157
15447
|
`- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
|
|
15158
|
-
`- **File:** ${
|
|
15448
|
+
`- **File:** ${path29.basename(req.artifact.path)}`,
|
|
15159
15449
|
...listingLines,
|
|
15160
15450
|
"",
|
|
15161
15451
|
"Copy these details into the platform's listing form when uploading.",
|
|
15162
15452
|
""
|
|
15163
15453
|
].join(`
|
|
15164
15454
|
`);
|
|
15165
|
-
await
|
|
15455
|
+
await writeFile15(path29.join(packageDir, "LISTING.md"), sheet, "utf8");
|
|
15166
15456
|
return packageDir;
|
|
15167
15457
|
}
|
|
15168
15458
|
|
|
@@ -15225,9 +15515,9 @@ var drivethrurpgProvider = {
|
|
|
15225
15515
|
};
|
|
15226
15516
|
|
|
15227
15517
|
// src/lib/publish/providers/gdrive.ts
|
|
15228
|
-
import { mkdtemp as mkdtemp3, readFile as
|
|
15518
|
+
import { mkdtemp as mkdtemp3, readFile as readFile35, readdir as readdir11, rm as rm9, stat as stat9, writeFile as writeFile16 } from "node:fs/promises";
|
|
15229
15519
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15230
|
-
import
|
|
15520
|
+
import path30 from "node:path";
|
|
15231
15521
|
import { Zip, ZipDeflate } from "fflate";
|
|
15232
15522
|
|
|
15233
15523
|
// src/lib/publish/google-drive.ts
|
|
@@ -15612,34 +15902,34 @@ function zipEntriesNonBlocking(entries, opts) {
|
|
|
15612
15902
|
async function collectZipEntries(root, dir, out, onWarn) {
|
|
15613
15903
|
const entries = await readdir11(dir, { withFileTypes: true });
|
|
15614
15904
|
for (const entry of entries) {
|
|
15615
|
-
const full =
|
|
15905
|
+
const full = path30.join(dir, entry.name);
|
|
15616
15906
|
if (entry.isDirectory()) {
|
|
15617
15907
|
await collectZipEntries(root, full, out, onWarn);
|
|
15618
15908
|
} else if (entry.isFile()) {
|
|
15619
|
-
const rel =
|
|
15620
|
-
out[rel] = await
|
|
15909
|
+
const rel = path30.relative(root, full).split(path30.sep).join("/");
|
|
15910
|
+
out[rel] = await readFile35(full);
|
|
15621
15911
|
} else if (entry.isSymbolicLink()) {
|
|
15622
15912
|
const resolved = await stat9(full).catch(() => null);
|
|
15623
15913
|
if (resolved?.isFile()) {
|
|
15624
|
-
const rel =
|
|
15625
|
-
out[rel] = await
|
|
15914
|
+
const rel = path30.relative(root, full).split(path30.sep).join("/");
|
|
15915
|
+
out[rel] = await readFile35(full);
|
|
15626
15916
|
} else if (resolved?.isDirectory()) {
|
|
15627
15917
|
await collectZipEntries(root, full, out, onWarn);
|
|
15628
15918
|
} else {
|
|
15629
|
-
onWarn?.(`Skipped "${
|
|
15919
|
+
onWarn?.(`Skipped "${path30.relative(root, full)}" in the website export — it's a broken symlink.`);
|
|
15630
15920
|
}
|
|
15631
15921
|
} else {
|
|
15632
|
-
onWarn?.(`Skipped "${
|
|
15922
|
+
onWarn?.(`Skipped "${path30.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
|
|
15633
15923
|
}
|
|
15634
15924
|
}
|
|
15635
15925
|
}
|
|
15636
|
-
async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl =
|
|
15926
|
+
async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile16) {
|
|
15637
15927
|
const entries = {};
|
|
15638
15928
|
await collectZipEntries(exportDir, exportDir, entries, onWarn);
|
|
15639
15929
|
const fileName = `${bookSlug(title)}-website.zip`;
|
|
15640
|
-
const tmpRoot = await mkdtemp3(
|
|
15930
|
+
const tmpRoot = await mkdtemp3(path30.join(tmpdir3(), "gutterpress-gdrive-"));
|
|
15641
15931
|
try {
|
|
15642
|
-
const filePath =
|
|
15932
|
+
const filePath = path30.join(tmpRoot, fileName);
|
|
15643
15933
|
const zipped = await zipEntriesNonBlocking(entries, { level: 6 });
|
|
15644
15934
|
await writeArchiveImpl(filePath, zipped);
|
|
15645
15935
|
return {
|
|
@@ -15687,7 +15977,7 @@ var gdriveProvider = {
|
|
|
15687
15977
|
const isHtml = req.artifact.format === "html";
|
|
15688
15978
|
const source = isHtml ? await zipHtmlExport(req.artifact.path, req.project.title, (msg) => req.deps.onProgress?.(msg)) : {
|
|
15689
15979
|
filePath: req.artifact.path,
|
|
15690
|
-
fileName:
|
|
15980
|
+
fileName: path30.basename(req.artifact.path),
|
|
15691
15981
|
mimeType: "application/pdf",
|
|
15692
15982
|
cleanup: async () => {}
|
|
15693
15983
|
};
|
|
@@ -15737,8 +16027,8 @@ var gdriveProvider = {
|
|
|
15737
16027
|
};
|
|
15738
16028
|
|
|
15739
16029
|
// src/lib/publish/butler.ts
|
|
15740
|
-
import { chmod as chmod2, mkdir as
|
|
15741
|
-
import
|
|
16030
|
+
import { chmod as chmod2, mkdir as mkdir19, stat as stat10, writeFile as writeFile17 } from "node:fs/promises";
|
|
16031
|
+
import path31 from "node:path";
|
|
15742
16032
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
15743
16033
|
var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
|
|
15744
16034
|
function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
|
|
@@ -15775,8 +16065,8 @@ async function ensureButler(deps) {
|
|
|
15775
16065
|
}
|
|
15776
16066
|
if (await commandExists("butler", runCommand, deps.env))
|
|
15777
16067
|
return "butler";
|
|
15778
|
-
const cacheDir =
|
|
15779
|
-
const cached =
|
|
16068
|
+
const cacheDir = path31.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
|
|
16069
|
+
const cached = path31.join(cacheDir, butlerBinaryName());
|
|
15780
16070
|
if (await fileExists2(cached))
|
|
15781
16071
|
return cached;
|
|
15782
16072
|
const channel = butlerBrothChannel();
|
|
@@ -15798,14 +16088,14 @@ async function ensureButler(deps) {
|
|
|
15798
16088
|
return new Uint8Array(await response.arrayBuffer());
|
|
15799
16089
|
});
|
|
15800
16090
|
const files = unzipSync2(archive);
|
|
15801
|
-
await
|
|
16091
|
+
await mkdir19(cacheDir, { recursive: true });
|
|
15802
16092
|
let foundBinary = false;
|
|
15803
16093
|
for (const [name, data] of Object.entries(files)) {
|
|
15804
16094
|
if (name.endsWith("/"))
|
|
15805
16095
|
continue;
|
|
15806
|
-
const target =
|
|
15807
|
-
await
|
|
15808
|
-
if (
|
|
16096
|
+
const target = path31.join(cacheDir, path31.basename(name));
|
|
16097
|
+
await writeFile17(target, data);
|
|
16098
|
+
if (path31.basename(name) === butlerBinaryName()) {
|
|
15809
16099
|
await chmod2(target, 493).catch(() => {});
|
|
15810
16100
|
foundBinary = true;
|
|
15811
16101
|
}
|
|
@@ -15926,9 +16216,9 @@ var itchProvider = {
|
|
|
15926
16216
|
throw new Error("No itch.io API key found. Connect itch.io (or set BUTLER_API_KEY) first.");
|
|
15927
16217
|
}
|
|
15928
16218
|
const butler = await ensureButler(req.deps);
|
|
15929
|
-
const
|
|
16219
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15930
16220
|
req.deps.onProgress?.(`Uploading to ${target}:${channel} with butler…`);
|
|
15931
|
-
const result = await
|
|
16221
|
+
const result = await run3(butler, ["push", req.artifact.path, `${target}:${channel}`], {
|
|
15932
16222
|
env: { BUTLER_API_KEY: resolved.credential.token },
|
|
15933
16223
|
onOutput: req.deps.onProgress
|
|
15934
16224
|
});
|
|
@@ -16001,8 +16291,8 @@ var kdpProvider = {
|
|
|
16001
16291
|
};
|
|
16002
16292
|
|
|
16003
16293
|
// src/lib/publish/providers/local-folder.ts
|
|
16004
|
-
import { cp as cp7, mkdir as
|
|
16005
|
-
import
|
|
16294
|
+
import { cp as cp7, mkdir as mkdir20, stat as stat11 } from "node:fs/promises";
|
|
16295
|
+
import path32 from "node:path";
|
|
16006
16296
|
var LOCAL_PUBLISH_DEFAULT_DIR = "dist";
|
|
16007
16297
|
var info7 = {
|
|
16008
16298
|
id: "local",
|
|
@@ -16018,7 +16308,7 @@ var info7 = {
|
|
|
16018
16308
|
};
|
|
16019
16309
|
function localPublishDir(req) {
|
|
16020
16310
|
const dir = typeof req.config.dir === "string" && req.config.dir.trim() ? req.config.dir.trim() : LOCAL_PUBLISH_DEFAULT_DIR;
|
|
16021
|
-
return
|
|
16311
|
+
return path32.resolve(req.project.projectDir, dir);
|
|
16022
16312
|
}
|
|
16023
16313
|
var localFolderProvider = {
|
|
16024
16314
|
info: info7,
|
|
@@ -16039,9 +16329,9 @@ var localFolderProvider = {
|
|
|
16039
16329
|
},
|
|
16040
16330
|
async upload(req) {
|
|
16041
16331
|
const dir = localPublishDir(req);
|
|
16042
|
-
await
|
|
16043
|
-
const dest = req.artifact.format === "html" ? dir :
|
|
16044
|
-
if (
|
|
16332
|
+
await mkdir20(dir, { recursive: true });
|
|
16333
|
+
const dest = req.artifact.format === "html" ? dir : path32.join(dir, path32.basename(req.artifact.path));
|
|
16334
|
+
if (path32.resolve(dest) !== path32.resolve(req.artifact.path)) {
|
|
16045
16335
|
await cp7(req.artifact.path, dest, { recursive: true });
|
|
16046
16336
|
}
|
|
16047
16337
|
return { kind: "published", detail: `Saved to ${dest}` };
|
|
@@ -16303,9 +16593,9 @@ async function resolvePublishRequest(options, deps) {
|
|
|
16303
16593
|
const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
|
|
16304
16594
|
const outDir = resolveOutputDir(manifestDir, config.title);
|
|
16305
16595
|
const effectiveFormat = resolvePublishFormat(provider.info, providerConfig);
|
|
16306
|
-
const defaultArtifact = effectiveFormat === "pdf" ?
|
|
16596
|
+
const defaultArtifact = effectiveFormat === "pdf" ? path33.join(outDir, artifactName(config.title, "pdf")) : outDir;
|
|
16307
16597
|
const artifact = {
|
|
16308
|
-
path: options.artifactPath ?
|
|
16598
|
+
path: options.artifactPath ? path33.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
|
|
16309
16599
|
format: effectiveFormat
|
|
16310
16600
|
};
|
|
16311
16601
|
return {
|
|
@@ -16369,7 +16659,7 @@ async function artifactIssues(artifact) {
|
|
|
16369
16659
|
var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
|
|
16370
16660
|
async function htmlDirIssues(dir) {
|
|
16371
16661
|
const issues = [];
|
|
16372
|
-
const isThere = async (rel) => stat12(
|
|
16662
|
+
const isThere = async (rel) => stat12(path33.join(dir, rel)).then(() => true, () => false);
|
|
16373
16663
|
if (!await isThere(BOOK_HTML)) {
|
|
16374
16664
|
issues.push({
|
|
16375
16665
|
severity: "error",
|
|
@@ -16488,17 +16778,17 @@ async function disconnectPublishCredential(key, deps, options = {}) {
|
|
|
16488
16778
|
}
|
|
16489
16779
|
}
|
|
16490
16780
|
// src/lib/publish/selections.ts
|
|
16491
|
-
import { chmod as chmod3, mkdir as
|
|
16492
|
-
import
|
|
16781
|
+
import { chmod as chmod3, mkdir as mkdir21, readFile as readFile36, writeFile as writeFile18 } from "node:fs/promises";
|
|
16782
|
+
import path34 from "node:path";
|
|
16493
16783
|
class PublishSelectionsStore {
|
|
16494
16784
|
filePath;
|
|
16495
16785
|
queue = Promise.resolve();
|
|
16496
16786
|
constructor(filePath) {
|
|
16497
|
-
this.filePath = filePath ??
|
|
16787
|
+
this.filePath = filePath ?? path34.join(defaultConfigDir(), "publish-selections.json");
|
|
16498
16788
|
}
|
|
16499
16789
|
async read() {
|
|
16500
16790
|
try {
|
|
16501
|
-
const raw = await
|
|
16791
|
+
const raw = await readFile36(this.filePath, "utf8");
|
|
16502
16792
|
const parsed = JSON.parse(raw);
|
|
16503
16793
|
if (parsed && typeof parsed === "object") {
|
|
16504
16794
|
return {
|
|
@@ -16511,19 +16801,19 @@ class PublishSelectionsStore {
|
|
|
16511
16801
|
return { version: 1, global: {}, projects: {} };
|
|
16512
16802
|
}
|
|
16513
16803
|
async write(data) {
|
|
16514
|
-
await
|
|
16515
|
-
await
|
|
16804
|
+
await mkdir21(path34.dirname(this.filePath), { recursive: true });
|
|
16805
|
+
await writeFile18(this.filePath, JSON.stringify(data, null, 2), {
|
|
16516
16806
|
encoding: "utf8",
|
|
16517
16807
|
mode: 384
|
|
16518
16808
|
});
|
|
16519
16809
|
await chmod3(this.filePath, 384);
|
|
16520
16810
|
}
|
|
16521
16811
|
enqueue(fn) {
|
|
16522
|
-
const
|
|
16523
|
-
this.queue =
|
|
16812
|
+
const run3 = this.queue.then(fn, fn);
|
|
16813
|
+
this.queue = run3.catch(() => {
|
|
16524
16814
|
return;
|
|
16525
16815
|
});
|
|
16526
|
-
return
|
|
16816
|
+
return run3;
|
|
16527
16817
|
}
|
|
16528
16818
|
resolve(providerId, projectDir) {
|
|
16529
16819
|
return this.enqueue(async () => {
|
|
@@ -16614,4 +16904,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
|
|
|
16614
16904
|
await writeManifestDoc(file, doc);
|
|
16615
16905
|
return settingsFromDoc(doc);
|
|
16616
16906
|
}
|
|
16617
|
-
export { 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, DEFAULT_NPM_REGISTRY, npmRegistryUrl, ensureProjectGitignore, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listNpmVersions, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, restorePinnedExtensions, restoreFailureMessage, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, 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, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, 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,
|
|
16907
|
+
export { 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, DEFAULT_NPM_REGISTRY, npmRegistryUrl, ensureProjectGitignore, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listNpmVersions, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, restorePinnedExtensions, restoreFailureMessage, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, readPageSetup, setPageSetup, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, 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, syncProject, refreshRemoteCopies, isValidRepositoryName, setUpOnlineBackup, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, REPAIR_SNAPSHOT_MESSAGE, repairOnlineBackup, scorchedEarth, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
|