gutterpress 0.11.17-alpha.2 → 0.11.17-alpha.3
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 +10 -2
- package/dist/{build-1p999cmg.js → build-394c1ee1.js} +4 -4
- package/dist/checks/pdf/index.d.ts +1 -0
- package/dist/checks/pdf/signature.d.ts +3 -0
- package/dist/{cli-70xgehq2.js → cli-3p1e95zc.js} +1 -1
- package/dist/{cli-6z5zthzt.js → cli-9nba5ve6.js} +562 -211
- package/dist/{cli-ppc0vz4b.js → cli-d57bsfpb.js} +1 -1
- package/dist/{cli-gymfg04q.js → cli-jyy9pqf3.js} +15 -10
- package/dist/cli.js +12 -12
- package/dist/{doctor-reqpnn3h.js → doctor-c459qv5j.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-ha7gg8h9.js} +4 -4
- package/dist/{gutterpress-viewer-zkv2stfy.js → gutterpress-viewer-1n793gwd.js} +5 -3
- package/dist/{index-c275p061.js → index-bscfmg5r.js} +1714 -1054
- package/dist/{index-5dcj1vts.js → index-hrx0gfkj.js} +23 -684
- package/dist/index-q1xqxrh9.js +949 -0
- package/dist/index.js +11 -3
- package/dist/lib/black-text-gray.d.ts +38 -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-kky3z7kv.js} +4 -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-t8v6szq6.js} +4 -4
- package/dist/preview/file-watcher.d.ts +2 -4
- package/dist/{preview-62y7qhkp.js → preview-cxab4y5x.js} +4 -4
- package/dist/{publish-k2zyt3x6.js → publish-s2mtenb5.js} +4 -4
- package/dist/render.js +30 -5
- package/dist/schema/manifest.types.d.ts +17 -0
- package/dist/{validate-fgwwxtbn.js → validate-xaajdw4x.js} +4 -4
- package/package.json +1 -1
- package/dist/index-eyaj6nkc.js +0 -267
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
EXIT_CODES,
|
|
4
4
|
MARGIN_BOX_IGNORED_PROPERTIES,
|
|
5
5
|
getAssetPath,
|
|
6
|
-
getAssetsDir
|
|
7
|
-
|
|
6
|
+
getAssetsDir,
|
|
7
|
+
lastPageMode,
|
|
8
|
+
parseSize
|
|
9
|
+
} from "./index-q1xqxrh9.js";
|
|
8
10
|
import {
|
|
9
11
|
INSTALL_HINTS,
|
|
10
12
|
execCapture,
|
|
@@ -140,7 +142,7 @@ var DTRPG_PRESET = {
|
|
|
140
142
|
source: {
|
|
141
143
|
files: null
|
|
142
144
|
},
|
|
143
|
-
print: { signature: 4, reserveLastPage: true },
|
|
145
|
+
print: { signature: 4, reserveLastPage: true, lastPage: undefined },
|
|
144
146
|
pdfx: {
|
|
145
147
|
flavor: "x1a",
|
|
146
148
|
icc: "profiles/CGATS21_CRPC1.icc",
|
|
@@ -158,7 +160,8 @@ var DTRPG_PRESET = {
|
|
|
158
160
|
},
|
|
159
161
|
lint: {
|
|
160
162
|
enabled: true,
|
|
161
|
-
configPath: null
|
|
163
|
+
configPath: null,
|
|
164
|
+
rules: {}
|
|
162
165
|
},
|
|
163
166
|
validate: {
|
|
164
167
|
enabled: true,
|
|
@@ -215,7 +218,7 @@ var BOOK_PRESET = {
|
|
|
215
218
|
source: {
|
|
216
219
|
files: null
|
|
217
220
|
},
|
|
218
|
-
print: { signature: 1, reserveLastPage: false },
|
|
221
|
+
print: { signature: 1, reserveLastPage: false, lastPage: undefined },
|
|
219
222
|
pdfx: {
|
|
220
223
|
flavor: "x1a",
|
|
221
224
|
icc: "profiles/CGATS21_CRPC1.icc",
|
|
@@ -233,7 +236,8 @@ var BOOK_PRESET = {
|
|
|
233
236
|
},
|
|
234
237
|
lint: {
|
|
235
238
|
enabled: true,
|
|
236
|
-
configPath: null
|
|
239
|
+
configPath: null,
|
|
240
|
+
rules: {}
|
|
237
241
|
},
|
|
238
242
|
validate: {
|
|
239
243
|
enabled: true,
|
|
@@ -606,6 +610,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
|
|
|
606
610
|
if (legacyPlugins !== undefined) {
|
|
607
611
|
throw new UsageError(pluginsRemovedMessage(legacyPlugins));
|
|
608
612
|
}
|
|
613
|
+
const lastPage = c.print?.lastPage ?? m.print?.lastPage;
|
|
614
|
+
if (lastPage !== undefined && lastPage !== "blank" && lastPage !== "short-by-one") {
|
|
615
|
+
throw new UsageError(`Manifest field \`print.lastPage\` must be "blank" or "short-by-one", got ${JSON.stringify(lastPage)}.`);
|
|
616
|
+
}
|
|
609
617
|
const rawExtensions = c.extensions ?? m.extensions ?? [];
|
|
610
618
|
if (!Array.isArray(rawExtensions)) {
|
|
611
619
|
throw new UsageError("Manifest field `extensions` must be a list of specifiers.");
|
|
@@ -622,7 +630,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
|
|
|
622
630
|
print: mergeShape(c.print, m.print, preset.print),
|
|
623
631
|
page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
|
|
624
632
|
ink: mergeShape(c.ink, m.ink, preset.ink),
|
|
625
|
-
lint:
|
|
633
|
+
lint: {
|
|
634
|
+
...mergeShape(c.lint, m.lint, preset.lint),
|
|
635
|
+
rules: { ...preset.lint.rules, ...m.lint?.rules, ...c.lint?.rules }
|
|
636
|
+
},
|
|
626
637
|
validate: {
|
|
627
638
|
enabled: c.validate?.enabled ?? m.validate?.enabled ?? preset.validate.enabled,
|
|
628
639
|
checks: { ...preset.validate.checks, ...m.validate?.checks, ...c.validate?.checks },
|
|
@@ -1037,7 +1048,7 @@ function resolveContainerShape(name, pluginName, decl) {
|
|
|
1037
1048
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`section\` that is not a boolean.`);
|
|
1038
1049
|
}
|
|
1039
1050
|
if (decl.section) {
|
|
1040
|
-
for (const field of ["tag", "label", "autoCloseAt"]) {
|
|
1051
|
+
for (const field of ["tag", "label", "autoCloseAt", "selfClosing"]) {
|
|
1041
1052
|
if (decl[field] !== undefined) {
|
|
1042
1053
|
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
1054
|
}
|
|
@@ -1094,11 +1105,26 @@ function resolveContainerShape(name, pluginName, decl) {
|
|
|
1094
1105
|
}
|
|
1095
1106
|
autoCloseAtEof = decl.autoCloseAt.includes("eof");
|
|
1096
1107
|
}
|
|
1108
|
+
if (decl.selfClosing !== undefined && typeof decl.selfClosing !== "boolean") {
|
|
1109
|
+
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`selfClosing\` that is not a boolean.`);
|
|
1110
|
+
}
|
|
1111
|
+
if (decl.selfClosing && decl.autoCloseAt !== undefined) {
|
|
1112
|
+
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.`);
|
|
1113
|
+
}
|
|
1097
1114
|
if (decl.validate !== undefined && typeof decl.validate !== "function") {
|
|
1098
1115
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
|
|
1099
1116
|
}
|
|
1100
1117
|
const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
|
|
1101
|
-
return {
|
|
1118
|
+
return {
|
|
1119
|
+
tag,
|
|
1120
|
+
classBase,
|
|
1121
|
+
variants,
|
|
1122
|
+
label,
|
|
1123
|
+
autoCloseAtEof,
|
|
1124
|
+
...decl.section ? { section: true } : {},
|
|
1125
|
+
...decl.selfClosing ? { selfClosing: true } : {},
|
|
1126
|
+
...check
|
|
1127
|
+
};
|
|
1102
1128
|
}
|
|
1103
1129
|
function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
|
|
1104
1130
|
const pluginName = originOf.get(name);
|
|
@@ -1357,7 +1383,8 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1357
1383
|
addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
|
|
1358
1384
|
attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
|
|
1359
1385
|
out.push(t);
|
|
1360
|
-
|
|
1386
|
+
if (!decl.selfClosing)
|
|
1387
|
+
declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
|
|
1361
1388
|
if (decl.label) {
|
|
1362
1389
|
const value = meta.attrs && meta.attrs[decl.label.attr];
|
|
1363
1390
|
if (value) {
|
|
@@ -1368,6 +1395,8 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1368
1395
|
t.meta.labelled = true;
|
|
1369
1396
|
}
|
|
1370
1397
|
}
|
|
1398
|
+
if (decl.selfClosing)
|
|
1399
|
+
out.push(new state.Token("layout_component_close", decl.tag, -1));
|
|
1371
1400
|
}
|
|
1372
1401
|
const stack = {
|
|
1373
1402
|
frames: [],
|
|
@@ -1646,7 +1675,7 @@ function plugin(md, pluginOptions = {}) {
|
|
|
1646
1675
|
continue;
|
|
1647
1676
|
}
|
|
1648
1677
|
if (!closeDeclaredFrame(closeDecl.baseKind)) {
|
|
1649
|
-
warn(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
|
|
1678
|
+
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
1679
|
}
|
|
1651
1680
|
continue;
|
|
1652
1681
|
}
|
|
@@ -2859,7 +2888,8 @@ ${projectCss.trim()}` : null
|
|
|
2859
2888
|
`);
|
|
2860
2889
|
const preloadTags = (opts.preloadImages ?? []).map((href) => `
|
|
2861
2890
|
<link rel="preload" as="image" href="${href}">`).join("");
|
|
2862
|
-
const
|
|
2891
|
+
const lastPage = opts.print && lastPageMode(opts.print);
|
|
2892
|
+
const printAttrs = (opts.print?.signature && opts.print.signature > 1 ? ` data-gp-signature="${opts.print.signature}"` : "") + (lastPage ? ` data-gp-last-page="${lastPage}"` : "");
|
|
2863
2893
|
return `<!DOCTYPE html>
|
|
2864
2894
|
<html lang="en"${printAttrs}>
|
|
2865
2895
|
<head>
|
|
@@ -5641,7 +5671,7 @@ import git from "isomorphic-git";
|
|
|
5641
5671
|
// package.json
|
|
5642
5672
|
var package_default = {
|
|
5643
5673
|
name: "gutterpress",
|
|
5644
|
-
version: "0.11.17-alpha.
|
|
5674
|
+
version: "0.11.17-alpha.3",
|
|
5645
5675
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
5646
5676
|
author: "itlackey",
|
|
5647
5677
|
license: "MPL-2.0",
|
|
@@ -5895,7 +5925,8 @@ async function writeBuildFingerprint(input) {
|
|
|
5895
5925
|
requestedFlavor: input.pdfx.requestedFlavor,
|
|
5896
5926
|
resolvedFlavor: input.pdfx.resolvedFlavor,
|
|
5897
5927
|
iccPath: input.pdfx.iccPath,
|
|
5898
|
-
stripAnnotations: input.pdfx.stripAnnotations
|
|
5928
|
+
stripAnnotations: input.pdfx.stripAnnotations,
|
|
5929
|
+
blackText: input.pdfx.blackText ?? null
|
|
5899
5930
|
},
|
|
5900
5931
|
outputDir: input.recordedOutputDir ? path10.resolve(input.recordedOutputDir) : outputDir
|
|
5901
5932
|
},
|
|
@@ -5982,13 +6013,64 @@ function* tokenize(s) {
|
|
|
5982
6013
|
}
|
|
5983
6014
|
}
|
|
5984
6015
|
}
|
|
5985
|
-
|
|
6016
|
+
var LARGE_TEXT_PT = 24;
|
|
6017
|
+
var SIZE_EPS = 0.05;
|
|
6018
|
+
var area = (m) => Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
6019
|
+
|
|
6020
|
+
class TextScale {
|
|
6021
|
+
ctm;
|
|
6022
|
+
font = 0;
|
|
6023
|
+
tm = 1;
|
|
6024
|
+
stack = [];
|
|
6025
|
+
constructor(ctm) {
|
|
6026
|
+
this.ctm = ctm;
|
|
6027
|
+
}
|
|
6028
|
+
get unit() {
|
|
6029
|
+
return this.ctm;
|
|
6030
|
+
}
|
|
6031
|
+
get size() {
|
|
6032
|
+
return this.font * this.tm * this.ctm;
|
|
6033
|
+
}
|
|
6034
|
+
op(op, nums) {
|
|
6035
|
+
switch (op) {
|
|
6036
|
+
case "q":
|
|
6037
|
+
this.stack.push([this.ctm, this.font]);
|
|
6038
|
+
return false;
|
|
6039
|
+
case "Q": {
|
|
6040
|
+
const p = this.stack.pop();
|
|
6041
|
+
if (p)
|
|
6042
|
+
[this.ctm, this.font] = p;
|
|
6043
|
+
return true;
|
|
6044
|
+
}
|
|
6045
|
+
case "cm":
|
|
6046
|
+
if (nums.length === 6)
|
|
6047
|
+
this.ctm *= area(nums);
|
|
6048
|
+
return true;
|
|
6049
|
+
case "BT":
|
|
6050
|
+
this.tm = 1;
|
|
6051
|
+
return true;
|
|
6052
|
+
case "Tm":
|
|
6053
|
+
if (nums.length === 6)
|
|
6054
|
+
this.tm = area(nums);
|
|
6055
|
+
return true;
|
|
6056
|
+
case "Tf":
|
|
6057
|
+
this.font = Math.abs(nums[nums.length - 1] ?? 0) || 0;
|
|
6058
|
+
return true;
|
|
6059
|
+
}
|
|
6060
|
+
return false;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
function rewriteBlackText(src, ctx = {}) {
|
|
5986
6064
|
let fill = null;
|
|
5987
6065
|
let stroke = null;
|
|
5988
6066
|
const stack = [];
|
|
6067
|
+
const scale = new TextScale(ctx.baseScale ?? 1);
|
|
5989
6068
|
let inText = false;
|
|
5990
6069
|
let fillDirty = false;
|
|
5991
6070
|
let strokeDirty = false;
|
|
6071
|
+
const spans = [];
|
|
6072
|
+
let lastOp = "";
|
|
6073
|
+
let rect = null;
|
|
5992
6074
|
const edits = [];
|
|
5993
6075
|
let operands = [];
|
|
5994
6076
|
const rgbOf = () => {
|
|
@@ -5997,12 +6079,46 @@ function rewriteBlackText(src) {
|
|
|
5997
6079
|
const v = operands.map((t) => Number(t.text));
|
|
5998
6080
|
return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
|
|
5999
6081
|
};
|
|
6082
|
+
const isSmall = () => scale.size <= LARGE_TEXT_PT + SIZE_EPS;
|
|
6083
|
+
let fillIns = null;
|
|
6084
|
+
let strokeIns = null;
|
|
6085
|
+
const syncOne = (colour, dirty, ins, at, gray, op) => {
|
|
6086
|
+
if (!colour || !isNearBlack(colour.rgb))
|
|
6087
|
+
return [dirty, ins];
|
|
6088
|
+
const small = isSmall();
|
|
6089
|
+
if (small && !dirty) {
|
|
6090
|
+
const e = { start: at, end: at, text: ` ${gray}` };
|
|
6091
|
+
edits.push(e);
|
|
6092
|
+
return [true, e];
|
|
6093
|
+
}
|
|
6094
|
+
if (!small && dirty) {
|
|
6095
|
+
if (ins)
|
|
6096
|
+
ins.text = "";
|
|
6097
|
+
else
|
|
6098
|
+
edits.push({ start: at, end: at, text: ` ${colour.raw} ${op}` });
|
|
6099
|
+
return [false, null];
|
|
6100
|
+
}
|
|
6101
|
+
return [dirty, ins];
|
|
6102
|
+
};
|
|
6103
|
+
const sync = (at) => {
|
|
6104
|
+
[fillDirty, fillIns] = syncOne(fill, fillDirty, fillIns, at, "0 g", "rg");
|
|
6105
|
+
[strokeDirty, strokeIns] = syncOne(stroke, strokeDirty, strokeIns, at, "0 G", "RG");
|
|
6106
|
+
};
|
|
6107
|
+
const flush = (s) => {
|
|
6108
|
+
for (const r of s.rects) {
|
|
6109
|
+
if (r.thickness > 0.2 * s.maxSmall)
|
|
6110
|
+
continue;
|
|
6111
|
+
edits.push({ start: r.start, end: r.start, text: "q 0 g " }, { start: r.end, end: r.end, text: " Q" });
|
|
6112
|
+
}
|
|
6113
|
+
};
|
|
6000
6114
|
for (const t of tokenize(src)) {
|
|
6001
6115
|
if (!t.op) {
|
|
6002
6116
|
operands.push(t);
|
|
6003
6117
|
continue;
|
|
6004
6118
|
}
|
|
6005
6119
|
const op = t.text;
|
|
6120
|
+
const nums = operands.map((o) => Number(o.text));
|
|
6121
|
+
const sizeMayHaveChanged = scale.op(op, nums);
|
|
6006
6122
|
switch (op) {
|
|
6007
6123
|
case "q":
|
|
6008
6124
|
stack.push([fill, stroke]);
|
|
@@ -6016,19 +6132,13 @@ function rewriteBlackText(src) {
|
|
|
6016
6132
|
case "BT":
|
|
6017
6133
|
inText = true;
|
|
6018
6134
|
fillDirty = strokeDirty = false;
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
ins += " 0 G";
|
|
6027
|
-
strokeDirty = true;
|
|
6028
|
-
}
|
|
6029
|
-
if (ins)
|
|
6030
|
-
edits.push({ start: t.end, end: t.end, text: ins });
|
|
6031
|
-
}
|
|
6135
|
+
fillIns = strokeIns = null;
|
|
6136
|
+
sync(t.end);
|
|
6137
|
+
break;
|
|
6138
|
+
case "Tf":
|
|
6139
|
+
case "Tm":
|
|
6140
|
+
if (inText && sizeMayHaveChanged)
|
|
6141
|
+
sync(t.end);
|
|
6032
6142
|
break;
|
|
6033
6143
|
case "ET":
|
|
6034
6144
|
if (inText) {
|
|
@@ -6041,6 +6151,7 @@ function rewriteBlackText(src) {
|
|
|
6041
6151
|
edits.push({ start: t.start, end: t.start, text: ins });
|
|
6042
6152
|
}
|
|
6043
6153
|
inText = false;
|
|
6154
|
+
fillIns = strokeIns = null;
|
|
6044
6155
|
break;
|
|
6045
6156
|
case "rg":
|
|
6046
6157
|
case "RG": {
|
|
@@ -6050,7 +6161,7 @@ function rewriteBlackText(src) {
|
|
|
6050
6161
|
fill = c;
|
|
6051
6162
|
else
|
|
6052
6163
|
stroke = c;
|
|
6053
|
-
if (inText && c && isNearBlack(c.rgb)) {
|
|
6164
|
+
if (inText && c && isNearBlack(c.rgb) && isSmall()) {
|
|
6054
6165
|
edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
|
|
6055
6166
|
if (isFill)
|
|
6056
6167
|
fillDirty = true;
|
|
@@ -6082,7 +6193,54 @@ function rewriteBlackText(src) {
|
|
|
6082
6193
|
if (inText)
|
|
6083
6194
|
strokeDirty = false;
|
|
6084
6195
|
break;
|
|
6196
|
+
case "Do": {
|
|
6197
|
+
const name = operands[operands.length - 1]?.text;
|
|
6198
|
+
if (name?.startsWith("/"))
|
|
6199
|
+
ctx.onDo?.(name.slice(1), scale.unit);
|
|
6200
|
+
break;
|
|
6201
|
+
}
|
|
6202
|
+
case "BDC":
|
|
6203
|
+
case "BMC":
|
|
6204
|
+
spans.push({ hasSmall: false, maxSmall: 0, rects: [] });
|
|
6205
|
+
break;
|
|
6206
|
+
case "EMC": {
|
|
6207
|
+
const s = spans.pop();
|
|
6208
|
+
if (!s)
|
|
6209
|
+
break;
|
|
6210
|
+
const parent = spans[spans.length - 1];
|
|
6211
|
+
if (s.hasSmall)
|
|
6212
|
+
flush(s);
|
|
6213
|
+
else
|
|
6214
|
+
parent?.rects.push(...s.rects);
|
|
6215
|
+
if (parent && s.hasSmall) {
|
|
6216
|
+
parent.hasSmall = true;
|
|
6217
|
+
parent.maxSmall = Math.max(parent.maxSmall, s.maxSmall);
|
|
6218
|
+
}
|
|
6219
|
+
break;
|
|
6220
|
+
}
|
|
6221
|
+
case "Tj":
|
|
6222
|
+
case "TJ":
|
|
6223
|
+
case "'":
|
|
6224
|
+
case '"': {
|
|
6225
|
+
fillIns = strokeIns = null;
|
|
6226
|
+
const span = spans[spans.length - 1];
|
|
6227
|
+
if (span && inText && isSmall()) {
|
|
6228
|
+
span.hasSmall = true;
|
|
6229
|
+
span.maxSmall = Math.max(span.maxSmall, scale.size);
|
|
6230
|
+
}
|
|
6231
|
+
break;
|
|
6232
|
+
}
|
|
6233
|
+
case "re":
|
|
6234
|
+
rect = !inText && fill && isNearBlack(fill.rgb) && operands.length === 4 && nums.every((x) => !Number.isNaN(x)) ? { start: operands[0].start, thickness: Math.min(Math.abs(nums[2]), Math.abs(nums[3])) * scale.unit } : null;
|
|
6235
|
+
break;
|
|
6236
|
+
case "f":
|
|
6237
|
+
case "F":
|
|
6238
|
+
case "f*":
|
|
6239
|
+
if (rect && lastOp === "re")
|
|
6240
|
+
spans[spans.length - 1]?.rects.push({ ...rect, end: t.end });
|
|
6241
|
+
break;
|
|
6085
6242
|
}
|
|
6243
|
+
lastOp = op;
|
|
6086
6244
|
operands = [];
|
|
6087
6245
|
}
|
|
6088
6246
|
return applyEdits(src, edits);
|
|
@@ -6090,6 +6248,7 @@ function rewriteBlackText(src) {
|
|
|
6090
6248
|
function applyEdits(s, edits) {
|
|
6091
6249
|
if (!edits.length)
|
|
6092
6250
|
return s;
|
|
6251
|
+
edits.sort((a, b) => a.start - b.start);
|
|
6093
6252
|
let out = "";
|
|
6094
6253
|
let pos = 0;
|
|
6095
6254
|
for (const e of edits) {
|
|
@@ -6098,9 +6257,8 @@ function applyEdits(s, edits) {
|
|
|
6098
6257
|
}
|
|
6099
6258
|
return out + s.slice(pos);
|
|
6100
6259
|
}
|
|
6101
|
-
async function
|
|
6102
|
-
const {
|
|
6103
|
-
const doc = await PDFDocument.load(pdf, { updateMetadata: false });
|
|
6260
|
+
async function visitContent(doc, visit) {
|
|
6261
|
+
const { PDFName, PDFRawStream, PDFArray, PDFRef, PDFDict, decodePDFRawStream } = await import("pdf-lib");
|
|
6104
6262
|
const ctx = doc.context;
|
|
6105
6263
|
const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
|
|
6106
6264
|
let changed = false;
|
|
@@ -6119,19 +6277,15 @@ async function blackTextToGray(pdf) {
|
|
|
6119
6277
|
ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
|
|
6120
6278
|
changed = true;
|
|
6121
6279
|
};
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
const
|
|
6131
|
-
if (next !== text)
|
|
6132
|
-
replace(ref, obj, next);
|
|
6133
|
-
}
|
|
6134
|
-
for (const page of doc.getPages()) {
|
|
6280
|
+
const formScale = new Map;
|
|
6281
|
+
const formMatrixScale = (form) => {
|
|
6282
|
+
const m = form.dict.lookupMaybe(PDFName.of("Matrix"), PDFArray);
|
|
6283
|
+
const nums = m?.asArray().map((n) => Number(n.toString()));
|
|
6284
|
+
return nums?.length === 6 && !nums.some(Number.isNaN) ? area(nums) : 1;
|
|
6285
|
+
};
|
|
6286
|
+
const pages = doc.getPages();
|
|
6287
|
+
for (let p = 0;p < pages.length; p++) {
|
|
6288
|
+
const page = pages[p];
|
|
6135
6289
|
const contents = page.node.Contents();
|
|
6136
6290
|
const refs = [];
|
|
6137
6291
|
if (contents instanceof PDFArray) {
|
|
@@ -6149,16 +6303,49 @@ async function blackTextToGray(pdf) {
|
|
|
6149
6303
|
if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
|
|
6150
6304
|
continue;
|
|
6151
6305
|
const parts = streams.map((s) => decode(s));
|
|
6152
|
-
if (parts.some((
|
|
6306
|
+
if (parts.some((q) => q === null))
|
|
6153
6307
|
continue;
|
|
6154
6308
|
const joined = parts.join(`
|
|
6155
6309
|
`);
|
|
6156
|
-
const
|
|
6310
|
+
const xobjects = page.node.Resources()?.lookupMaybe(PDFName.of("XObject"), PDFDict);
|
|
6311
|
+
const next = visit(joined, {
|
|
6312
|
+
baseScale: 1,
|
|
6313
|
+
onDo: (name, scale) => {
|
|
6314
|
+
const ref = xobjects?.get(PDFName.of(name));
|
|
6315
|
+
const form = ref instanceof PDFRef ? ctx.lookup(ref) : undefined;
|
|
6316
|
+
if (!(ref instanceof PDFRef) || !(form instanceof PDFRawStream))
|
|
6317
|
+
return;
|
|
6318
|
+
if (form.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
|
|
6319
|
+
return;
|
|
6320
|
+
const at = scale * formMatrixScale(form);
|
|
6321
|
+
formScale.set(ref.tag, Math.min(formScale.get(ref.tag) ?? Infinity, at));
|
|
6322
|
+
}
|
|
6323
|
+
});
|
|
6157
6324
|
if (next === joined)
|
|
6158
6325
|
continue;
|
|
6159
6326
|
replace(refs[0], streams[0], next);
|
|
6160
6327
|
page.node.set(PDFName.of("Contents"), refs[0]);
|
|
6161
6328
|
}
|
|
6329
|
+
for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
|
|
6330
|
+
if (!(obj instanceof PDFRawStream))
|
|
6331
|
+
continue;
|
|
6332
|
+
if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
|
|
6333
|
+
continue;
|
|
6334
|
+
const text = decode(obj);
|
|
6335
|
+
if (text === null)
|
|
6336
|
+
continue;
|
|
6337
|
+
const next = visit(text, {
|
|
6338
|
+
baseScale: formScale.get(ref.tag) ?? 0
|
|
6339
|
+
});
|
|
6340
|
+
if (next !== text)
|
|
6341
|
+
replace(ref, obj, next);
|
|
6342
|
+
}
|
|
6343
|
+
return changed;
|
|
6344
|
+
}
|
|
6345
|
+
async function blackTextToGray(pdf) {
|
|
6346
|
+
const { PDFDocument } = await import("pdf-lib");
|
|
6347
|
+
const doc = await PDFDocument.load(pdf, { updateMetadata: false });
|
|
6348
|
+
const changed = await visitContent(doc, (src, info2) => rewriteBlackText(src, info2));
|
|
6162
6349
|
return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
|
|
6163
6350
|
}
|
|
6164
6351
|
|
|
@@ -6766,6 +6953,32 @@ function getImagePixelSize(page, name, timeoutMs = 2000) {
|
|
|
6766
6953
|
}
|
|
6767
6954
|
});
|
|
6768
6955
|
}
|
|
6956
|
+
var PAINT_OPS = new Set([
|
|
6957
|
+
"stroke",
|
|
6958
|
+
"closeStroke",
|
|
6959
|
+
"fill",
|
|
6960
|
+
"eoFill",
|
|
6961
|
+
"fillStroke",
|
|
6962
|
+
"eoFillStroke",
|
|
6963
|
+
"closeFillStroke",
|
|
6964
|
+
"closeEOFillStroke",
|
|
6965
|
+
"shadingFill",
|
|
6966
|
+
"paintImageXObject",
|
|
6967
|
+
"paintInlineImageXObject",
|
|
6968
|
+
"paintImageXObjectRepeat",
|
|
6969
|
+
"paintImageMaskXObject",
|
|
6970
|
+
"paintImageMaskXObjectGroup",
|
|
6971
|
+
"paintImageMaskXObjectRepeat",
|
|
6972
|
+
"paintSolidColorImageMask",
|
|
6973
|
+
"showText",
|
|
6974
|
+
"showSpacedText",
|
|
6975
|
+
"nextLineShowText",
|
|
6976
|
+
"nextLineSetSpacingShowText"
|
|
6977
|
+
].map((name) => OPS[name]).filter((op) => op !== undefined));
|
|
6978
|
+
async function pageHasMarks(doc, pageNum) {
|
|
6979
|
+
const ops = await (await doc.getPage(pageNum)).getOperatorList();
|
|
6980
|
+
return ops.fnArray.some((fn, i) => fn === OPS.constructPath ? PAINT_OPS.has(ops.argsArray[i][0]) : PAINT_OPS.has(fn));
|
|
6981
|
+
}
|
|
6769
6982
|
async function getImageResolutions(doc) {
|
|
6770
6983
|
const { imagesByPage } = await getOpPass(doc);
|
|
6771
6984
|
const out = [];
|
|
@@ -7269,8 +7482,62 @@ var check8 = {
|
|
|
7269
7482
|
};
|
|
7270
7483
|
registerCheck(check8);
|
|
7271
7484
|
|
|
7272
|
-
// src/checks/pdf/
|
|
7485
|
+
// src/checks/pdf/signature.ts
|
|
7273
7486
|
var check9 = {
|
|
7487
|
+
id: "pdf.print.signature",
|
|
7488
|
+
name: "Signature",
|
|
7489
|
+
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)",
|
|
7490
|
+
category: "pdf",
|
|
7491
|
+
phase: "post-build",
|
|
7492
|
+
enabledWhen: (config) => config.print.signature > 1,
|
|
7493
|
+
async run(ctx) {
|
|
7494
|
+
if (!ctx.pdfPath)
|
|
7495
|
+
return [];
|
|
7496
|
+
const doc = await loadPdf(ctx.pdfPath);
|
|
7497
|
+
if (!doc) {
|
|
7498
|
+
return [inspectionFailed(check9.id, "Could not parse the PDF's page count.", { file: ctx.pdfPath })];
|
|
7499
|
+
}
|
|
7500
|
+
const size = ctx.config.print.signature;
|
|
7501
|
+
const pages = doc.numPages;
|
|
7502
|
+
if ((pages + 1) % size === 0)
|
|
7503
|
+
return [];
|
|
7504
|
+
const data = { pages, signature: size };
|
|
7505
|
+
if (pages % size !== 0) {
|
|
7506
|
+
return [
|
|
7507
|
+
finding(check9.id, {
|
|
7508
|
+
severity: "error",
|
|
7509
|
+
message: `Page count ${pages} does not fit a signature of ${size}: it must be a multiple of ${size}, or one short of one.`,
|
|
7510
|
+
file: ctx.pdfPath,
|
|
7511
|
+
detail: "Set `print.signature` in the manifest and rebuild; the build pads the interior for you.",
|
|
7512
|
+
code: "signature-page-count",
|
|
7513
|
+
data
|
|
7514
|
+
})
|
|
7515
|
+
];
|
|
7516
|
+
}
|
|
7517
|
+
if (!lastPageMode(ctx.config.print))
|
|
7518
|
+
return [];
|
|
7519
|
+
try {
|
|
7520
|
+
if (!await pageHasMarks(doc, pages))
|
|
7521
|
+
return [];
|
|
7522
|
+
} catch {
|
|
7523
|
+
return [inspectionFailed(check9.id, "Could not read the last page.", { file: ctx.pdfPath })];
|
|
7524
|
+
}
|
|
7525
|
+
return [
|
|
7526
|
+
finding(check9.id, {
|
|
7527
|
+
severity: "error",
|
|
7528
|
+
message: `The last page (${pages}) has content, but the printer needs it blank for its own information.`,
|
|
7529
|
+
file: ctx.pdfPath,
|
|
7530
|
+
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.",
|
|
7531
|
+
code: "signature-last-page",
|
|
7532
|
+
data
|
|
7533
|
+
})
|
|
7534
|
+
];
|
|
7535
|
+
}
|
|
7536
|
+
};
|
|
7537
|
+
registerCheck(check9);
|
|
7538
|
+
|
|
7539
|
+
// src/checks/pdf/bookmarks.ts
|
|
7540
|
+
var check10 = {
|
|
7274
7541
|
id: "pdf.nav.bookmarks",
|
|
7275
7542
|
name: "Bookmarks",
|
|
7276
7543
|
description: "Checks for PDF outline (bookmarks) tree",
|
|
@@ -7284,14 +7551,14 @@ var check9 = {
|
|
|
7284
7551
|
const doc = await loadPdf(ctx.pdfPath);
|
|
7285
7552
|
if (!doc) {
|
|
7286
7553
|
return [
|
|
7287
|
-
inspectionFailed(
|
|
7554
|
+
inspectionFailed(check10.id, "Could not inspect PDF for bookmarks.", {
|
|
7288
7555
|
file: ctx.pdfPath
|
|
7289
7556
|
})
|
|
7290
7557
|
];
|
|
7291
7558
|
}
|
|
7292
7559
|
if (await getOutlineCount(doc) === 0) {
|
|
7293
7560
|
return [
|
|
7294
|
-
finding(
|
|
7561
|
+
finding(check10.id, {
|
|
7295
7562
|
severity: "warning",
|
|
7296
7563
|
message: "PDF does not contain bookmarks (outline tree).",
|
|
7297
7564
|
file: ctx.pdfPath
|
|
@@ -7301,10 +7568,10 @@ var check9 = {
|
|
|
7301
7568
|
return [];
|
|
7302
7569
|
}
|
|
7303
7570
|
};
|
|
7304
|
-
registerCheck(
|
|
7571
|
+
registerCheck(check10);
|
|
7305
7572
|
|
|
7306
7573
|
// src/checks/pdf/toc-links.ts
|
|
7307
|
-
var
|
|
7574
|
+
var check11 = {
|
|
7308
7575
|
id: "pdf.nav.toc-links",
|
|
7309
7576
|
name: "TOC Links",
|
|
7310
7577
|
description: "Detects link annotations on TOC pages",
|
|
@@ -7319,7 +7586,7 @@ var check10 = {
|
|
|
7319
7586
|
if (!doc) {
|
|
7320
7587
|
return [
|
|
7321
7588
|
{
|
|
7322
|
-
checkId:
|
|
7589
|
+
checkId: check11.id,
|
|
7323
7590
|
severity: "warning",
|
|
7324
7591
|
message: "Could not inspect PDF for TOC links.",
|
|
7325
7592
|
file: ctx.pdfPath
|
|
@@ -7330,7 +7597,7 @@ var check10 = {
|
|
|
7330
7597
|
if (!hasLinks) {
|
|
7331
7598
|
return [
|
|
7332
7599
|
{
|
|
7333
|
-
checkId:
|
|
7600
|
+
checkId: check11.id,
|
|
7334
7601
|
severity: "warning",
|
|
7335
7602
|
message: "No link annotations found in PDF (expected for TOC).",
|
|
7336
7603
|
file: ctx.pdfPath
|
|
@@ -7340,10 +7607,10 @@ var check10 = {
|
|
|
7340
7607
|
return [];
|
|
7341
7608
|
}
|
|
7342
7609
|
};
|
|
7343
|
-
registerCheck(
|
|
7610
|
+
registerCheck(check11);
|
|
7344
7611
|
|
|
7345
7612
|
// src/checks/pdf/cross-refs.ts
|
|
7346
|
-
var
|
|
7613
|
+
var check12 = {
|
|
7347
7614
|
id: "pdf.nav.cross-refs",
|
|
7348
7615
|
name: "Cross References",
|
|
7349
7616
|
description: "Counts and verifies internal link annotations",
|
|
@@ -7356,7 +7623,7 @@ var check11 = {
|
|
|
7356
7623
|
if (!doc) {
|
|
7357
7624
|
return [
|
|
7358
7625
|
{
|
|
7359
|
-
checkId:
|
|
7626
|
+
checkId: check12.id,
|
|
7360
7627
|
severity: "info",
|
|
7361
7628
|
message: "Could not inspect PDF for cross-references.",
|
|
7362
7629
|
file: ctx.pdfPath
|
|
@@ -7366,7 +7633,7 @@ var check11 = {
|
|
|
7366
7633
|
const linkCount = await countLinkAnnotations(doc);
|
|
7367
7634
|
return [
|
|
7368
7635
|
{
|
|
7369
|
-
checkId:
|
|
7636
|
+
checkId: check12.id,
|
|
7370
7637
|
severity: "info",
|
|
7371
7638
|
message: `PDF contains ${linkCount} internal link annotation${linkCount !== 1 ? "s" : ""}.`,
|
|
7372
7639
|
file: ctx.pdfPath
|
|
@@ -7374,10 +7641,10 @@ var check11 = {
|
|
|
7374
7641
|
];
|
|
7375
7642
|
}
|
|
7376
7643
|
};
|
|
7377
|
-
registerCheck(
|
|
7644
|
+
registerCheck(check12);
|
|
7378
7645
|
|
|
7379
7646
|
// src/checks/pdf/page-labels.ts
|
|
7380
|
-
var
|
|
7647
|
+
var check13 = {
|
|
7381
7648
|
id: "pdf.nav.page-labels",
|
|
7382
7649
|
name: "Page Labels",
|
|
7383
7650
|
description: "Verifies PDF page labels / folio numbering",
|
|
@@ -7392,7 +7659,7 @@ var check12 = {
|
|
|
7392
7659
|
const labels = await getPageLabels(doc);
|
|
7393
7660
|
return [
|
|
7394
7661
|
{
|
|
7395
|
-
checkId:
|
|
7662
|
+
checkId: check13.id,
|
|
7396
7663
|
severity: "info",
|
|
7397
7664
|
message: labels && labels.length > 0 ? "PDF contains page labels." : "PDF does not contain explicit page labels.",
|
|
7398
7665
|
file: ctx.pdfPath
|
|
@@ -7400,10 +7667,10 @@ var check12 = {
|
|
|
7400
7667
|
];
|
|
7401
7668
|
}
|
|
7402
7669
|
};
|
|
7403
|
-
registerCheck(
|
|
7670
|
+
registerCheck(check13);
|
|
7404
7671
|
|
|
7405
7672
|
// src/checks/pdf/image-resolution.ts
|
|
7406
|
-
var
|
|
7673
|
+
var check14 = {
|
|
7407
7674
|
id: "pdf.print.image-resolution",
|
|
7408
7675
|
name: "Image Resolution",
|
|
7409
7676
|
description: "Checks DPI of images embedded in the PDF",
|
|
@@ -7430,7 +7697,7 @@ var check13 = {
|
|
|
7430
7697
|
lowResPages.sort((a, b) => a - b);
|
|
7431
7698
|
return [
|
|
7432
7699
|
{
|
|
7433
|
-
checkId:
|
|
7700
|
+
checkId: check14.id,
|
|
7434
7701
|
severity: "warning",
|
|
7435
7702
|
message: `Low-resolution images (below ${minDpi} DPI) found on pages: ${lowResPages.join(", ")}`,
|
|
7436
7703
|
file: ctx.pdfPath
|
|
@@ -7440,10 +7707,10 @@ var check13 = {
|
|
|
7440
7707
|
return [];
|
|
7441
7708
|
}
|
|
7442
7709
|
};
|
|
7443
|
-
registerCheck(
|
|
7710
|
+
registerCheck(check14);
|
|
7444
7711
|
|
|
7445
7712
|
// src/checks/pdf/transparency.ts
|
|
7446
|
-
var
|
|
7713
|
+
var check15 = {
|
|
7447
7714
|
id: "pdf.print.transparency",
|
|
7448
7715
|
name: "Transparency",
|
|
7449
7716
|
description: "Checks for transparency markers (/Transparency, /SMask, /BM) in the PDF",
|
|
@@ -7465,7 +7732,7 @@ var check14 = {
|
|
|
7465
7732
|
if (found.length > 0) {
|
|
7466
7733
|
return [
|
|
7467
7734
|
{
|
|
7468
|
-
checkId:
|
|
7735
|
+
checkId: check15.id,
|
|
7469
7736
|
severity: "warning",
|
|
7470
7737
|
message: `Transparency detected in PDF: ${found.join(", ")}. This may cause issues with some printers.`,
|
|
7471
7738
|
file: ctx.pdfPath
|
|
@@ -7475,7 +7742,7 @@ var check14 = {
|
|
|
7475
7742
|
return [];
|
|
7476
7743
|
}
|
|
7477
7744
|
};
|
|
7478
|
-
registerCheck(
|
|
7745
|
+
registerCheck(check15);
|
|
7479
7746
|
|
|
7480
7747
|
// src/checks/pdf/bleed.ts
|
|
7481
7748
|
function findBox(bytes, box) {
|
|
@@ -7485,7 +7752,7 @@ function findBox(bytes, box) {
|
|
|
7485
7752
|
return null;
|
|
7486
7753
|
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
7487
7754
|
}
|
|
7488
|
-
var
|
|
7755
|
+
var check16 = {
|
|
7489
7756
|
id: "pdf.print.bleed",
|
|
7490
7757
|
name: "Bleed Box",
|
|
7491
7758
|
description: "Compares MediaBox vs TrimBox/BleedBox to verify bleed area",
|
|
@@ -7499,7 +7766,7 @@ var check15 = {
|
|
|
7499
7766
|
const bytes = await readPdfBytes(ctx.pdfPath).catch(() => "");
|
|
7500
7767
|
if (!bytes) {
|
|
7501
7768
|
return [
|
|
7502
|
-
inspectionFailed(
|
|
7769
|
+
inspectionFailed(check16.id, "Could not inspect PDF for bleed boxes.", {
|
|
7503
7770
|
file: ctx.pdfPath
|
|
7504
7771
|
})
|
|
7505
7772
|
];
|
|
@@ -7509,14 +7776,14 @@ var check15 = {
|
|
|
7509
7776
|
const bleed = findBox(bytes, "BleedBox");
|
|
7510
7777
|
if (!media) {
|
|
7511
7778
|
return [
|
|
7512
|
-
inspectionFailed(
|
|
7779
|
+
inspectionFailed(check16.id, "Could not parse MediaBox from PDF.", {
|
|
7513
7780
|
file: ctx.pdfPath
|
|
7514
7781
|
})
|
|
7515
7782
|
];
|
|
7516
7783
|
}
|
|
7517
7784
|
if (!trim && !bleed) {
|
|
7518
7785
|
return [
|
|
7519
|
-
finding(
|
|
7786
|
+
finding(check16.id, {
|
|
7520
7787
|
severity: "warning",
|
|
7521
7788
|
message: "No TrimBox or BleedBox found. Bleed area cannot be verified.",
|
|
7522
7789
|
file: ctx.pdfPath
|
|
@@ -7529,7 +7796,7 @@ var check15 = {
|
|
|
7529
7796
|
const bleedSize = ctx.config.validate.pdf.bleedSize;
|
|
7530
7797
|
if (mediaW - trimW < bleedSize * 2 * 0.9) {
|
|
7531
7798
|
return [
|
|
7532
|
-
finding(
|
|
7799
|
+
finding(check16.id, {
|
|
7533
7800
|
severity: "warning",
|
|
7534
7801
|
message: `Bleed area appears insufficient. Expected at least ${bleedSize}pt on each side.`,
|
|
7535
7802
|
file: ctx.pdfPath
|
|
@@ -7540,10 +7807,10 @@ var check15 = {
|
|
|
7540
7807
|
return [];
|
|
7541
7808
|
}
|
|
7542
7809
|
};
|
|
7543
|
-
registerCheck(
|
|
7810
|
+
registerCheck(check16);
|
|
7544
7811
|
|
|
7545
7812
|
// src/checks/pdf/pdfx-metadata.ts
|
|
7546
|
-
var
|
|
7813
|
+
var check17 = {
|
|
7547
7814
|
id: "pdf.print.pdfx-metadata",
|
|
7548
7815
|
name: "PDF/X Metadata",
|
|
7549
7816
|
description: "Verifies PDF/X DOCINFO metadata via qpdf JSON",
|
|
@@ -7562,22 +7829,22 @@ var check16 = {
|
|
|
7562
7829
|
const objects = parseQpdfObjectsJson(stdout);
|
|
7563
7830
|
if (!objects) {
|
|
7564
7831
|
return [
|
|
7565
|
-
inspectionFailed(
|
|
7832
|
+
inspectionFailed(check17.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
|
|
7566
7833
|
];
|
|
7567
7834
|
}
|
|
7568
|
-
return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(
|
|
7835
|
+
return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check17.id, {
|
|
7569
7836
|
severity: "error",
|
|
7570
7837
|
message,
|
|
7571
7838
|
file: ctx.pdfPath
|
|
7572
7839
|
}));
|
|
7573
7840
|
} catch {
|
|
7574
7841
|
return [
|
|
7575
|
-
inspectionFailed(
|
|
7842
|
+
inspectionFailed(check17.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
|
|
7576
7843
|
];
|
|
7577
7844
|
}
|
|
7578
7845
|
}
|
|
7579
7846
|
};
|
|
7580
|
-
registerCheck(
|
|
7847
|
+
registerCheck(check17);
|
|
7581
7848
|
|
|
7582
7849
|
// src/checks/source/htmlhint.ts
|
|
7583
7850
|
import { existsSync as existsSync9 } from "node:fs";
|
|
@@ -7585,7 +7852,7 @@ import { readFile as readFile15 } from "node:fs/promises";
|
|
|
7585
7852
|
import htmlhint from "htmlhint";
|
|
7586
7853
|
var { HTMLHint } = htmlhint;
|
|
7587
7854
|
var CONFIG_NAMES2 = [".htmlhintrc"];
|
|
7588
|
-
var
|
|
7855
|
+
var check18 = {
|
|
7589
7856
|
id: "source.htmlhint",
|
|
7590
7857
|
name: "HTMLHint",
|
|
7591
7858
|
description: "Runs htmlhint with project config to validate generated HTML",
|
|
@@ -7605,7 +7872,7 @@ var check17 = {
|
|
|
7605
7872
|
const html = await readFile15(ctx.htmlPath, "utf8");
|
|
7606
7873
|
const messages = HTMLHint.verify(html, ruleset);
|
|
7607
7874
|
return messages.map((m) => ({
|
|
7608
|
-
checkId:
|
|
7875
|
+
checkId: check18.id,
|
|
7609
7876
|
severity: m.type === "error" ? "error" : "warning",
|
|
7610
7877
|
message: `${m.rule.id}: ${m.message}`,
|
|
7611
7878
|
file: ctx.htmlPath,
|
|
@@ -7614,13 +7881,35 @@ var check17 = {
|
|
|
7614
7881
|
}));
|
|
7615
7882
|
}
|
|
7616
7883
|
};
|
|
7617
|
-
registerCheck(
|
|
7884
|
+
registerCheck(check18);
|
|
7618
7885
|
|
|
7619
7886
|
// src/checks/source/stylelint.ts
|
|
7620
7887
|
import { readFile as readFile16 } from "node:fs/promises";
|
|
7621
7888
|
|
|
7622
7889
|
// src/lib/printsafe.ts
|
|
7623
7890
|
import postcss2 from "postcss";
|
|
7891
|
+
|
|
7892
|
+
// src/lib/lint-rules.ts
|
|
7893
|
+
function ruleLevel(rules, id) {
|
|
7894
|
+
const v = rules?.[id];
|
|
7895
|
+
if (v === "off" || v === false)
|
|
7896
|
+
return "off";
|
|
7897
|
+
if (v === "warn" || v === "warning")
|
|
7898
|
+
return "warn";
|
|
7899
|
+
if (v === "error")
|
|
7900
|
+
return "error";
|
|
7901
|
+
return;
|
|
7902
|
+
}
|
|
7903
|
+
var DIRECTIVE = /^\s*gutterpress-disable-next-line\s+(.*?)\s*$/;
|
|
7904
|
+
function parseDisableDirective(commentText) {
|
|
7905
|
+
const m = DIRECTIVE.exec(commentText);
|
|
7906
|
+
if (!m)
|
|
7907
|
+
return null;
|
|
7908
|
+
const ids = m[1].split(/\s--(?:\s|$)/)[0].split(/[\s,]+/).filter(Boolean);
|
|
7909
|
+
return ids.length > 0 ? ids : null;
|
|
7910
|
+
}
|
|
7911
|
+
|
|
7912
|
+
// src/lib/printsafe.ts
|
|
7624
7913
|
var ruleRemoteUrls = "printsafe/no-remote-urls";
|
|
7625
7914
|
var ruleRiskyProps = "printsafe/no-risky-print-effects";
|
|
7626
7915
|
var ruleSyntax = "printsafe/syntax-error";
|
|
@@ -7645,9 +7934,11 @@ var riskyProperties = new Set([
|
|
|
7645
7934
|
"animation",
|
|
7646
7935
|
"animation-name",
|
|
7647
7936
|
"transition",
|
|
7648
|
-
"will-change"
|
|
7649
|
-
"clip-path"
|
|
7937
|
+
"will-change"
|
|
7650
7938
|
]);
|
|
7939
|
+
function isRasterizingClipPath(decl) {
|
|
7940
|
+
return decl.prop.toLowerCase() === "clip-path" && /\burl\(/i.test(decl.value);
|
|
7941
|
+
}
|
|
7651
7942
|
var marginBoxAtRuleNames = new Set([
|
|
7652
7943
|
"top-left-corner",
|
|
7653
7944
|
"top-left",
|
|
@@ -7774,7 +8065,34 @@ function nodeLoc(node) {
|
|
|
7774
8065
|
column: node.source?.start?.column ?? 1
|
|
7775
8066
|
};
|
|
7776
8067
|
}
|
|
7777
|
-
function checkCss(css, from) {
|
|
8068
|
+
function checkCss(css, from, options = {}) {
|
|
8069
|
+
return filterFindings(checkCssRaw(css, from), css, options.rules);
|
|
8070
|
+
}
|
|
8071
|
+
function filterFindings(findings, css, rules) {
|
|
8072
|
+
const silenced = new Map;
|
|
8073
|
+
for (const m of css.matchAll(/\/\*([\s\S]*?)\*\//g)) {
|
|
8074
|
+
const ids = parseDisableDirective(m[1]);
|
|
8075
|
+
if (!ids)
|
|
8076
|
+
continue;
|
|
8077
|
+
const endLine = css.slice(0, m.index + m[0].length).split(`
|
|
8078
|
+
`).length;
|
|
8079
|
+
const set = silenced.get(endLine + 1) ?? new Set;
|
|
8080
|
+
for (const id of ids)
|
|
8081
|
+
set.add(id);
|
|
8082
|
+
silenced.set(endLine + 1, set);
|
|
8083
|
+
}
|
|
8084
|
+
const out = [];
|
|
8085
|
+
for (const f of findings) {
|
|
8086
|
+
if (silenced.get(f.line)?.has(f.rule))
|
|
8087
|
+
continue;
|
|
8088
|
+
const level = ruleLevel(rules, f.rule);
|
|
8089
|
+
if (level === "off")
|
|
8090
|
+
continue;
|
|
8091
|
+
out.push(level === "warn" ? { ...f, severity: "warning" } : level === "error" ? { ...f, severity: "error" } : f);
|
|
8092
|
+
}
|
|
8093
|
+
return out;
|
|
8094
|
+
}
|
|
8095
|
+
function checkCssRaw(css, from) {
|
|
7778
8096
|
let root;
|
|
7779
8097
|
try {
|
|
7780
8098
|
root = postcss2.parse(css, from ? { from } : undefined);
|
|
@@ -7823,6 +8141,13 @@ function checkCss(css, from) {
|
|
|
7823
8141
|
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
8142
|
...nodeLoc(decl)
|
|
7825
8143
|
});
|
|
8144
|
+
} else if (isRasterizingClipPath(decl)) {
|
|
8145
|
+
warnings.push({
|
|
8146
|
+
rule: ruleRiskyProps,
|
|
8147
|
+
severity: "warning",
|
|
8148
|
+
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.`,
|
|
8149
|
+
...nodeLoc(decl)
|
|
8150
|
+
});
|
|
7826
8151
|
} else if (riskyProperties.has(prop)) {
|
|
7827
8152
|
warnings.push({
|
|
7828
8153
|
rule: ruleRiskyProps,
|
|
@@ -7876,7 +8201,7 @@ function checkCss(css, from) {
|
|
|
7876
8201
|
}
|
|
7877
8202
|
|
|
7878
8203
|
// src/checks/source/stylelint.ts
|
|
7879
|
-
var
|
|
8204
|
+
var check19 = {
|
|
7880
8205
|
id: "source.stylelint",
|
|
7881
8206
|
name: "Print-safety CSS",
|
|
7882
8207
|
description: "Checks CSS for print-safety issues (remote URLs, rasterizing effects, and page-layer containment)",
|
|
@@ -7896,16 +8221,16 @@ var check18 = {
|
|
|
7896
8221
|
css = await readFile16(file, "utf8");
|
|
7897
8222
|
} catch (err) {
|
|
7898
8223
|
results.push({
|
|
7899
|
-
checkId:
|
|
8224
|
+
checkId: check19.id,
|
|
7900
8225
|
severity: "error",
|
|
7901
8226
|
message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
|
|
7902
8227
|
file
|
|
7903
8228
|
});
|
|
7904
8229
|
continue;
|
|
7905
8230
|
}
|
|
7906
|
-
for (const w of checkCss(css, file)) {
|
|
8231
|
+
for (const w of checkCss(css, file, { rules: ctx.config.lint.rules })) {
|
|
7907
8232
|
results.push({
|
|
7908
|
-
checkId:
|
|
8233
|
+
checkId: check19.id,
|
|
7909
8234
|
severity: w.severity,
|
|
7910
8235
|
message: `${w.rule}: ${w.message}`,
|
|
7911
8236
|
file,
|
|
@@ -7917,7 +8242,7 @@ var check18 = {
|
|
|
7917
8242
|
return results;
|
|
7918
8243
|
}
|
|
7919
8244
|
};
|
|
7920
|
-
registerCheck(
|
|
8245
|
+
registerCheck(check19);
|
|
7921
8246
|
|
|
7922
8247
|
// src/checks/source/css-ownership.ts
|
|
7923
8248
|
import { readFile as readFile17 } from "node:fs/promises";
|
|
@@ -8038,7 +8363,7 @@ function checkOwnership(root, file, maps) {
|
|
|
8038
8363
|
});
|
|
8039
8364
|
return out;
|
|
8040
8365
|
}
|
|
8041
|
-
var
|
|
8366
|
+
var check20 = {
|
|
8042
8367
|
id: "source.css-ownership",
|
|
8043
8368
|
name: "CSS ownership contract",
|
|
8044
8369
|
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 +8386,7 @@ var check19 = {
|
|
|
8061
8386
|
} catch (err) {
|
|
8062
8387
|
return [
|
|
8063
8388
|
{
|
|
8064
|
-
checkId:
|
|
8389
|
+
checkId: check20.id,
|
|
8065
8390
|
severity: "error",
|
|
8066
8391
|
message: `Failed to parse CSS ownership contract "${configPath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
8067
8392
|
file: configPath
|
|
@@ -8103,7 +8428,7 @@ var check19 = {
|
|
|
8103
8428
|
}
|
|
8104
8429
|
for (const v of violations) {
|
|
8105
8430
|
results.push({
|
|
8106
|
-
checkId:
|
|
8431
|
+
checkId: check20.id,
|
|
8107
8432
|
severity: "warning",
|
|
8108
8433
|
message: v.message,
|
|
8109
8434
|
file: absFile,
|
|
@@ -8115,7 +8440,7 @@ var check19 = {
|
|
|
8115
8440
|
return results;
|
|
8116
8441
|
}
|
|
8117
8442
|
};
|
|
8118
|
-
registerCheck(
|
|
8443
|
+
registerCheck(check20);
|
|
8119
8444
|
|
|
8120
8445
|
// src/checks/source/local-refs.ts
|
|
8121
8446
|
import { existsSync as existsSync10 } from "node:fs";
|
|
@@ -8202,7 +8527,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
|
|
|
8202
8527
|
}
|
|
8203
8528
|
|
|
8204
8529
|
// src/checks/source/local-refs.ts
|
|
8205
|
-
var
|
|
8530
|
+
var check21 = {
|
|
8206
8531
|
id: "source.links.local-refs",
|
|
8207
8532
|
name: "Local Markdown References",
|
|
8208
8533
|
description: "Checks local markdown links and image refs resolve on disk",
|
|
@@ -8210,12 +8535,12 @@ var check20 = {
|
|
|
8210
8535
|
phase: "pre-build",
|
|
8211
8536
|
async run(ctx) {
|
|
8212
8537
|
const results = [];
|
|
8213
|
-
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx,
|
|
8538
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
|
|
8214
8539
|
if (isNonFilesystemRef(ref))
|
|
8215
8540
|
continue;
|
|
8216
8541
|
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
8217
8542
|
if (escape) {
|
|
8218
|
-
results.push(finding(
|
|
8543
|
+
results.push(finding(check21.id, {
|
|
8219
8544
|
severity: "error",
|
|
8220
8545
|
message: escape,
|
|
8221
8546
|
file,
|
|
@@ -8225,7 +8550,7 @@ var check20 = {
|
|
|
8225
8550
|
}
|
|
8226
8551
|
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
8227
8552
|
continue;
|
|
8228
|
-
results.push(finding(
|
|
8553
|
+
results.push(finding(check21.id, {
|
|
8229
8554
|
severity: kind === "image" ? "warning" : "error",
|
|
8230
8555
|
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
8231
8556
|
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
@@ -8250,7 +8575,7 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
|
|
|
8250
8575
|
return existsSync10(fileRelative);
|
|
8251
8576
|
}
|
|
8252
8577
|
}
|
|
8253
|
-
registerCheck(
|
|
8578
|
+
registerCheck(check21);
|
|
8254
8579
|
|
|
8255
8580
|
// src/lib/build-staging.ts
|
|
8256
8581
|
import path11 from "node:path";
|
|
@@ -8516,7 +8841,7 @@ async function createStageRoot() {
|
|
|
8516
8841
|
}
|
|
8517
8842
|
|
|
8518
8843
|
// src/checks/source/dangling-links.ts
|
|
8519
|
-
var
|
|
8844
|
+
var check22 = {
|
|
8520
8845
|
id: "source.links.dangling",
|
|
8521
8846
|
name: "Dangling Links",
|
|
8522
8847
|
description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
|
|
@@ -8524,10 +8849,10 @@ var check21 = {
|
|
|
8524
8849
|
phase: "pre-build",
|
|
8525
8850
|
async run(ctx) {
|
|
8526
8851
|
const results = [];
|
|
8527
|
-
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx,
|
|
8852
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check22.id, results)) {
|
|
8528
8853
|
if (kind !== "link" || isPrintResolvableHref(ref))
|
|
8529
8854
|
continue;
|
|
8530
|
-
results.push(finding(
|
|
8855
|
+
results.push(finding(check22.id, {
|
|
8531
8856
|
severity: "warning",
|
|
8532
8857
|
code: "dangling-link",
|
|
8533
8858
|
data: { ref },
|
|
@@ -8539,11 +8864,11 @@ var check21 = {
|
|
|
8539
8864
|
return results;
|
|
8540
8865
|
}
|
|
8541
8866
|
};
|
|
8542
|
-
registerCheck(
|
|
8867
|
+
registerCheck(check22);
|
|
8543
8868
|
|
|
8544
8869
|
// src/checks/source/accessibility-alt-text.ts
|
|
8545
8870
|
import { readFile as readFile19 } from "node:fs/promises";
|
|
8546
|
-
var
|
|
8871
|
+
var check23 = {
|
|
8547
8872
|
id: "source.accessibility.alt-text",
|
|
8548
8873
|
name: "Image Alt Text",
|
|
8549
8874
|
description: "Checks markdown images include non-empty alt text",
|
|
@@ -8572,7 +8897,7 @@ var check22 = {
|
|
|
8572
8897
|
if (alt.trim())
|
|
8573
8898
|
continue;
|
|
8574
8899
|
results.push({
|
|
8575
|
-
checkId:
|
|
8900
|
+
checkId: check23.id,
|
|
8576
8901
|
severity: "warning",
|
|
8577
8902
|
message: "Image is missing alt text",
|
|
8578
8903
|
file,
|
|
@@ -8597,11 +8922,11 @@ function extractImageAlts(line) {
|
|
|
8597
8922
|
}
|
|
8598
8923
|
return alts;
|
|
8599
8924
|
}
|
|
8600
|
-
registerCheck(
|
|
8925
|
+
registerCheck(check23);
|
|
8601
8926
|
|
|
8602
8927
|
// src/checks/source/accessibility-heading-order.ts
|
|
8603
8928
|
import { readFile as readFile20 } from "node:fs/promises";
|
|
8604
|
-
var
|
|
8929
|
+
var check24 = {
|
|
8605
8930
|
id: "source.accessibility.heading-order",
|
|
8606
8931
|
name: "Heading Order",
|
|
8607
8932
|
description: "Checks markdown heading levels do not jump by more than one",
|
|
@@ -8619,6 +8944,7 @@ var check23 = {
|
|
|
8619
8944
|
`);
|
|
8620
8945
|
let inFence = false;
|
|
8621
8946
|
let prevLevel;
|
|
8947
|
+
let silencedLine = -1;
|
|
8622
8948
|
for (let i = 0;i < lines.length; i++) {
|
|
8623
8949
|
const line = lines[i] ?? "";
|
|
8624
8950
|
if (/^\s*(```|~~~)/.test(line)) {
|
|
@@ -8627,13 +8953,18 @@ var check23 = {
|
|
|
8627
8953
|
}
|
|
8628
8954
|
if (inFence)
|
|
8629
8955
|
continue;
|
|
8956
|
+
const comment = /^\s*<!--(.*)-->\s*$/.exec(line);
|
|
8957
|
+
if (comment && parseDisableDirective(comment[1])?.includes(check24.id)) {
|
|
8958
|
+
silencedLine = i + 1;
|
|
8959
|
+
continue;
|
|
8960
|
+
}
|
|
8630
8961
|
const match = line.match(/^(#{1,6})\s+\S/);
|
|
8631
8962
|
if (!match)
|
|
8632
8963
|
continue;
|
|
8633
8964
|
const level = match[1].length;
|
|
8634
|
-
if (prevLevel != null && level > prevLevel + 1) {
|
|
8965
|
+
if (prevLevel != null && level > prevLevel + 1 && i !== silencedLine) {
|
|
8635
8966
|
results.push({
|
|
8636
|
-
checkId:
|
|
8967
|
+
checkId: check24.id,
|
|
8637
8968
|
severity: "warning",
|
|
8638
8969
|
message: `Heading level jump from h${prevLevel} to h${level}`,
|
|
8639
8970
|
file,
|
|
@@ -8647,11 +8978,11 @@ var check23 = {
|
|
|
8647
8978
|
return results;
|
|
8648
8979
|
}
|
|
8649
8980
|
};
|
|
8650
|
-
registerCheck(
|
|
8981
|
+
registerCheck(check24);
|
|
8651
8982
|
|
|
8652
8983
|
// src/checks/source/layout-markers.ts
|
|
8653
8984
|
import { readFile as readFile21 } from "node:fs/promises";
|
|
8654
|
-
var
|
|
8985
|
+
var check25 = {
|
|
8655
8986
|
id: "source.markdown.layout-markers",
|
|
8656
8987
|
name: "Layout Markers",
|
|
8657
8988
|
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
|
|
@@ -8663,7 +8994,7 @@ var check24 = {
|
|
|
8663
8994
|
return [];
|
|
8664
8995
|
const results = [];
|
|
8665
8996
|
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
8666
|
-
results.push(inspectionFailed(
|
|
8997
|
+
results.push(inspectionFailed(check25.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
|
|
8667
8998
|
});
|
|
8668
8999
|
const md = createMarkdownRenderer(plugins);
|
|
8669
9000
|
for (const file of files) {
|
|
@@ -8672,7 +9003,7 @@ var check24 = {
|
|
|
8672
9003
|
const env = {};
|
|
8673
9004
|
md.render(content, env);
|
|
8674
9005
|
for (const w of env.layoutWarnings ?? []) {
|
|
8675
|
-
results.push(finding(
|
|
9006
|
+
results.push(finding(check25.id, {
|
|
8676
9007
|
severity: w.severity ?? "warning",
|
|
8677
9008
|
message: w.message,
|
|
8678
9009
|
file,
|
|
@@ -8681,13 +9012,13 @@ var check24 = {
|
|
|
8681
9012
|
}));
|
|
8682
9013
|
}
|
|
8683
9014
|
} catch (error2) {
|
|
8684
|
-
results.push(inspectionFailed(
|
|
9015
|
+
results.push(inspectionFailed(check25.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
|
|
8685
9016
|
}
|
|
8686
9017
|
}
|
|
8687
9018
|
return results;
|
|
8688
9019
|
}
|
|
8689
9020
|
};
|
|
8690
|
-
registerCheck(
|
|
9021
|
+
registerCheck(check25);
|
|
8691
9022
|
|
|
8692
9023
|
// src/checks/source/merge-markers.ts
|
|
8693
9024
|
import { readFile as readFile22 } from "node:fs/promises";
|
|
@@ -8720,7 +9051,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
|
|
|
8720
9051
|
function withoutOnlineTag(relPath) {
|
|
8721
9052
|
return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
|
|
8722
9053
|
}
|
|
8723
|
-
var
|
|
9054
|
+
var check26 = {
|
|
8724
9055
|
id: "source.sync.merge-markers",
|
|
8725
9056
|
name: "Combined Versions",
|
|
8726
9057
|
description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
|
|
@@ -8734,7 +9065,7 @@ var check25 = {
|
|
|
8734
9065
|
try {
|
|
8735
9066
|
content = await readFile22(file, "utf8");
|
|
8736
9067
|
} catch {
|
|
8737
|
-
results.push(inspectionFailed(
|
|
9068
|
+
results.push(inspectionFailed(check26.id, `Could not read source file: ${file}`, { file }));
|
|
8738
9069
|
continue;
|
|
8739
9070
|
}
|
|
8740
9071
|
if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
|
|
@@ -8746,7 +9077,7 @@ var check25 = {
|
|
|
8746
9077
|
const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
|
|
8747
9078
|
if (line === OPEN_SENTINEL) {
|
|
8748
9079
|
inBlock = true;
|
|
8749
|
-
results.push(finding(
|
|
9080
|
+
results.push(finding(check26.id, {
|
|
8750
9081
|
severity: "error",
|
|
8751
9082
|
code: "two-versions-passage",
|
|
8752
9083
|
message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
|
|
@@ -8755,7 +9086,7 @@ var check25 = {
|
|
|
8755
9086
|
}));
|
|
8756
9087
|
} else if (line === CLOSE_SENTINEL) {
|
|
8757
9088
|
if (!inBlock) {
|
|
8758
|
-
results.push(finding(
|
|
9089
|
+
results.push(finding(check26.id, {
|
|
8759
9090
|
severity: "error",
|
|
8760
9091
|
code: "leftover-version-marker",
|
|
8761
9092
|
message: "This marker line is left over from combining two versions — delete it.",
|
|
@@ -8776,7 +9107,7 @@ var check25 = {
|
|
|
8776
9107
|
});
|
|
8777
9108
|
for (const sibling of siblings.sort()) {
|
|
8778
9109
|
const rel = path12.relative(ctx.inputDir, sibling).split(path12.sep).join("/");
|
|
8779
|
-
results.push(finding(
|
|
9110
|
+
results.push(finding(check26.id, {
|
|
8780
9111
|
severity: "warning",
|
|
8781
9112
|
code: "kept-both-versions",
|
|
8782
9113
|
message: `Two versions of ${withoutOnlineTag(rel)} are in your book — keep the one you want, then delete the other.`,
|
|
@@ -8784,12 +9115,12 @@ var check25 = {
|
|
|
8784
9115
|
}));
|
|
8785
9116
|
}
|
|
8786
9117
|
} catch (error2) {
|
|
8787
|
-
results.push(inspectionFailed(
|
|
9118
|
+
results.push(inspectionFailed(check26.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
8788
9119
|
}
|
|
8789
9120
|
return results;
|
|
8790
9121
|
}
|
|
8791
9122
|
};
|
|
8792
|
-
registerCheck(
|
|
9123
|
+
registerCheck(check26);
|
|
8793
9124
|
|
|
8794
9125
|
// src/checks/asset/image-file-size.ts
|
|
8795
9126
|
import { stat as stat4 } from "node:fs/promises";
|
|
@@ -9015,7 +9346,7 @@ async function collectImageFiles(dirs, exts, ignore2 = ASSET_SCAN_IGNORE_GLOBS)
|
|
|
9015
9346
|
}
|
|
9016
9347
|
|
|
9017
9348
|
// src/checks/asset/image-file-size.ts
|
|
9018
|
-
var
|
|
9349
|
+
var check27 = {
|
|
9019
9350
|
id: "asset.image.file-size",
|
|
9020
9351
|
name: "Image File Size",
|
|
9021
9352
|
description: "Checks that image files do not exceed the maximum size limit",
|
|
@@ -9036,14 +9367,14 @@ var check26 = {
|
|
|
9036
9367
|
if (info2.size > maxSize) {
|
|
9037
9368
|
const sizeMb = (info2.size / 1e6).toFixed(1);
|
|
9038
9369
|
const maxMb = (maxSize / 1e6).toFixed(1);
|
|
9039
|
-
results.push(finding(
|
|
9370
|
+
results.push(finding(check27.id, {
|
|
9040
9371
|
severity: "warning",
|
|
9041
9372
|
message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
|
|
9042
9373
|
file
|
|
9043
9374
|
}));
|
|
9044
9375
|
}
|
|
9045
9376
|
} catch {
|
|
9046
|
-
results.push(inspectionFailed(
|
|
9377
|
+
results.push(inspectionFailed(check27.id, `Could not stat image file: ${file}`, {
|
|
9047
9378
|
file
|
|
9048
9379
|
}));
|
|
9049
9380
|
}
|
|
@@ -9051,10 +9382,10 @@ var check26 = {
|
|
|
9051
9382
|
return results;
|
|
9052
9383
|
}
|
|
9053
9384
|
};
|
|
9054
|
-
registerCheck(
|
|
9385
|
+
registerCheck(check27);
|
|
9055
9386
|
|
|
9056
9387
|
// src/checks/asset/image-resolution.ts
|
|
9057
|
-
var
|
|
9388
|
+
var check28 = {
|
|
9058
9389
|
id: "asset.image.resolution",
|
|
9059
9390
|
name: "Image Resolution",
|
|
9060
9391
|
description: "Checks source image DPI from embedded density metadata",
|
|
@@ -9076,7 +9407,7 @@ var check27 = {
|
|
|
9076
9407
|
const { xDpi, yDpi } = info2;
|
|
9077
9408
|
if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
|
|
9078
9409
|
results.push({
|
|
9079
|
-
checkId:
|
|
9410
|
+
checkId: check28.id,
|
|
9080
9411
|
severity: "warning",
|
|
9081
9412
|
message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
|
|
9082
9413
|
file
|
|
@@ -9086,7 +9417,7 @@ var check27 = {
|
|
|
9086
9417
|
return results;
|
|
9087
9418
|
}
|
|
9088
9419
|
};
|
|
9089
|
-
registerCheck(
|
|
9420
|
+
registerCheck(check28);
|
|
9090
9421
|
|
|
9091
9422
|
// src/checks/asset/image-color-space.ts
|
|
9092
9423
|
var LABEL = {
|
|
@@ -9094,7 +9425,7 @@ var LABEL = {
|
|
|
9094
9425
|
gray: "Gray",
|
|
9095
9426
|
cmyk: "CMYK"
|
|
9096
9427
|
};
|
|
9097
|
-
var
|
|
9428
|
+
var check29 = {
|
|
9098
9429
|
id: "asset.image.color-space",
|
|
9099
9430
|
name: "Image Color Space",
|
|
9100
9431
|
description: "Validates image color spaces against allowed list",
|
|
@@ -9115,7 +9446,7 @@ var check28 = {
|
|
|
9115
9446
|
const cs = info2?.colorSpace;
|
|
9116
9447
|
if (cs && !allowedLower.has(cs)) {
|
|
9117
9448
|
results.push({
|
|
9118
|
-
checkId:
|
|
9449
|
+
checkId: check29.id,
|
|
9119
9450
|
severity: "warning",
|
|
9120
9451
|
message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
|
|
9121
9452
|
file
|
|
@@ -9125,10 +9456,10 @@ var check28 = {
|
|
|
9125
9456
|
return results;
|
|
9126
9457
|
}
|
|
9127
9458
|
};
|
|
9128
|
-
registerCheck(
|
|
9459
|
+
registerCheck(check29);
|
|
9129
9460
|
|
|
9130
9461
|
// src/checks/asset/image-alpha.ts
|
|
9131
|
-
var
|
|
9462
|
+
var check30 = {
|
|
9132
9463
|
id: "asset.image.alpha-channel",
|
|
9133
9464
|
name: "Image Alpha Channel",
|
|
9134
9465
|
description: "Checks for alpha channels in PNG/TIFF images",
|
|
@@ -9146,7 +9477,7 @@ var check29 = {
|
|
|
9146
9477
|
const info2 = await inspectImage(file);
|
|
9147
9478
|
if (info2?.hasAlpha) {
|
|
9148
9479
|
results.push({
|
|
9149
|
-
checkId:
|
|
9480
|
+
checkId: check30.id,
|
|
9150
9481
|
severity: "warning",
|
|
9151
9482
|
message: "Image contains alpha channel, which may cause print issues",
|
|
9152
9483
|
file
|
|
@@ -9156,10 +9487,10 @@ var check29 = {
|
|
|
9156
9487
|
return results;
|
|
9157
9488
|
}
|
|
9158
9489
|
};
|
|
9159
|
-
registerCheck(
|
|
9490
|
+
registerCheck(check30);
|
|
9160
9491
|
|
|
9161
9492
|
// src/checks/asset/image-tac.ts
|
|
9162
|
-
var
|
|
9493
|
+
var check31 = {
|
|
9163
9494
|
id: "asset.image.tac-raster",
|
|
9164
9495
|
name: "Image TAC (Raster)",
|
|
9165
9496
|
description: "Rasterizes and checks TAC per image using Ghostscript",
|
|
@@ -9178,7 +9509,7 @@ var check30 = {
|
|
|
9178
9509
|
const ghostscript = await resolveGhostscript();
|
|
9179
9510
|
if (!ghostscript) {
|
|
9180
9511
|
return [
|
|
9181
|
-
inspectionFailed(
|
|
9512
|
+
inspectionFailed(check31.id, "Could not inspect image ink coverage: Ghostscript executable not found")
|
|
9182
9513
|
];
|
|
9183
9514
|
}
|
|
9184
9515
|
for (const file of files) {
|
|
@@ -9196,7 +9527,7 @@ var check30 = {
|
|
|
9196
9527
|
if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
|
|
9197
9528
|
const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
|
|
9198
9529
|
if (tac > maxTac) {
|
|
9199
|
-
results.push(finding(
|
|
9530
|
+
results.push(finding(check31.id, {
|
|
9200
9531
|
severity: "warning",
|
|
9201
9532
|
message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
|
|
9202
9533
|
file,
|
|
@@ -9208,16 +9539,16 @@ var check30 = {
|
|
|
9208
9539
|
}
|
|
9209
9540
|
}
|
|
9210
9541
|
} catch {
|
|
9211
|
-
results.push(inspectionFailed(
|
|
9542
|
+
results.push(inspectionFailed(check31.id, `Could not inspect image ink coverage: ${file}`, { file }));
|
|
9212
9543
|
}
|
|
9213
9544
|
}
|
|
9214
9545
|
return results;
|
|
9215
9546
|
}
|
|
9216
9547
|
};
|
|
9217
|
-
registerCheck(
|
|
9548
|
+
registerCheck(check31);
|
|
9218
9549
|
|
|
9219
9550
|
// src/checks/asset/approved-fonts.ts
|
|
9220
|
-
var
|
|
9551
|
+
var check32 = {
|
|
9221
9552
|
id: "asset.font.approved-files",
|
|
9222
9553
|
name: "Approved Font Files",
|
|
9223
9554
|
description: "Checks font files against the approved file patterns",
|
|
@@ -9256,7 +9587,7 @@ var check31 = {
|
|
|
9256
9587
|
for (const font of allFonts) {
|
|
9257
9588
|
if (!approvedFonts.has(font)) {
|
|
9258
9589
|
results.push({
|
|
9259
|
-
checkId:
|
|
9590
|
+
checkId: check32.id,
|
|
9260
9591
|
severity: "warning",
|
|
9261
9592
|
message: "Font file not in approved list",
|
|
9262
9593
|
file: font
|
|
@@ -9266,7 +9597,7 @@ var check31 = {
|
|
|
9266
9597
|
return results;
|
|
9267
9598
|
}
|
|
9268
9599
|
};
|
|
9269
|
-
registerCheck(
|
|
9600
|
+
registerCheck(check32);
|
|
9270
9601
|
|
|
9271
9602
|
// src/checks/asset/font-license.ts
|
|
9272
9603
|
import { existsSync as existsSync11 } from "node:fs";
|
|
@@ -9281,7 +9612,7 @@ var LICENSE_NAMES = [
|
|
|
9281
9612
|
"OFL-1.1.txt",
|
|
9282
9613
|
"COPYING"
|
|
9283
9614
|
];
|
|
9284
|
-
var
|
|
9615
|
+
var check33 = {
|
|
9285
9616
|
id: "asset.font.license",
|
|
9286
9617
|
name: "Font License",
|
|
9287
9618
|
description: "Checks for license files in font directories",
|
|
@@ -9308,7 +9639,7 @@ var check32 = {
|
|
|
9308
9639
|
const hasLicense = LICENSE_NAMES.some((name) => existsSync11(resolve7(fontDir, name)));
|
|
9309
9640
|
if (!hasLicense) {
|
|
9310
9641
|
results.push({
|
|
9311
|
-
checkId:
|
|
9642
|
+
checkId: check33.id,
|
|
9312
9643
|
severity: "warning",
|
|
9313
9644
|
message: `No font license file found in directory`,
|
|
9314
9645
|
file: fontDir
|
|
@@ -9318,10 +9649,10 @@ var check32 = {
|
|
|
9318
9649
|
return results;
|
|
9319
9650
|
}
|
|
9320
9651
|
};
|
|
9321
|
-
registerCheck(
|
|
9652
|
+
registerCheck(check33);
|
|
9322
9653
|
|
|
9323
9654
|
// src/checks/heuristic/text-density.ts
|
|
9324
|
-
var
|
|
9655
|
+
var check34 = {
|
|
9325
9656
|
id: "heuristic.whitespace.text-density",
|
|
9326
9657
|
name: "Text Density",
|
|
9327
9658
|
description: "Checks characters-per-page ratio",
|
|
@@ -9350,7 +9681,7 @@ var check33 = {
|
|
|
9350
9681
|
});
|
|
9351
9682
|
if (lowPages.length > 0) {
|
|
9352
9683
|
results.push({
|
|
9353
|
-
checkId:
|
|
9684
|
+
checkId: check34.id,
|
|
9354
9685
|
severity: "info",
|
|
9355
9686
|
message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
|
|
9356
9687
|
file: ctx.pdfPath
|
|
@@ -9358,7 +9689,7 @@ var check33 = {
|
|
|
9358
9689
|
}
|
|
9359
9690
|
if (highPages.length > 0) {
|
|
9360
9691
|
results.push({
|
|
9361
|
-
checkId:
|
|
9692
|
+
checkId: check34.id,
|
|
9362
9693
|
severity: "info",
|
|
9363
9694
|
message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
|
|
9364
9695
|
file: ctx.pdfPath
|
|
@@ -9367,11 +9698,11 @@ var check33 = {
|
|
|
9367
9698
|
return results;
|
|
9368
9699
|
}
|
|
9369
9700
|
};
|
|
9370
|
-
registerCheck(
|
|
9701
|
+
registerCheck(check34);
|
|
9371
9702
|
|
|
9372
9703
|
// src/checks/heuristic/section-density.ts
|
|
9373
9704
|
import { readFile as readFile24 } from "node:fs/promises";
|
|
9374
|
-
var
|
|
9705
|
+
var check35 = {
|
|
9375
9706
|
id: "heuristic.chunking.section-density",
|
|
9376
9707
|
name: "Section Density",
|
|
9377
9708
|
description: "Checks heading/paragraph/callout density from source Markdown",
|
|
@@ -9397,7 +9728,7 @@ var check34 = {
|
|
|
9397
9728
|
const line = lines[i];
|
|
9398
9729
|
if (/^#{1,6}\s/.test(line)) {
|
|
9399
9730
|
if (paragraphCount > maxParas) {
|
|
9400
|
-
results.push(finding(
|
|
9731
|
+
results.push(finding(check35.id, {
|
|
9401
9732
|
severity: "info",
|
|
9402
9733
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
9403
9734
|
file,
|
|
@@ -9417,7 +9748,7 @@ var check34 = {
|
|
|
9417
9748
|
}
|
|
9418
9749
|
}
|
|
9419
9750
|
if (paragraphCount > maxParas) {
|
|
9420
|
-
results.push(finding(
|
|
9751
|
+
results.push(finding(check35.id, {
|
|
9421
9752
|
severity: "info",
|
|
9422
9753
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
9423
9754
|
file,
|
|
@@ -9425,7 +9756,7 @@ var check34 = {
|
|
|
9425
9756
|
}));
|
|
9426
9757
|
}
|
|
9427
9758
|
} catch {
|
|
9428
|
-
results.push(inspectionFailed(
|
|
9759
|
+
results.push(inspectionFailed(check35.id, `Could not read source file: ${file}`, {
|
|
9429
9760
|
file
|
|
9430
9761
|
}));
|
|
9431
9762
|
}
|
|
@@ -9433,10 +9764,10 @@ var check34 = {
|
|
|
9433
9764
|
return results;
|
|
9434
9765
|
}
|
|
9435
9766
|
};
|
|
9436
|
-
registerCheck(
|
|
9767
|
+
registerCheck(check35);
|
|
9437
9768
|
|
|
9438
9769
|
// src/checks/heuristic/layer-count.ts
|
|
9439
|
-
var
|
|
9770
|
+
var check36 = {
|
|
9440
9771
|
id: "heuristic.decoration.layer-count",
|
|
9441
9772
|
name: "Layer Count",
|
|
9442
9773
|
description: "Counts image objects per page",
|
|
@@ -9461,7 +9792,7 @@ var check35 = {
|
|
|
9461
9792
|
heavyPages.sort((a, b) => a - b);
|
|
9462
9793
|
return [
|
|
9463
9794
|
{
|
|
9464
|
-
checkId:
|
|
9795
|
+
checkId: check36.id,
|
|
9465
9796
|
severity: "info",
|
|
9466
9797
|
message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
|
|
9467
9798
|
file: ctx.pdfPath
|
|
@@ -9471,10 +9802,10 @@ var check35 = {
|
|
|
9471
9802
|
return [];
|
|
9472
9803
|
}
|
|
9473
9804
|
};
|
|
9474
|
-
registerCheck(
|
|
9805
|
+
registerCheck(check36);
|
|
9475
9806
|
|
|
9476
9807
|
// src/checks/heuristic/placement-variance.ts
|
|
9477
|
-
var
|
|
9808
|
+
var check37 = {
|
|
9478
9809
|
id: "heuristic.layout.placement-variance",
|
|
9479
9810
|
name: "Placement Variance",
|
|
9480
9811
|
description: "Analyzes text baseline coordinates for layout consistency",
|
|
@@ -9492,7 +9823,7 @@ var check36 = {
|
|
|
9492
9823
|
const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
|
|
9493
9824
|
return [
|
|
9494
9825
|
{
|
|
9495
|
-
checkId:
|
|
9826
|
+
checkId: check37.id,
|
|
9496
9827
|
severity: "info",
|
|
9497
9828
|
message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
|
|
9498
9829
|
file: ctx.pdfPath
|
|
@@ -9500,7 +9831,7 @@ var check36 = {
|
|
|
9500
9831
|
];
|
|
9501
9832
|
}
|
|
9502
9833
|
};
|
|
9503
|
-
registerCheck(
|
|
9834
|
+
registerCheck(check37);
|
|
9504
9835
|
|
|
9505
9836
|
// src/checks/runner.ts
|
|
9506
9837
|
function getCheckSeverityOverride(checkId, config) {
|
|
@@ -9530,25 +9861,32 @@ async function runChecks(ctx, opts = {}) {
|
|
|
9530
9861
|
}
|
|
9531
9862
|
const release = retainPdfCache();
|
|
9532
9863
|
try {
|
|
9533
|
-
for (const
|
|
9864
|
+
for (const check38 of checks2) {
|
|
9534
9865
|
try {
|
|
9535
|
-
|
|
9536
|
-
const severityOverride = getCheckSeverityOverride(
|
|
9866
|
+
let results = await check38.run(ctx);
|
|
9867
|
+
const severityOverride = getCheckSeverityOverride(check38.id, ctx.config);
|
|
9537
9868
|
if (severityOverride) {
|
|
9538
9869
|
for (const r of results) {
|
|
9539
9870
|
r.severity = severityOverride;
|
|
9540
9871
|
}
|
|
9541
9872
|
}
|
|
9873
|
+
const lintLevel = ruleLevel(ctx.config.lint.rules, check38.id);
|
|
9874
|
+
if (lintLevel === "off")
|
|
9875
|
+
results = [];
|
|
9876
|
+
else if (lintLevel) {
|
|
9877
|
+
for (const r of results)
|
|
9878
|
+
r.severity = lintLevel === "warn" ? "warning" : "error";
|
|
9879
|
+
}
|
|
9542
9880
|
if (results.length === 0) {
|
|
9543
|
-
passed.push(
|
|
9881
|
+
passed.push(check38.id);
|
|
9544
9882
|
} else {
|
|
9545
9883
|
allResults.push(...results);
|
|
9546
9884
|
}
|
|
9547
9885
|
} catch (err) {
|
|
9548
9886
|
allResults.push({
|
|
9549
|
-
checkId:
|
|
9887
|
+
checkId: check38.id,
|
|
9550
9888
|
severity: "error",
|
|
9551
|
-
message: `Check "${
|
|
9889
|
+
message: `Check "${check38.id}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
9552
9890
|
});
|
|
9553
9891
|
}
|
|
9554
9892
|
}
|
|
@@ -9588,15 +9926,15 @@ function emptyReport() {
|
|
|
9588
9926
|
async function checkToolAvailability(config, opts = {}) {
|
|
9589
9927
|
const { checks: checks2 } = selectChecks(opts, config);
|
|
9590
9928
|
const toolToChecks = new Map;
|
|
9591
|
-
for (const
|
|
9592
|
-
if (!
|
|
9929
|
+
for (const check38 of checks2) {
|
|
9930
|
+
if (!check38.requiredTools?.length)
|
|
9593
9931
|
continue;
|
|
9594
|
-
for (const tool of
|
|
9932
|
+
for (const tool of check38.requiredTools) {
|
|
9595
9933
|
const existing = toolToChecks.get(tool);
|
|
9596
9934
|
if (existing) {
|
|
9597
|
-
existing.push(
|
|
9935
|
+
existing.push(check38.id);
|
|
9598
9936
|
} else {
|
|
9599
|
-
toolToChecks.set(tool, [
|
|
9937
|
+
toolToChecks.set(tool, [check38.id]);
|
|
9600
9938
|
}
|
|
9601
9939
|
}
|
|
9602
9940
|
}
|
|
@@ -9612,11 +9950,11 @@ async function checkToolAvailability(config, opts = {}) {
|
|
|
9612
9950
|
const missing = results.filter((r) => !r.found).map((r) => r.tool);
|
|
9613
9951
|
const missingSet = new Set(missing);
|
|
9614
9952
|
const skippedChecks = [];
|
|
9615
|
-
for (const
|
|
9616
|
-
if (!
|
|
9953
|
+
for (const check38 of checks2) {
|
|
9954
|
+
if (!check38.requiredTools?.length)
|
|
9617
9955
|
continue;
|
|
9618
|
-
if (
|
|
9619
|
-
skippedChecks.push(
|
|
9956
|
+
if (check38.requiredTools.some((t) => missingSet.has(t))) {
|
|
9957
|
+
skippedChecks.push(check38.id);
|
|
9620
9958
|
}
|
|
9621
9959
|
}
|
|
9622
9960
|
return { available, missing, skippedChecks, toolToChecks };
|
|
@@ -9889,7 +10227,7 @@ async function executeValidation(args) {
|
|
|
9889
10227
|
};
|
|
9890
10228
|
if (args.fix) {
|
|
9891
10229
|
const selected = selectChecks(runnerOptions, config).checks;
|
|
9892
|
-
if (selected.some((
|
|
10230
|
+
if (selected.some((check38) => check38.id === MARKDOWNLINT_CHECK_ID)) {
|
|
9893
10231
|
await applyMarkdownlintFixes(context);
|
|
9894
10232
|
} else {
|
|
9895
10233
|
log.warn(`--fix: ${MARKDOWNLINT_CHECK_ID} is not part of this run — no files were fixed.`);
|
|
@@ -10241,7 +10579,8 @@ class HtmlOutput {
|
|
|
10241
10579
|
requestedFlavor: null,
|
|
10242
10580
|
resolvedFlavor: config.pdfx.flavor,
|
|
10243
10581
|
iccPath: null,
|
|
10244
|
-
stripAnnotations: null
|
|
10582
|
+
stripAnnotations: null,
|
|
10583
|
+
blackText: null
|
|
10245
10584
|
}
|
|
10246
10585
|
}, `Wrote: ${path13.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
|
|
10247
10586
|
}
|
|
@@ -10261,12 +10600,12 @@ class PdfOutput {
|
|
|
10261
10600
|
const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
|
|
10262
10601
|
await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
|
|
10263
10602
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
10264
|
-
const { buildNativePdf } = await import("./engine-
|
|
10603
|
+
const { buildNativePdf } = await import("./engine-3xqgm82d.js");
|
|
10265
10604
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
10266
10605
|
title: config.title,
|
|
10267
10606
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
10268
10607
|
signature: config.print.signature,
|
|
10269
|
-
|
|
10608
|
+
lastPage: lastPageMode(config.print),
|
|
10270
10609
|
allowShrink: opts.allowShrink
|
|
10271
10610
|
}, browser);
|
|
10272
10611
|
for (const d of engineDiagnostics)
|
|
@@ -10325,7 +10664,8 @@ class PdfOutput {
|
|
|
10325
10664
|
requestedFlavor: pdfxMode ?? null,
|
|
10326
10665
|
resolvedFlavor: config.pdfx.flavor,
|
|
10327
10666
|
iccPath: effectiveIccPath,
|
|
10328
|
-
stripAnnotations: shouldStripAnnotations
|
|
10667
|
+
stripAnnotations: shouldStripAnnotations,
|
|
10668
|
+
blackText: pdfxMode ? config.pdfx.blackText : null
|
|
10329
10669
|
}
|
|
10330
10670
|
}, `Wrote: ${reportedPdf}`, { htmlPath: htmlFile, pdfPath: reportedPdf }, pdfName, engineDiagnostics);
|
|
10331
10671
|
} finally {
|
|
@@ -11726,9 +12066,169 @@ async function getSystemDiagnostics() {
|
|
|
11726
12066
|
};
|
|
11727
12067
|
}
|
|
11728
12068
|
// src/lib/project-scaffold.ts
|
|
11729
|
-
import { access, copyFile, cp as cp2, mkdir as
|
|
11730
|
-
import { constants as FS, existsSync as
|
|
12069
|
+
import { access, copyFile, cp as cp2, mkdir as mkdir9, readdir as readdir6, readFile as readFile29, stat as stat6, writeFile as writeFile9 } from "node:fs/promises";
|
|
12070
|
+
import { constants as FS, existsSync as existsSync14 } from "node:fs";
|
|
12071
|
+
import path20 from "node:path";
|
|
12072
|
+
|
|
12073
|
+
// src/lib/page-size.ts
|
|
12074
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
12075
|
+
import { mkdir as mkdir8, readFile as readFile28, writeFile as writeFile8 } from "node:fs/promises";
|
|
11731
12076
|
import path19 from "node:path";
|
|
12077
|
+
import postcss4 from "postcss";
|
|
12078
|
+
import { Scalar as Scalar2 } from "yaml";
|
|
12079
|
+
function isPositivePoints(value) {
|
|
12080
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
12081
|
+
}
|
|
12082
|
+
function writePageBounds(doc, page) {
|
|
12083
|
+
const out = { width: page.width, height: page.height };
|
|
12084
|
+
const tolerance = page.tolerance ?? numberAt(doc, ["page", "tolerance"]);
|
|
12085
|
+
if (tolerance !== undefined)
|
|
12086
|
+
out.tolerance = tolerance;
|
|
12087
|
+
doc.set("page", doc.createNode(out));
|
|
12088
|
+
}
|
|
12089
|
+
function numberAt(doc, keyPath) {
|
|
12090
|
+
const v = doc.getIn(keyPath);
|
|
12091
|
+
return isPositivePoints(v) ? v : undefined;
|
|
12092
|
+
}
|
|
12093
|
+
function pageSizeForChoice(choice) {
|
|
12094
|
+
if (!PRESET_IDS.includes(choice.preset)) {
|
|
12095
|
+
throw new UsageError(`Unknown preset "${choice.preset}". Choose one of: ${PRESET_IDS.join(", ")}.`);
|
|
12096
|
+
}
|
|
12097
|
+
if (choice.preset === "custom") {
|
|
12098
|
+
const { page } = choice;
|
|
12099
|
+
if (!page || !isPositivePoints(page.width) || !isPositivePoints(page.height)) {
|
|
12100
|
+
throw new UsageError("A custom page size needs a positive width and height in points (72pt = 1in).");
|
|
12101
|
+
}
|
|
12102
|
+
return { width: page.width, height: page.height };
|
|
12103
|
+
}
|
|
12104
|
+
const base = PRESETS[choice.preset].page;
|
|
12105
|
+
return { width: base.width, height: base.height };
|
|
12106
|
+
}
|
|
12107
|
+
var round3 = (n) => Math.round(n * 1000) / 1000;
|
|
12108
|
+
function cssLength(pt) {
|
|
12109
|
+
const inches = round3(pt / 72);
|
|
12110
|
+
return Math.abs(inches * 72 - pt) < 0.000001 ? `${inches}in` : `${round3(pt)}pt`;
|
|
12111
|
+
}
|
|
12112
|
+
function formatPageSize(page) {
|
|
12113
|
+
return `${cssLength(page.width)} ${cssLength(page.height)}`;
|
|
12114
|
+
}
|
|
12115
|
+
var SAME_SIZE_PT = 0.5;
|
|
12116
|
+
function sameSize(a, b) {
|
|
12117
|
+
return Math.abs(a.width - b.width) < SAME_SIZE_PT && Math.abs(a.height - b.height) < SAME_SIZE_PT;
|
|
12118
|
+
}
|
|
12119
|
+
function plainPageRules(root) {
|
|
12120
|
+
const rules = [];
|
|
12121
|
+
root.walkAtRules("page", (rule) => {
|
|
12122
|
+
if (rule.params.trim() === "")
|
|
12123
|
+
rules.push(rule);
|
|
12124
|
+
});
|
|
12125
|
+
return rules;
|
|
12126
|
+
}
|
|
12127
|
+
function sizeDeclsOf(rule) {
|
|
12128
|
+
const decls = [];
|
|
12129
|
+
rule.each((node) => {
|
|
12130
|
+
if (node.type === "decl" && node.prop.toLowerCase() === "size")
|
|
12131
|
+
decls.push(node);
|
|
12132
|
+
});
|
|
12133
|
+
return decls;
|
|
12134
|
+
}
|
|
12135
|
+
function readPageSizeFromCss(css) {
|
|
12136
|
+
let root;
|
|
12137
|
+
try {
|
|
12138
|
+
root = postcss4.parse(css);
|
|
12139
|
+
} catch {
|
|
12140
|
+
return null;
|
|
12141
|
+
}
|
|
12142
|
+
const decls = plainPageRules(root).flatMap(sizeDeclsOf);
|
|
12143
|
+
const last = decls[decls.length - 1];
|
|
12144
|
+
return last ? parseSize(last.value) ?? null : null;
|
|
12145
|
+
}
|
|
12146
|
+
function setPageSizeInCss(css, page) {
|
|
12147
|
+
const root = postcss4.parse(css);
|
|
12148
|
+
const decls = plainPageRules(root).flatMap(sizeDeclsOf);
|
|
12149
|
+
const last = decls[decls.length - 1];
|
|
12150
|
+
const value = formatPageSize(page);
|
|
12151
|
+
if (last) {
|
|
12152
|
+
const current = parseSize(last.value);
|
|
12153
|
+
if (current && sameSize(current, page))
|
|
12154
|
+
return css;
|
|
12155
|
+
last.value = value;
|
|
12156
|
+
return root.toString();
|
|
12157
|
+
}
|
|
12158
|
+
const rule = `@page {
|
|
12159
|
+
size: ${value};
|
|
12160
|
+
}
|
|
12161
|
+
`;
|
|
12162
|
+
if (css.trim() === "")
|
|
12163
|
+
return css + rule;
|
|
12164
|
+
return `${css}${css.endsWith(`
|
|
12165
|
+
`) ? "" : `
|
|
12166
|
+
`}
|
|
12167
|
+
${rule}`;
|
|
12168
|
+
}
|
|
12169
|
+
async function ownStylesheets(projectDir) {
|
|
12170
|
+
const rels = await resolveActiveStyles(projectDir).catch(() => []);
|
|
12171
|
+
return rels.map((rel) => path19.resolve(projectDir, rel)).filter((abs) => {
|
|
12172
|
+
const inside = path19.relative(projectDir, abs);
|
|
12173
|
+
return !inside.startsWith("..") && !path19.isAbsolute(inside) && existsSync13(abs);
|
|
12174
|
+
});
|
|
12175
|
+
}
|
|
12176
|
+
var relDisplay2 = (projectDir, abs) => path19.relative(projectDir, abs).split(path19.sep).join("/");
|
|
12177
|
+
async function findCssSize(projectDir, sheets) {
|
|
12178
|
+
let found = null;
|
|
12179
|
+
for (const file of sheets) {
|
|
12180
|
+
const size = readPageSizeFromCss(await readFile28(file, "utf8"));
|
|
12181
|
+
if (size)
|
|
12182
|
+
found = { file, size };
|
|
12183
|
+
}
|
|
12184
|
+
return found;
|
|
12185
|
+
}
|
|
12186
|
+
async function readPageSetup(projectDir) {
|
|
12187
|
+
const { doc } = await loadManifestDoc(projectDir);
|
|
12188
|
+
const declared = doc.get("preset");
|
|
12189
|
+
const preset = declared === undefined || declared === null ? "dtrpg" : PRESET_IDS.includes(String(declared)) ? declared : null;
|
|
12190
|
+
const base = preset ? PRESETS[preset].page : null;
|
|
12191
|
+
const width = numberAt(doc, ["page", "width"]) ?? base?.width;
|
|
12192
|
+
const height = numberAt(doc, ["page", "height"]) ?? base?.height;
|
|
12193
|
+
const tolerance = numberAt(doc, ["page", "tolerance"]) ?? base?.tolerance ?? 0.5;
|
|
12194
|
+
const found = await findCssSize(projectDir, await ownStylesheets(projectDir));
|
|
12195
|
+
return {
|
|
12196
|
+
preset,
|
|
12197
|
+
bounds: width !== undefined && height !== undefined ? { width, height, tolerance } : null,
|
|
12198
|
+
css: found ? { ...found.size, file: relDisplay2(projectDir, found.file) } : null
|
|
12199
|
+
};
|
|
12200
|
+
}
|
|
12201
|
+
async function setPageSetup(projectDir, choice) {
|
|
12202
|
+
const size = pageSizeForChoice(choice);
|
|
12203
|
+
const sheets = await ownStylesheets(projectDir);
|
|
12204
|
+
const found = await findCssSize(projectDir, sheets);
|
|
12205
|
+
const newSheet = path19.join(projectDir, "styles", "book.css");
|
|
12206
|
+
const target = found?.file ?? sheets[sheets.length - 1] ?? newSheet;
|
|
12207
|
+
const before = existsSync13(target) ? await readFile28(target, "utf8") : "";
|
|
12208
|
+
let after;
|
|
12209
|
+
try {
|
|
12210
|
+
after = setPageSizeInCss(before, size);
|
|
12211
|
+
} catch (e) {
|
|
12212
|
+
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.`);
|
|
12213
|
+
}
|
|
12214
|
+
const { doc, file } = await loadManifestDoc(projectDir);
|
|
12215
|
+
doc.set("preset", choice.preset);
|
|
12216
|
+
if (choice.preset === "custom")
|
|
12217
|
+
writePageBounds(doc, size);
|
|
12218
|
+
else
|
|
12219
|
+
doc.delete("page");
|
|
12220
|
+
if (target === newSheet && !sheets.includes(newSheet)) {
|
|
12221
|
+
ensureSeq(doc, "styles").add(new Scalar2("styles/book.css"));
|
|
12222
|
+
}
|
|
12223
|
+
await writeManifestDoc(file, doc);
|
|
12224
|
+
if (after !== before || !existsSync13(target)) {
|
|
12225
|
+
await mkdir8(path19.dirname(target), { recursive: true });
|
|
12226
|
+
await writeFile8(target, after, "utf8");
|
|
12227
|
+
}
|
|
12228
|
+
return readPageSetup(projectDir);
|
|
12229
|
+
}
|
|
12230
|
+
|
|
12231
|
+
// src/lib/project-scaffold.ts
|
|
11732
12232
|
function starterOverridesCss() {
|
|
11733
12233
|
return `/**
|
|
11734
12234
|
* Your book's own styles.
|
|
@@ -11757,9 +12257,6 @@ function escapeYamlScalar(value) {
|
|
|
11757
12257
|
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
11758
12258
|
}
|
|
11759
12259
|
var DEFAULT_AUTHOR = "Anonymous";
|
|
11760
|
-
function isPositivePoints(value) {
|
|
11761
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11762
|
-
}
|
|
11763
12260
|
function requirePreset(preset, customPage) {
|
|
11764
12261
|
if (!preset || !PRESET_IDS.includes(preset)) {
|
|
11765
12262
|
const got = preset ? ` (got "${preset}")` : "";
|
|
@@ -11801,7 +12298,7 @@ async function scaffoldProject(options) {
|
|
|
11801
12298
|
throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
|
|
11802
12299
|
}
|
|
11803
12300
|
const parentDir = options.parentDir;
|
|
11804
|
-
if (!parentDir || !
|
|
12301
|
+
if (!parentDir || !path20.isAbsolute(parentDir)) {
|
|
11805
12302
|
throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
|
|
11806
12303
|
}
|
|
11807
12304
|
try {
|
|
@@ -11809,7 +12306,7 @@ async function scaffoldProject(options) {
|
|
|
11809
12306
|
} catch {
|
|
11810
12307
|
throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
11811
12308
|
}
|
|
11812
|
-
const projectDir =
|
|
12309
|
+
const projectDir = path20.join(parentDir, folderName);
|
|
11813
12310
|
let targetExists = true;
|
|
11814
12311
|
try {
|
|
11815
12312
|
await access(projectDir, FS.F_OK);
|
|
@@ -11824,23 +12321,23 @@ async function scaffoldProject(options) {
|
|
|
11824
12321
|
const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
|
|
11825
12322
|
const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
|
|
11826
12323
|
try {
|
|
11827
|
-
await
|
|
12324
|
+
await mkdir9(projectDir, { recursive: true });
|
|
11828
12325
|
if (customTemplateDir) {
|
|
11829
12326
|
const entries = await readdir6(customTemplateDir, { withFileTypes: true });
|
|
11830
12327
|
for (const entry of entries) {
|
|
11831
12328
|
if (entry.name === ".gutterpress-template.json")
|
|
11832
12329
|
continue;
|
|
11833
|
-
await cp2(
|
|
12330
|
+
await cp2(path20.join(customTemplateDir, entry.name), path20.join(projectDir, entry.name), { recursive: true });
|
|
11834
12331
|
}
|
|
11835
|
-
await
|
|
12332
|
+
await mkdir9(path20.join(projectDir, "assets"), { recursive: true });
|
|
11836
12333
|
} else {
|
|
11837
12334
|
const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
|
|
11838
12335
|
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11839
|
-
await
|
|
11840
|
-
await copyFile(tplManifest,
|
|
11841
|
-
await copyFile(tplChapter,
|
|
11842
|
-
await
|
|
11843
|
-
await
|
|
12336
|
+
await mkdir9(path20.join(projectDir, "assets"), { recursive: true });
|
|
12337
|
+
await copyFile(tplManifest, path20.join(projectDir, MANIFEST_FILENAMES[0]));
|
|
12338
|
+
await copyFile(tplChapter, path20.join(projectDir, "chapter-01.md"));
|
|
12339
|
+
await mkdir9(path20.join(projectDir, "styles"), { recursive: true });
|
|
12340
|
+
await writeFile9(path20.join(projectDir, "styles", "book.css"), starterOverridesCss(), "utf8");
|
|
11844
12341
|
}
|
|
11845
12342
|
} catch (e) {
|
|
11846
12343
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not create the book files: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -11850,34 +12347,26 @@ async function scaffoldProject(options) {
|
|
|
11850
12347
|
} catch (e) {
|
|
11851
12348
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
|
|
11852
12349
|
}
|
|
11853
|
-
const manifestPath = MANIFEST_FILENAMES.map((name2) =>
|
|
12350
|
+
const manifestPath = MANIFEST_FILENAMES.map((name2) => path20.join(projectDir, name2)).find(existsSync14) ?? path20.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
11854
12351
|
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11855
12352
|
const substitutions = {
|
|
11856
12353
|
"{{TITLE}}": escapeYamlScalar(name),
|
|
11857
12354
|
"{{AUTHOR}}": escapeYamlScalar(author)
|
|
11858
12355
|
};
|
|
11859
|
-
let openFile =
|
|
12356
|
+
let openFile = path20.join(projectDir, "chapter-01.md");
|
|
11860
12357
|
try {
|
|
11861
12358
|
await fillTemplateFile(manifestPath, substitutions);
|
|
11862
12359
|
if (preset) {
|
|
11863
12360
|
const { doc, file } = await loadManifestDoc(projectDir);
|
|
11864
12361
|
doc.set("preset", preset);
|
|
11865
12362
|
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
|
-
}
|
|
12363
|
+
if (options.customPage)
|
|
12364
|
+
writePageBounds(doc, options.customPage);
|
|
11876
12365
|
await writeManifestDoc(file, doc);
|
|
11877
12366
|
}
|
|
11878
12367
|
const firstSource = await firstSourceFile(manifestPath);
|
|
11879
12368
|
if (firstSource)
|
|
11880
|
-
openFile =
|
|
12369
|
+
openFile = path20.join(projectDir, firstSource);
|
|
11881
12370
|
await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
|
|
11882
12371
|
} catch (e) {
|
|
11883
12372
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not finalize the book files: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -11899,7 +12388,7 @@ async function scaffoldProject(options) {
|
|
|
11899
12388
|
versionHistory = "none";
|
|
11900
12389
|
const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
|
|
11901
12390
|
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
11902
|
-
versionHistoryError = enclosingRepoDir ? `This book was added to ${
|
|
12391
|
+
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
12392
|
}
|
|
11904
12393
|
}
|
|
11905
12394
|
const result = {
|
|
@@ -11919,7 +12408,7 @@ function prettifyFolderName(base) {
|
|
|
11919
12408
|
}
|
|
11920
12409
|
async function adoptFolder(options) {
|
|
11921
12410
|
const dir = options.dir;
|
|
11922
|
-
if (typeof dir !== "string" || !
|
|
12411
|
+
if (typeof dir !== "string" || !path20.isAbsolute(dir)) {
|
|
11923
12412
|
throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
|
|
11924
12413
|
}
|
|
11925
12414
|
let st;
|
|
@@ -11931,11 +12420,11 @@ async function adoptFolder(options) {
|
|
|
11931
12420
|
if (!st.isDirectory()) {
|
|
11932
12421
|
throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
|
|
11933
12422
|
}
|
|
11934
|
-
if (MANIFEST_FILENAMES.some((name) =>
|
|
12423
|
+
if (MANIFEST_FILENAMES.some((name) => existsSync14(path20.join(dir, name)))) {
|
|
11935
12424
|
throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress book.");
|
|
11936
12425
|
}
|
|
11937
12426
|
const template = options.template ?? "book";
|
|
11938
|
-
const title = (options.title ?? "").trim() || prettifyFolderName(
|
|
12427
|
+
const title = (options.title ?? "").trim() || prettifyFolderName(path20.basename(dir));
|
|
11939
12428
|
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
11940
12429
|
let mdFiles;
|
|
11941
12430
|
try {
|
|
@@ -11943,14 +12432,14 @@ async function adoptFolder(options) {
|
|
|
11943
12432
|
mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
11944
12433
|
if (mdFiles.length === 0) {
|
|
11945
12434
|
const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
|
|
11946
|
-
await copyFile(tplChapter,
|
|
11947
|
-
await fillTemplateFile(
|
|
12435
|
+
await copyFile(tplChapter, path20.join(dir, "chapter-01.md"));
|
|
12436
|
+
await fillTemplateFile(path20.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
|
|
11948
12437
|
mdFiles = ["chapter-01.md"];
|
|
11949
12438
|
}
|
|
11950
|
-
await
|
|
11951
|
-
const bookCssPath =
|
|
11952
|
-
if (!
|
|
11953
|
-
await
|
|
12439
|
+
await mkdir9(path20.join(dir, "styles"), { recursive: true });
|
|
12440
|
+
const bookCssPath = path20.join(dir, "styles", "book.css");
|
|
12441
|
+
if (!existsSync14(bookCssPath)) {
|
|
12442
|
+
await writeFile9(bookCssPath, starterOverridesCss(), "utf8");
|
|
11954
12443
|
}
|
|
11955
12444
|
const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
|
|
11956
12445
|
`);
|
|
@@ -11966,8 +12455,8 @@ ${filesYaml}
|
|
|
11966
12455
|
styles:
|
|
11967
12456
|
- styles/book.css
|
|
11968
12457
|
`;
|
|
11969
|
-
await
|
|
11970
|
-
await
|
|
12458
|
+
await writeFile9(path20.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
|
|
12459
|
+
await mkdir9(path20.join(dir, "assets"), { recursive: true });
|
|
11971
12460
|
await ensureProjectGitignore(dir);
|
|
11972
12461
|
} catch (e) {
|
|
11973
12462
|
if (e instanceof CreateProjectErrorImpl)
|
|
@@ -11994,8 +12483,8 @@ styles:
|
|
|
11994
12483
|
}
|
|
11995
12484
|
const result = {
|
|
11996
12485
|
projectDir: dir,
|
|
11997
|
-
manifestPath:
|
|
11998
|
-
openFile:
|
|
12486
|
+
manifestPath: path20.join(dir, MANIFEST_FILENAMES[0]),
|
|
12487
|
+
openFile: path20.join(dir, mdFiles[0]),
|
|
11999
12488
|
versionHistory
|
|
12000
12489
|
};
|
|
12001
12490
|
if (versionHistoryError !== undefined)
|
|
@@ -12005,7 +12494,7 @@ styles:
|
|
|
12005
12494
|
async function firstSourceFile(manifestPath) {
|
|
12006
12495
|
let text;
|
|
12007
12496
|
try {
|
|
12008
|
-
text = await
|
|
12497
|
+
text = await readFile29(manifestPath, "utf8");
|
|
12009
12498
|
} catch {
|
|
12010
12499
|
return;
|
|
12011
12500
|
}
|
|
@@ -12017,16 +12506,16 @@ async function firstSourceFile(manifestPath) {
|
|
|
12017
12506
|
return m ? m[1].trim() : undefined;
|
|
12018
12507
|
}
|
|
12019
12508
|
async function fillTemplateFile(filePath, substitutions) {
|
|
12020
|
-
let text = await
|
|
12509
|
+
let text = await readFile29(filePath, "utf8");
|
|
12021
12510
|
for (const [token, value] of Object.entries(substitutions)) {
|
|
12022
12511
|
text = text.split(token).join(value);
|
|
12023
12512
|
}
|
|
12024
|
-
await
|
|
12513
|
+
await writeFile9(filePath, text, "utf8");
|
|
12025
12514
|
}
|
|
12026
12515
|
// src/lib/extension-scaffold.ts
|
|
12027
|
-
import { access as access2, mkdir as
|
|
12516
|
+
import { access as access2, mkdir as mkdir10, readFile as readFile30, writeFile as writeFile10 } from "node:fs/promises";
|
|
12028
12517
|
import { constants as FS2 } from "node:fs";
|
|
12029
|
-
import
|
|
12518
|
+
import path21 from "node:path";
|
|
12030
12519
|
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
12031
12520
|
var RESERVED_PREFIX = "gp-";
|
|
12032
12521
|
var TEMPLATE_FILES = {
|
|
@@ -12104,7 +12593,7 @@ async function scaffoldExtension(options) {
|
|
|
12104
12593
|
}
|
|
12105
12594
|
const prefix = resolveExtensionPrefix(slug, options.prefix);
|
|
12106
12595
|
const parentDir = options.parentDir;
|
|
12107
|
-
if (!parentDir || !
|
|
12596
|
+
if (!parentDir || !path21.isAbsolute(parentDir)) {
|
|
12108
12597
|
throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
|
|
12109
12598
|
}
|
|
12110
12599
|
try {
|
|
@@ -12112,7 +12601,7 @@ async function scaffoldExtension(options) {
|
|
|
12112
12601
|
} catch {
|
|
12113
12602
|
throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
|
|
12114
12603
|
}
|
|
12115
|
-
const extensionDir =
|
|
12604
|
+
const extensionDir = path21.join(parentDir, slug);
|
|
12116
12605
|
let targetExists = true;
|
|
12117
12606
|
try {
|
|
12118
12607
|
await access2(extensionDir, FS2.F_OK);
|
|
@@ -12136,14 +12625,14 @@ async function scaffoldExtension(options) {
|
|
|
12136
12625
|
const entries = TEMPLATE_FILES[kind];
|
|
12137
12626
|
const written = [];
|
|
12138
12627
|
try {
|
|
12139
|
-
await
|
|
12628
|
+
await mkdir10(extensionDir, { recursive: true });
|
|
12140
12629
|
for (const [assetName, relDest] of entries) {
|
|
12141
12630
|
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
12142
|
-
const dest =
|
|
12143
|
-
await
|
|
12144
|
-
const text = await
|
|
12631
|
+
const dest = path21.join(extensionDir, relDest);
|
|
12632
|
+
await mkdir10(path21.dirname(dest), { recursive: true });
|
|
12633
|
+
const text = await readFile30(src, "utf8");
|
|
12145
12634
|
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
12146
|
-
await
|
|
12635
|
+
await writeFile10(dest, substitute(text, table), "utf8");
|
|
12147
12636
|
written.push(relDest);
|
|
12148
12637
|
}
|
|
12149
12638
|
} catch (e) {
|
|
@@ -12151,11 +12640,11 @@ async function scaffoldExtension(options) {
|
|
|
12151
12640
|
}
|
|
12152
12641
|
return {
|
|
12153
12642
|
extensionDir,
|
|
12154
|
-
manifestPath:
|
|
12643
|
+
manifestPath: path21.join(extensionDir, "package.json"),
|
|
12155
12644
|
kind,
|
|
12156
12645
|
slug,
|
|
12157
12646
|
prefix,
|
|
12158
|
-
openFile:
|
|
12647
|
+
openFile: path21.join(extensionDir, OPEN_FILE[kind]),
|
|
12159
12648
|
files: written
|
|
12160
12649
|
};
|
|
12161
12650
|
}
|
|
@@ -12167,10 +12656,10 @@ function substitute(text, table) {
|
|
|
12167
12656
|
return out;
|
|
12168
12657
|
}
|
|
12169
12658
|
// src/lib/project-templates.ts
|
|
12170
|
-
import { cp as cp3, mkdir as
|
|
12171
|
-
import { constants as FS3, existsSync as
|
|
12659
|
+
import { cp as cp3, mkdir as mkdir11, readFile as readFile31, readdir as readdir7, stat as stat7, writeFile as writeFile11 } from "node:fs/promises";
|
|
12660
|
+
import { constants as FS3, existsSync as existsSync15 } from "node:fs";
|
|
12172
12661
|
import { access as access3 } from "node:fs/promises";
|
|
12173
|
-
import
|
|
12662
|
+
import path22 from "node:path";
|
|
12174
12663
|
import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
|
|
12175
12664
|
var BUILT_IN_TEMPLATE_IDS = [
|
|
12176
12665
|
"book",
|
|
@@ -12179,7 +12668,7 @@ var BUILT_IN_TEMPLATE_IDS = [
|
|
|
12179
12668
|
];
|
|
12180
12669
|
async function readTemplateChoices(manifestPath) {
|
|
12181
12670
|
try {
|
|
12182
|
-
const text = await
|
|
12671
|
+
const text = await readFile31(manifestPath, "utf8");
|
|
12183
12672
|
const doc = parseDocument2(text);
|
|
12184
12673
|
const out = {};
|
|
12185
12674
|
const preset = doc.get("preset");
|
|
@@ -12255,7 +12744,7 @@ function rewriteEntry(node, newPath) {
|
|
|
12255
12744
|
node.value = newPath;
|
|
12256
12745
|
}
|
|
12257
12746
|
function commonAncestorDir(absFiles) {
|
|
12258
|
-
const dirSegs = absFiles.map((f) =>
|
|
12747
|
+
const dirSegs = absFiles.map((f) => path22.resolve(f).split(path22.sep).slice(0, -1));
|
|
12259
12748
|
if (dirSegs.length === 0)
|
|
12260
12749
|
return "";
|
|
12261
12750
|
let common = dirSegs[0];
|
|
@@ -12265,22 +12754,22 @@ function commonAncestorDir(absFiles) {
|
|
|
12265
12754
|
i++;
|
|
12266
12755
|
common = common.slice(0, i);
|
|
12267
12756
|
}
|
|
12268
|
-
return common.join(
|
|
12757
|
+
return common.join(path22.sep) || path22.sep;
|
|
12269
12758
|
}
|
|
12270
12759
|
function pickVendorDir(templateDir, base) {
|
|
12271
|
-
const preferred =
|
|
12272
|
-
if (!
|
|
12760
|
+
const preferred = path22.basename(base) || VENDOR_DIR;
|
|
12761
|
+
if (!existsSync15(path22.join(templateDir, preferred)))
|
|
12273
12762
|
return preferred;
|
|
12274
12763
|
for (let n = 1;; n++) {
|
|
12275
12764
|
const candidate = `${preferred}-${n}`;
|
|
12276
|
-
if (!
|
|
12765
|
+
if (!existsSync15(path22.join(templateDir, candidate)))
|
|
12277
12766
|
return candidate;
|
|
12278
12767
|
}
|
|
12279
12768
|
}
|
|
12280
12769
|
var TEMPLATE_META_FILE = ".gutterpress-template.json";
|
|
12281
12770
|
async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
12282
12771
|
const { doc, file } = await loadManifestDoc(templateDir);
|
|
12283
|
-
const escapes = (p) => escapesProjectRoot(sourceProjectDir,
|
|
12772
|
+
const escapes = (p) => escapesProjectRoot(sourceProjectDir, path22.resolve(sourceProjectDir, p));
|
|
12284
12773
|
const stylesSeq = doc.get("styles", true);
|
|
12285
12774
|
const extensionsSeq = doc.get("extensions", true);
|
|
12286
12775
|
const escapingStyles = collectEscaping(isSeq3(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
|
|
@@ -12296,16 +12785,16 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
|
12296
12785
|
return { vendoredRefs: [], excludedRefs };
|
|
12297
12786
|
}
|
|
12298
12787
|
const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
|
|
12299
|
-
const extensionFiles = escapingExtensions.map((p) =>
|
|
12788
|
+
const extensionFiles = escapingExtensions.map((p) => path22.resolve(sourceProjectDir, p.value));
|
|
12300
12789
|
const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
|
|
12301
12790
|
const base = commonAncestorDir(externalFiles);
|
|
12302
12791
|
const vendorDir = pickVendorDir(templateDir, base);
|
|
12303
|
-
await Promise.all(externalFiles.filter((abs) =>
|
|
12304
|
-
const dest =
|
|
12305
|
-
await
|
|
12792
|
+
await Promise.all(externalFiles.filter((abs) => existsSync15(abs)).map(async (abs) => {
|
|
12793
|
+
const dest = path22.join(templateDir, vendorDir, path22.relative(base, abs));
|
|
12794
|
+
await mkdir11(path22.dirname(dest), { recursive: true });
|
|
12306
12795
|
await cp3(abs, dest, { recursive: true });
|
|
12307
12796
|
}));
|
|
12308
|
-
const bookLocal = (value) => toPosixPath(
|
|
12797
|
+
const bookLocal = (value) => toPosixPath(path22.join(vendorDir, path22.relative(base, path22.resolve(sourceProjectDir, value))));
|
|
12309
12798
|
const vendoredRefs = [];
|
|
12310
12799
|
for (const { node, value } of escapingStyles) {
|
|
12311
12800
|
const rewritten = bookLocal(value);
|
|
@@ -12325,7 +12814,7 @@ async function saveProjectAsTemplate(options) {
|
|
|
12325
12814
|
const id = slugify(name);
|
|
12326
12815
|
if (!id)
|
|
12327
12816
|
throw new Error(`Could not derive a template id from "${name}".`);
|
|
12328
|
-
const dir =
|
|
12817
|
+
const dir = path22.join(templatesRoot, id);
|
|
12329
12818
|
let exists = true;
|
|
12330
12819
|
try {
|
|
12331
12820
|
await access3(dir, FS3.F_OK);
|
|
@@ -12335,20 +12824,20 @@ async function saveProjectAsTemplate(options) {
|
|
|
12335
12824
|
if (exists) {
|
|
12336
12825
|
throw new Error(`A template named "${name}" already exists. Choose a different name.`);
|
|
12337
12826
|
}
|
|
12338
|
-
await
|
|
12827
|
+
await mkdir11(dir, { recursive: true });
|
|
12339
12828
|
const entries = await readdir7(projectDir, { withFileTypes: true });
|
|
12340
12829
|
for (const entry of entries) {
|
|
12341
12830
|
if (SKIP_ENTRIES.has(entry.name))
|
|
12342
12831
|
continue;
|
|
12343
12832
|
if (entry.name === TEMPLATE_META_FILE)
|
|
12344
12833
|
continue;
|
|
12345
|
-
await cp3(
|
|
12834
|
+
await cp3(path22.join(projectDir, entry.name), path22.join(dir, entry.name), { recursive: true });
|
|
12346
12835
|
}
|
|
12347
12836
|
const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
|
|
12348
12837
|
const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
|
|
12349
12838
|
if (manifestName)
|
|
12350
|
-
await retokeniseManifest(
|
|
12351
|
-
await
|
|
12839
|
+
await retokeniseManifest(path22.join(dir, manifestName));
|
|
12840
|
+
await writeFile11(path22.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
|
|
12352
12841
|
return {
|
|
12353
12842
|
id,
|
|
12354
12843
|
label: name,
|
|
@@ -12361,12 +12850,12 @@ async function saveProjectAsTemplate(options) {
|
|
|
12361
12850
|
async function retokeniseManifest(manifestPath) {
|
|
12362
12851
|
let text;
|
|
12363
12852
|
try {
|
|
12364
|
-
text = await
|
|
12853
|
+
text = await readFile31(manifestPath, "utf8");
|
|
12365
12854
|
} catch {
|
|
12366
12855
|
return;
|
|
12367
12856
|
}
|
|
12368
12857
|
text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
|
|
12369
|
-
await
|
|
12858
|
+
await writeFile11(manifestPath, text, "utf8");
|
|
12370
12859
|
}
|
|
12371
12860
|
async function listCustomTemplates(templatesRoot) {
|
|
12372
12861
|
let entries;
|
|
@@ -12379,10 +12868,10 @@ async function listCustomTemplates(templatesRoot) {
|
|
|
12379
12868
|
for (const entry of entries) {
|
|
12380
12869
|
if (!entry.isDirectory())
|
|
12381
12870
|
continue;
|
|
12382
|
-
const dir =
|
|
12871
|
+
const dir = path22.join(templatesRoot, entry.name);
|
|
12383
12872
|
let label = prettify(entry.name);
|
|
12384
12873
|
try {
|
|
12385
|
-
const meta = JSON.parse(await
|
|
12874
|
+
const meta = JSON.parse(await readFile31(path22.join(dir, TEMPLATE_META_FILE), "utf8"));
|
|
12386
12875
|
if (typeof meta.label === "string" && meta.label)
|
|
12387
12876
|
label = meta.label;
|
|
12388
12877
|
} catch {}
|
|
@@ -12392,7 +12881,7 @@ async function listCustomTemplates(templatesRoot) {
|
|
|
12392
12881
|
description: "Your saved template.",
|
|
12393
12882
|
kind: "custom",
|
|
12394
12883
|
dir,
|
|
12395
|
-
...await readTemplateChoices(
|
|
12884
|
+
...await readTemplateChoices(path22.join(dir, MANIFEST_FILENAMES[0]))
|
|
12396
12885
|
});
|
|
12397
12886
|
}
|
|
12398
12887
|
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
@@ -12404,12 +12893,12 @@ async function importTemplateFromFolder(options) {
|
|
|
12404
12893
|
if (!srcStat.isDirectory()) {
|
|
12405
12894
|
throw new Error("The chosen path is not a folder.");
|
|
12406
12895
|
}
|
|
12407
|
-
const name = options.name?.trim() || prettify(
|
|
12896
|
+
const name = options.name?.trim() || prettify(path22.basename(sourceDir));
|
|
12408
12897
|
return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
|
|
12409
12898
|
}
|
|
12410
12899
|
// src/lib/snippets.ts
|
|
12411
|
-
import { readdir as readdir8, readFile as
|
|
12412
|
-
import
|
|
12900
|
+
import { readdir as readdir8, readFile as readFile32, writeFile as writeFile12, mkdir as mkdir12, rm as rm4 } from "node:fs/promises";
|
|
12901
|
+
import path23 from "node:path";
|
|
12413
12902
|
var SNIPPETS_DIR = "snippets";
|
|
12414
12903
|
var LEVEL_RANK = { project: 0, extension: 1, core: 2 };
|
|
12415
12904
|
var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
@@ -12429,9 +12918,9 @@ function substituteVariables(template, values) {
|
|
|
12429
12918
|
return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
|
|
12430
12919
|
}
|
|
12431
12920
|
function resolveSnippetPath(projectDir, fileName) {
|
|
12432
|
-
const dir =
|
|
12433
|
-
const full =
|
|
12434
|
-
if (full !==
|
|
12921
|
+
const dir = path23.resolve(projectDir, SNIPPETS_DIR);
|
|
12922
|
+
const full = path23.resolve(dir, fileName);
|
|
12923
|
+
if (full !== path23.join(dir, path23.basename(fileName)) || path23.dirname(full) !== dir) {
|
|
12435
12924
|
throw new Error(`Unsafe snippet filename: ${fileName}`);
|
|
12436
12925
|
}
|
|
12437
12926
|
return full;
|
|
@@ -12449,7 +12938,7 @@ async function scanSnippetFiles(dir) {
|
|
|
12449
12938
|
continue;
|
|
12450
12939
|
let body = "";
|
|
12451
12940
|
try {
|
|
12452
|
-
body = await
|
|
12941
|
+
body = await readFile32(path23.join(dir, fileName), "utf8");
|
|
12453
12942
|
} catch {
|
|
12454
12943
|
continue;
|
|
12455
12944
|
}
|
|
@@ -12464,7 +12953,7 @@ async function scanSnippetFiles(dir) {
|
|
|
12464
12953
|
return entries;
|
|
12465
12954
|
}
|
|
12466
12955
|
async function listSnippets(projectDir) {
|
|
12467
|
-
const files = await scanSnippetFiles(
|
|
12956
|
+
const files = await scanSnippetFiles(path23.join(projectDir, SNIPPETS_DIR));
|
|
12468
12957
|
return files.map((file) => ({ ...file, source: { kind: "project" } }));
|
|
12469
12958
|
}
|
|
12470
12959
|
async function saveSnippet(projectDir, name, body) {
|
|
@@ -12472,9 +12961,9 @@ async function saveSnippet(projectDir, name, body) {
|
|
|
12472
12961
|
if (!stem)
|
|
12473
12962
|
throw new Error(`Could not derive a filename from "${name}".`);
|
|
12474
12963
|
const fileName = `${stem}.md`;
|
|
12475
|
-
const dir =
|
|
12476
|
-
await
|
|
12477
|
-
await
|
|
12964
|
+
const dir = path23.join(projectDir, SNIPPETS_DIR);
|
|
12965
|
+
await mkdir12(dir, { recursive: true });
|
|
12966
|
+
await writeFile12(resolveSnippetPath(projectDir, fileName), body, "utf8");
|
|
12478
12967
|
return { name, fileName, variables: extractVariables(body), body, source: { kind: "project" } };
|
|
12479
12968
|
}
|
|
12480
12969
|
async function deleteSnippet(projectDir, fileName) {
|
|
@@ -12504,7 +12993,7 @@ async function collectLibrary(projectDir) {
|
|
|
12504
12993
|
continue;
|
|
12505
12994
|
const source = { kind: "extension", ref: entry.use, name: entry.label };
|
|
12506
12995
|
const folder = await declaredSnippetsFolder(entry);
|
|
12507
|
-
const snippets2 = folder ? (await scanSnippetFiles(
|
|
12996
|
+
const snippets2 = folder ? (await scanSnippetFiles(path23.join(entry.dir, folder))).map((file) => ({ ...file, source })) : [];
|
|
12508
12997
|
loaded.push({ entry, source, folder, snippets: snippets2, markers: await declaredMarkers(entry, projectDir) });
|
|
12509
12998
|
}
|
|
12510
12999
|
let registry;
|
|
@@ -12518,24 +13007,29 @@ async function collectLibrary(projectDir) {
|
|
|
12518
13007
|
const resolved = registry.get(name);
|
|
12519
13008
|
if (!resolved || resolved.deprecated !== undefined)
|
|
12520
13009
|
continue;
|
|
12521
|
-
components.push({
|
|
13010
|
+
components.push({
|
|
13011
|
+
name,
|
|
13012
|
+
source,
|
|
13013
|
+
variants: Object.keys(resolved.variants ?? {}),
|
|
13014
|
+
...resolved.selfClosing ? { selfClosing: true } : {}
|
|
13015
|
+
});
|
|
12522
13016
|
if (!entry.dir)
|
|
12523
13017
|
continue;
|
|
12524
13018
|
const own = typeof decl.snippet === "string" && decl.snippet.trim() ? decl.snippet.trim() : null;
|
|
12525
|
-
const rel = own ??
|
|
13019
|
+
const rel = own ?? path23.join(folder ?? SNIPPETS_DIR, `${name}.md`);
|
|
12526
13020
|
if (pathEscapesFolder(rel))
|
|
12527
13021
|
continue;
|
|
12528
|
-
const listed = folder &&
|
|
13022
|
+
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
13023
|
if (listed) {
|
|
12530
13024
|
listed.component = name;
|
|
12531
13025
|
continue;
|
|
12532
13026
|
}
|
|
12533
|
-
const body = await
|
|
13027
|
+
const body = await readFile32(path23.join(entry.dir, rel), "utf8").catch(() => null);
|
|
12534
13028
|
if (body === null)
|
|
12535
13029
|
continue;
|
|
12536
13030
|
snippets2.push({
|
|
12537
|
-
name: prettify(
|
|
12538
|
-
fileName: rel.split(
|
|
13031
|
+
name: prettify(path23.basename(rel).replace(/\.md$/i, "")),
|
|
13032
|
+
fileName: rel.split(path23.sep).join("/"),
|
|
12539
13033
|
variables: extractVariables(body),
|
|
12540
13034
|
body,
|
|
12541
13035
|
source,
|
|
@@ -12579,10 +13073,10 @@ async function listMarkerComponents(projectDir) {
|
|
|
12579
13073
|
return (await collectLibrary(projectDir)).components;
|
|
12580
13074
|
}
|
|
12581
13075
|
// src/lib/extension-import.ts
|
|
12582
|
-
import { existsSync as
|
|
12583
|
-
import { cp as cp4, mkdir as
|
|
13076
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
13077
|
+
import { cp as cp4, mkdir as mkdir13, mkdtemp as mkdtemp2, readFile as readFile33, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
|
|
12584
13078
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
12585
|
-
import
|
|
13079
|
+
import path24 from "node:path";
|
|
12586
13080
|
import { unzipSync } from "fflate";
|
|
12587
13081
|
var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
12588
13082
|
var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
|
|
@@ -12649,11 +13143,11 @@ function mb2(n) {
|
|
|
12649
13143
|
return `${Math.round(n / (1024 * 1024))}MB`;
|
|
12650
13144
|
}
|
|
12651
13145
|
async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallbackName) {
|
|
12652
|
-
const cssPath =
|
|
12653
|
-
if (!
|
|
13146
|
+
const cssPath = path24.join(sourceDir, "theme.css");
|
|
13147
|
+
if (!existsSync16(cssPath)) {
|
|
12654
13148
|
throw new Error("A theme must contain a theme.css file.");
|
|
12655
13149
|
}
|
|
12656
|
-
const css = await
|
|
13150
|
+
const css = await readFile33(cssPath, "utf8");
|
|
12657
13151
|
const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
|
|
12658
13152
|
if (reject) {
|
|
12659
13153
|
throw new Error(`theme.css could not be parsed — ${reject.message}`);
|
|
@@ -12662,7 +13156,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12662
13156
|
for (const w of printFindings) {
|
|
12663
13157
|
warnings.push({ code: "print-safety", message: w.message });
|
|
12664
13158
|
}
|
|
12665
|
-
const metaExists =
|
|
13159
|
+
const metaExists = existsSync16(path24.join(sourceDir, EXTENSION_MANIFEST_FILENAME));
|
|
12666
13160
|
const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
|
|
12667
13161
|
if (!metaExists) {
|
|
12668
13162
|
warnings.push({
|
|
@@ -12681,7 +13175,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12681
13175
|
for (let i = 0;i < extraSheets.length; i++) {
|
|
12682
13176
|
const rel = extraSheets[i];
|
|
12683
13177
|
const sheetPath = extraSheetPaths[i];
|
|
12684
|
-
const sheetCss = await
|
|
13178
|
+
const sheetCss = await readFile33(sheetPath, "utf8");
|
|
12685
13179
|
const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
|
|
12686
13180
|
if (sheetReject) {
|
|
12687
13181
|
throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
|
|
@@ -12700,8 +13194,8 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12700
13194
|
});
|
|
12701
13195
|
}
|
|
12702
13196
|
const id = await uniqueExtensionId(projectDir, meta.name?.trim() || fallbackName);
|
|
12703
|
-
const dest =
|
|
12704
|
-
await
|
|
13197
|
+
const dest = path24.join(projectDir, EXTENSIONS_DIR, id);
|
|
13198
|
+
await mkdir13(path24.dirname(dest), { recursive: true });
|
|
12705
13199
|
await cp4(sourceDir, dest, { recursive: true });
|
|
12706
13200
|
await completeLandedManifest(dest, id, meta, metaExists);
|
|
12707
13201
|
const entry = await addExtension(projectDir, `./${EXTENSIONS_DIR}/${id}`);
|
|
@@ -12710,18 +13204,18 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12710
13204
|
async function completeLandedManifest(dest, id, meta, metaExists) {
|
|
12711
13205
|
if (metaExists && extensionStyleList(meta).length > 0)
|
|
12712
13206
|
return;
|
|
12713
|
-
const file =
|
|
13207
|
+
const file = path24.join(dest, EXTENSION_MANIFEST_FILENAME);
|
|
12714
13208
|
let pkg = {};
|
|
12715
13209
|
if (metaExists) {
|
|
12716
13210
|
try {
|
|
12717
|
-
const parsed = JSON.parse(await
|
|
13211
|
+
const parsed = JSON.parse(await readFile33(file, "utf8"));
|
|
12718
13212
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12719
13213
|
pkg = parsed;
|
|
12720
13214
|
}
|
|
12721
13215
|
} catch {}
|
|
12722
13216
|
}
|
|
12723
13217
|
const gutterpress = pkg.gutterpress && typeof pkg.gutterpress === "object" && !Array.isArray(pkg.gutterpress) ? pkg.gutterpress : {};
|
|
12724
|
-
await
|
|
13218
|
+
await writeFile13(file, JSON.stringify({
|
|
12725
13219
|
...pkg,
|
|
12726
13220
|
name: typeof pkg.name === "string" && pkg.name.trim() ? pkg.name : id,
|
|
12727
13221
|
gutterpress: { ...gutterpress, styles: ["theme.css"] }
|
|
@@ -12768,7 +13262,7 @@ async function importExtensionFromZip(projectDir, archive) {
|
|
|
12768
13262
|
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
13263
|
}
|
|
12770
13264
|
const prefix = root === "" ? "" : `${root}/`;
|
|
12771
|
-
const tmp = await mkdtemp2(
|
|
13265
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-theme-"));
|
|
12772
13266
|
try {
|
|
12773
13267
|
const relPaths = [];
|
|
12774
13268
|
for (const [name, data] of Object.entries(files)) {
|
|
@@ -12780,13 +13274,13 @@ async function importExtensionFromZip(projectDir, archive) {
|
|
|
12780
13274
|
const rel = prefix ? norm.slice(prefix.length) : norm;
|
|
12781
13275
|
if (!rel || rel.split("/").some((seg) => seg === ".."))
|
|
12782
13276
|
continue;
|
|
12783
|
-
const dest =
|
|
12784
|
-
if (dest !== tmp && !dest.startsWith(tmp +
|
|
13277
|
+
const dest = path24.join(tmp, rel);
|
|
13278
|
+
if (dest !== tmp && !dest.startsWith(tmp + path24.sep)) {
|
|
12785
13279
|
throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
|
|
12786
13280
|
}
|
|
12787
13281
|
relPaths.push(rel);
|
|
12788
|
-
await
|
|
12789
|
-
await
|
|
13282
|
+
await mkdir13(path24.dirname(dest), { recursive: true });
|
|
13283
|
+
await writeFile13(dest, data);
|
|
12790
13284
|
}
|
|
12791
13285
|
return await finalizeExtensionImport(projectDir, tmp, relPaths, root || "imported-look");
|
|
12792
13286
|
} finally {
|
|
@@ -12799,10 +13293,10 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
12799
13293
|
throw new Error(`The CSS could not be parsed — ${reject.message}`);
|
|
12800
13294
|
}
|
|
12801
13295
|
const displayName = name.trim() || "Imported theme";
|
|
12802
|
-
const tmp = await mkdtemp2(
|
|
13296
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-theme-"));
|
|
12803
13297
|
try {
|
|
12804
|
-
await
|
|
12805
|
-
await
|
|
13298
|
+
await writeFile13(path24.join(tmp, "theme.css"), css, "utf8");
|
|
13299
|
+
await writeFile13(path24.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify({ name: slugify(displayName, "look"), gutterpress: { styles: ["theme.css"] } }, null, 2), "utf8");
|
|
12806
13300
|
const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], displayName);
|
|
12807
13301
|
result.warnings.unshift({
|
|
12808
13302
|
code: "no-theme-json",
|
|
@@ -12814,14 +13308,14 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
12814
13308
|
}
|
|
12815
13309
|
}
|
|
12816
13310
|
async function importExtensionFromFile(projectDir, filePath) {
|
|
12817
|
-
const ext =
|
|
13311
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
12818
13312
|
if (ext === ".zip") {
|
|
12819
|
-
const buf = await
|
|
13313
|
+
const buf = await readFile33(filePath);
|
|
12820
13314
|
return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
12821
13315
|
}
|
|
12822
13316
|
if (ext === ".css") {
|
|
12823
|
-
const css = await
|
|
12824
|
-
return importExtensionFromCssText(projectDir, css, prettify(
|
|
13317
|
+
const css = await readFile33(filePath, "utf8");
|
|
13318
|
+
return importExtensionFromCssText(projectDir, css, prettify(path24.basename(filePath, ".css")));
|
|
12825
13319
|
}
|
|
12826
13320
|
throw new Error("Choose a .zip extension package or a .css stylesheet.");
|
|
12827
13321
|
}
|
|
@@ -12869,7 +13363,7 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12869
13363
|
let baseName;
|
|
12870
13364
|
if (looksLikeCssUrl(trimmed)) {
|
|
12871
13365
|
css = await fetchText(trimmed);
|
|
12872
|
-
baseName =
|
|
13366
|
+
baseName = path24.basename(new URL(trimmed, "https://x/").pathname, ".css") || "look";
|
|
12873
13367
|
} else {
|
|
12874
13368
|
const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
12875
13369
|
css = await fetchText(`${base}theme.css`);
|
|
@@ -12878,14 +13372,14 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12878
13372
|
if (parsed && typeof parsed === "object")
|
|
12879
13373
|
meta = parsed;
|
|
12880
13374
|
} catch {}
|
|
12881
|
-
baseName = typeof meta.name === "string" && meta.name ||
|
|
13375
|
+
baseName = typeof meta.name === "string" && meta.name || path24.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "look";
|
|
12882
13376
|
}
|
|
12883
13377
|
if (!css.trim())
|
|
12884
13378
|
throw new Error(`Fetched CSS from ${trimmed} was empty.`);
|
|
12885
13379
|
assertLooksLikeCss(css, trimmed);
|
|
12886
|
-
const tmp = await mkdtemp2(
|
|
13380
|
+
const tmp = await mkdtemp2(path24.join(tmpdir2(), "gutterpress-ext-"));
|
|
12887
13381
|
try {
|
|
12888
|
-
await
|
|
13382
|
+
await writeFile13(path24.join(tmp, "theme.css"), css, "utf8");
|
|
12889
13383
|
const gp = meta.gutterpress && typeof meta.gutterpress === "object" && !Array.isArray(meta.gutterpress) ? meta.gutterpress : {};
|
|
12890
13384
|
const finalMeta = {
|
|
12891
13385
|
name: typeof meta.name === "string" && meta.name || slugify(baseName, "look"),
|
|
@@ -12897,7 +13391,7 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
12897
13391
|
preview: typeof gp.preview === "string" ? gp.preview : null
|
|
12898
13392
|
}
|
|
12899
13393
|
};
|
|
12900
|
-
await
|
|
13394
|
+
await writeFile13(path24.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify(finalMeta, null, 2), "utf8");
|
|
12901
13395
|
return await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], baseName);
|
|
12902
13396
|
} finally {
|
|
12903
13397
|
await rm5(tmp, { recursive: true, force: true });
|
|
@@ -13018,7 +13512,7 @@ async function searchNpmExtensions(query, options = {}) {
|
|
|
13018
13512
|
return { matches, total: pages.reduce((sum, page) => sum + page.total, 0) };
|
|
13019
13513
|
}
|
|
13020
13514
|
// src/lib/manifest-config.ts
|
|
13021
|
-
import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as
|
|
13515
|
+
import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
|
|
13022
13516
|
function hasKey(obj, key) {
|
|
13023
13517
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
13024
13518
|
}
|
|
@@ -13101,7 +13595,7 @@ async function setActiveStyles(projectDir, paths) {
|
|
|
13101
13595
|
} else {
|
|
13102
13596
|
const seq = new YAMLSeq3(doc.schema);
|
|
13103
13597
|
for (const p of paths)
|
|
13104
|
-
seq.add(new
|
|
13598
|
+
seq.add(new Scalar3(p));
|
|
13105
13599
|
doc.set("styles", seq);
|
|
13106
13600
|
}
|
|
13107
13601
|
await writeManifestDoc(file, doc);
|
|
@@ -13643,7 +14137,8 @@ async function listGitHubRepositories(credential, options = {}) {
|
|
|
13643
14137
|
fullName: r.full_name,
|
|
13644
14138
|
private: r.private,
|
|
13645
14139
|
defaultBranch: r.default_branch,
|
|
13646
|
-
htmlUrl: r.html_url
|
|
14140
|
+
htmlUrl: r.html_url,
|
|
14141
|
+
...r.size === 0 && !r.archived && r.permissions?.push !== false ? { maybeEmpty: true } : {}
|
|
13647
14142
|
});
|
|
13648
14143
|
}
|
|
13649
14144
|
if (body.length < PER_PAGE)
|
|
@@ -13708,10 +14203,12 @@ async function listGitHubBranches(credential, owner, repo, options = {}) {
|
|
|
13708
14203
|
}
|
|
13709
14204
|
return branches;
|
|
13710
14205
|
}
|
|
13711
|
-
// src/lib/remote-auth/
|
|
13712
|
-
import
|
|
13713
|
-
|
|
13714
|
-
|
|
14206
|
+
// src/lib/remote-auth/online-backup.ts
|
|
14207
|
+
import git5 from "isomorphic-git";
|
|
14208
|
+
|
|
14209
|
+
// src/lib/remote-auth/sync.ts
|
|
14210
|
+
import git4 from "isomorphic-git";
|
|
14211
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
13715
14212
|
|
|
13716
14213
|
// src/lib/remote-auth/git-http.ts
|
|
13717
14214
|
import httpNode from "isomorphic-git/http/node";
|
|
@@ -13804,617 +14301,68 @@ function withIdleTimeout(http3, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
|
|
|
13804
14301
|
}
|
|
13805
14302
|
var defaultGitHttp = withIdleTimeout(httpNode);
|
|
13806
14303
|
|
|
13807
|
-
// src/lib/remote-auth/
|
|
14304
|
+
// src/lib/remote-auth/converge-merge.ts
|
|
14305
|
+
import { mkdir as mkdir14, unlink as unlink3, writeFile as writeFile14 } from "node:fs/promises";
|
|
14306
|
+
import path25 from "node:path";
|
|
13808
14307
|
import git2 from "isomorphic-git";
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
return gitScopeFor(source);
|
|
13813
|
-
return projectDir;
|
|
13814
|
-
}
|
|
13815
|
-
function isCredentialTransmissionSafe(url) {
|
|
13816
|
-
try {
|
|
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
|
-
}
|
|
13828
|
-
}
|
|
13829
|
-
|
|
13830
|
-
class InsecureTransportError extends Error {
|
|
13831
|
-
code = "InsecureTransport";
|
|
13832
|
-
constructor() {
|
|
13833
|
-
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.");
|
|
13834
|
-
this.name = "InsecureTransportError";
|
|
13835
|
-
}
|
|
14308
|
+
import diff3Merge from "diff3";
|
|
14309
|
+
function isMergeConflictError(e) {
|
|
14310
|
+
return e?.code === "MergeConflictError";
|
|
13836
14311
|
}
|
|
13837
|
-
function
|
|
13838
|
-
return e?.code === "
|
|
14312
|
+
function isCheckoutConflict(e) {
|
|
14313
|
+
return e?.code === "CheckoutConflictError";
|
|
13839
14314
|
}
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
|
|
13849
|
-
password: credential.token
|
|
13850
|
-
};
|
|
13851
|
-
}
|
|
13852
|
-
};
|
|
14315
|
+
var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
|
|
14316
|
+
var CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
|
|
14317
|
+
var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
|
|
14318
|
+
var MARKER_UNSAFE_EXT = /\.svg$/i;
|
|
14319
|
+
var OUR_LABEL = "your version";
|
|
14320
|
+
var THEIR_LABEL = "online version";
|
|
14321
|
+
function stringLooksBinary(s) {
|
|
14322
|
+
return s.includes("\x00");
|
|
13853
14323
|
}
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
const
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
try {
|
|
13871
|
-
credential = await options.tokenStore.get(host) ?? undefined;
|
|
13872
|
-
} catch {
|
|
13873
|
-
credential = undefined;
|
|
14324
|
+
var LINEBREAKS = /^.*(\r?\n|$)/gm;
|
|
14325
|
+
function mergeWithMarkers(baseContent, ourContent, theirContent) {
|
|
14326
|
+
const result = diff3Merge(ourContent.match(LINEBREAKS) ?? [], baseContent.match(LINEBREAKS) ?? [], theirContent.match(LINEBREAKS) ?? []);
|
|
14327
|
+
let merged = "";
|
|
14328
|
+
for (const item of result) {
|
|
14329
|
+
if (item.ok)
|
|
14330
|
+
merged += item.ok.join("");
|
|
14331
|
+
if (item.conflict) {
|
|
14332
|
+
merged += `<<<<<<< ${OUR_LABEL}
|
|
14333
|
+
`;
|
|
14334
|
+
merged += item.conflict.a.join("");
|
|
14335
|
+
merged += `=======
|
|
14336
|
+
`;
|
|
14337
|
+
merged += item.conflict.b.join("");
|
|
14338
|
+
merged += `>>>>>>> ${THEIR_LABEL}
|
|
14339
|
+
`;
|
|
13874
14340
|
}
|
|
13875
14341
|
}
|
|
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;
|
|
14342
|
+
return merged;
|
|
13902
14343
|
}
|
|
13903
|
-
function
|
|
13904
|
-
|
|
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
|
-
};
|
|
14344
|
+
async function readBlob(dir, cache2, oid, filepath) {
|
|
14345
|
+
return git2.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
|
|
13918
14346
|
}
|
|
13919
|
-
function
|
|
13920
|
-
|
|
13921
|
-
|
|
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);
|
|
14347
|
+
async function writeTreeFile(dir, filepath, content) {
|
|
14348
|
+
const abs = path25.join(dir, filepath);
|
|
14349
|
+
await mkdir14(path25.dirname(abs), { recursive: true });
|
|
14350
|
+
await writeFile14(abs, content);
|
|
14403
14351
|
}
|
|
14404
14352
|
async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
|
|
14405
|
-
const bases = await
|
|
14353
|
+
const bases = await git2.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
|
|
14406
14354
|
const base = bases[0];
|
|
14407
14355
|
if (!base && !allowUnrelated)
|
|
14408
14356
|
return null;
|
|
14409
14357
|
const out = [];
|
|
14410
|
-
await
|
|
14358
|
+
await git2.walk({
|
|
14411
14359
|
fs: gitFs,
|
|
14412
14360
|
dir,
|
|
14413
14361
|
cache: cache2,
|
|
14414
14362
|
trees: [
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
...base ? [
|
|
14363
|
+
git2.TREE({ ref: ourTip }),
|
|
14364
|
+
git2.TREE({ ref: theirTip }),
|
|
14365
|
+
...base ? [git2.TREE({ ref: base })] : []
|
|
14418
14366
|
],
|
|
14419
14367
|
map: async (filepath, entries) => {
|
|
14420
14368
|
if (filepath === ".")
|
|
@@ -14445,14 +14393,14 @@ async function convergeMerge(params) {
|
|
|
14445
14393
|
authorEmail: params.authorEmail,
|
|
14446
14394
|
cache: cache2
|
|
14447
14395
|
});
|
|
14448
|
-
const ourTip = await
|
|
14396
|
+
const ourTip = await git2.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14449
14397
|
const combined = new Set;
|
|
14450
14398
|
const keptBothFiles = [];
|
|
14451
14399
|
let tipBeforeMerge = ourTip;
|
|
14452
14400
|
const attempt = async () => {
|
|
14453
|
-
tipBeforeMerge = await
|
|
14401
|
+
tipBeforeMerge = await git2.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14454
14402
|
step("merge", { local: tipBeforeMerge, fetched: theirs });
|
|
14455
|
-
return
|
|
14403
|
+
return git2.merge({
|
|
14456
14404
|
fs: gitFs,
|
|
14457
14405
|
dir,
|
|
14458
14406
|
cache: cache2,
|
|
@@ -14539,37 +14487,237 @@ async function convergeMerge(params) {
|
|
|
14539
14487
|
}
|
|
14540
14488
|
step("checkout");
|
|
14541
14489
|
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
|
|
14490
|
+
await git2.checkout({ fs: gitFs, dir, cache: cache2, ref: branch, force: false });
|
|
14491
|
+
} catch (e) {
|
|
14492
|
+
if (!isCheckoutConflict(e))
|
|
14493
|
+
throw e;
|
|
14494
|
+
logger.warn("checkout", "checkout refused; restoring branch", errorLogData(e));
|
|
14495
|
+
step("rollback", { local: tipBeforeMerge });
|
|
14496
|
+
await git2.writeRef({
|
|
14497
|
+
fs: gitFs,
|
|
14498
|
+
dir,
|
|
14499
|
+
ref: await git2.expandRef({ fs: gitFs, dir, ref: branch }),
|
|
14500
|
+
value: tipBeforeMerge,
|
|
14501
|
+
force: true
|
|
14502
|
+
});
|
|
14503
|
+
step("checkout");
|
|
14504
|
+
throw e;
|
|
14505
|
+
}
|
|
14506
|
+
if (postRestore.length > 0) {
|
|
14507
|
+
for (const r of postRestore) {
|
|
14508
|
+
step("restore-conflict-file", { filepath: r.path });
|
|
14509
|
+
await writeTreeFile(dir, r.path, r.bytes);
|
|
14510
|
+
}
|
|
14511
|
+
step("snapshot-restored");
|
|
14512
|
+
if (await hasPendingChanges(dir))
|
|
14513
|
+
await snapshot(CONVERGE_RESTORE_MESSAGE);
|
|
14514
|
+
}
|
|
14515
|
+
step("merged-ref");
|
|
14516
|
+
return {
|
|
14517
|
+
oid: await git2.resolveRef({ fs: gitFs, dir, ref: branch }),
|
|
14518
|
+
combinedFiles: [...combined].sort(),
|
|
14519
|
+
keptBothFiles
|
|
14520
|
+
};
|
|
14521
|
+
}
|
|
14522
|
+
|
|
14523
|
+
// src/lib/remote-auth/transport.ts
|
|
14524
|
+
import git3 from "isomorphic-git";
|
|
14525
|
+
async function repoDirFor(projectDir) {
|
|
14526
|
+
const source = await detectProjectSource(projectDir);
|
|
14527
|
+
if (source.type === "local-git-folder")
|
|
14528
|
+
return gitScopeFor(source);
|
|
14529
|
+
return projectDir;
|
|
14530
|
+
}
|
|
14531
|
+
function isCredentialTransmissionSafe(url) {
|
|
14532
|
+
try {
|
|
14533
|
+
const u = new URL(url);
|
|
14534
|
+
if (u.protocol === "https:")
|
|
14535
|
+
return true;
|
|
14536
|
+
if (u.protocol === "http:") {
|
|
14537
|
+
const host = u.hostname.toLowerCase();
|
|
14538
|
+
return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
|
|
14539
|
+
}
|
|
14540
|
+
return false;
|
|
14541
|
+
} catch {
|
|
14542
|
+
return false;
|
|
14543
|
+
}
|
|
14544
|
+
}
|
|
14545
|
+
|
|
14546
|
+
class InsecureTransportError extends Error {
|
|
14547
|
+
code = "InsecureTransport";
|
|
14548
|
+
constructor() {
|
|
14549
|
+
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.");
|
|
14550
|
+
this.name = "InsecureTransportError";
|
|
14551
|
+
}
|
|
14552
|
+
}
|
|
14553
|
+
function isInsecureTransportError(e) {
|
|
14554
|
+
return e?.code === "InsecureTransport";
|
|
14555
|
+
}
|
|
14556
|
+
function onAuthFor(credential) {
|
|
14557
|
+
if (!credential)
|
|
14558
|
+
return {};
|
|
14559
|
+
return {
|
|
14560
|
+
onAuth: (url) => {
|
|
14561
|
+
if (!isCredentialTransmissionSafe(url))
|
|
14562
|
+
throw new InsecureTransportError;
|
|
14563
|
+
return {
|
|
14564
|
+
username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
|
|
14565
|
+
password: credential.token
|
|
14566
|
+
};
|
|
14567
|
+
}
|
|
14568
|
+
};
|
|
14569
|
+
}
|
|
14570
|
+
async function resolveTransport(dir, options) {
|
|
14571
|
+
const origin = await syncRemoteFor(dir);
|
|
14572
|
+
if (!origin?.url)
|
|
14573
|
+
throw new Error(MSG_NO_REMOTE);
|
|
14574
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(origin.url);
|
|
14575
|
+
let parsed;
|
|
14576
|
+
try {
|
|
14577
|
+
parsed = new URL(cleanUrl);
|
|
14578
|
+
} catch {
|
|
14579
|
+
throw new Error(MSG_SSH_REMOTE);
|
|
14580
|
+
}
|
|
14581
|
+
if (!/^https?:$/.test(parsed.protocol))
|
|
14582
|
+
throw new Error(MSG_SSH_REMOTE);
|
|
14583
|
+
const host = credentialHostKey(cleanUrl);
|
|
14584
|
+
let credential = options.credential ?? undefined;
|
|
14585
|
+
if (!credential && options.tokenStore) {
|
|
14586
|
+
try {
|
|
14587
|
+
credential = await options.tokenStore.get(host) ?? undefined;
|
|
14588
|
+
} catch {
|
|
14589
|
+
credential = undefined;
|
|
14590
|
+
}
|
|
14591
|
+
}
|
|
14592
|
+
if (!credential && urlCredential)
|
|
14593
|
+
credential = urlCredential;
|
|
14594
|
+
return {
|
|
14595
|
+
remote: origin.remote,
|
|
14596
|
+
url: cleanUrl,
|
|
14597
|
+
host,
|
|
14598
|
+
...credential ? { credential } : {}
|
|
14599
|
+
};
|
|
14600
|
+
}
|
|
14601
|
+
function classifyTransportFailure(e) {
|
|
14602
|
+
if (isInsecureTransportError(e))
|
|
14603
|
+
return "insecure_transport";
|
|
14604
|
+
const err = e;
|
|
14605
|
+
if (err?.code === "HttpError") {
|
|
14606
|
+
const status = err.data?.statusCode;
|
|
14607
|
+
if (status === 401 || status === 403 || status === 404)
|
|
14608
|
+
return "auth_required";
|
|
14609
|
+
}
|
|
14610
|
+
const msg = `${err?.message ?? String(e)} ${err?.data?.prettyDetails ?? ""}`;
|
|
14611
|
+
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)) {
|
|
14612
|
+
return "auth_required";
|
|
14613
|
+
}
|
|
14614
|
+
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network|fetch failed|couldn't reach|socket hang ?up/i.test(msg)) {
|
|
14615
|
+
return "network_unavailable";
|
|
14616
|
+
}
|
|
14617
|
+
return null;
|
|
14618
|
+
}
|
|
14619
|
+
function failureOutcome(e, snapshotId) {
|
|
14620
|
+
const kind = classifyTransportFailure(e);
|
|
14621
|
+
const base = snapshotId ? { snapshotId } : {};
|
|
14622
|
+
if (kind === "auth_required")
|
|
14623
|
+
return { status: "auth", message: MSG_AUTH, ...base };
|
|
14624
|
+
if (kind === "network_unavailable")
|
|
14625
|
+
return { status: "offline", message: MSG_OFFLINE, ...base };
|
|
14626
|
+
if (kind === "insecure_transport") {
|
|
14627
|
+
return { status: "error", message: MSG_INSECURE_TRANSPORT, ...base };
|
|
14628
|
+
}
|
|
14629
|
+
return {
|
|
14630
|
+
status: "error",
|
|
14631
|
+
message: "Syncing didn't complete. Your work is saved on this computer — please try again.",
|
|
14632
|
+
...base
|
|
14633
|
+
};
|
|
14634
|
+
}
|
|
14635
|
+
function setupErrorMessage(e) {
|
|
14636
|
+
if (e instanceof Error && (e.message === MSG_NO_REMOTE || e.message === MSG_SSH_REMOTE || e.message === MSG_NO_BRANCH)) {
|
|
14637
|
+
return e.message;
|
|
14638
|
+
}
|
|
14639
|
+
return null;
|
|
14640
|
+
}
|
|
14641
|
+
async function snapshotBeforeAction(args) {
|
|
14642
|
+
const { projectDir, dir, cache: cache2 } = args;
|
|
14643
|
+
const hasChanges = await hasPendingChanges(dir, cache2);
|
|
14644
|
+
const staleStaging = gitFs.existsSync(snapshotStagingMarkerPath(dir));
|
|
14645
|
+
if (!hasChanges && !staleStaging)
|
|
14646
|
+
return;
|
|
14647
|
+
const snap = await snapshotWorkingTreeUnlocked({
|
|
14648
|
+
projectDir,
|
|
14649
|
+
repoRoot: dir,
|
|
14650
|
+
message: args.message?.trim() || SYNC_SNAPSHOT_MESSAGE,
|
|
14651
|
+
authorName: args.authorName,
|
|
14652
|
+
authorEmail: args.authorEmail,
|
|
14653
|
+
cache: cache2
|
|
14654
|
+
});
|
|
14655
|
+
return snap.id;
|
|
14656
|
+
}
|
|
14657
|
+
async function currentBranchOrThrow(dir) {
|
|
14658
|
+
const branch = await git3.currentBranch({ fs: gitFs, dir });
|
|
14659
|
+
if (!branch)
|
|
14660
|
+
throw new Error(MSG_NO_BRANCH);
|
|
14661
|
+
return branch;
|
|
14662
|
+
}
|
|
14663
|
+
async function resolveRefOrNull(dir, ref) {
|
|
14664
|
+
try {
|
|
14665
|
+
return await git3.resolveRef({ fs: gitFs, dir, ref });
|
|
14666
|
+
} catch {
|
|
14667
|
+
return null;
|
|
14668
|
+
}
|
|
14669
|
+
}
|
|
14670
|
+
async function guardTrackingRef(dir, ref, cache2, fn) {
|
|
14671
|
+
const before = await resolveRefOrNull(dir, ref);
|
|
14672
|
+
try {
|
|
14673
|
+
return await fn();
|
|
14674
|
+
} catch (e) {
|
|
14675
|
+
try {
|
|
14676
|
+
const after = await resolveRefOrNull(dir, ref);
|
|
14677
|
+
if (after && after !== before) {
|
|
14678
|
+
const landed = await git3.readObject({ fs: gitFs, dir, oid: after, cache: cache2 }).then(() => true, () => false);
|
|
14679
|
+
if (!landed) {
|
|
14680
|
+
if (before)
|
|
14681
|
+
await git3.writeRef({ fs: gitFs, dir, ref, value: before, force: true });
|
|
14682
|
+
else
|
|
14683
|
+
await git3.deleteRef({ fs: gitFs, dir, ref });
|
|
14684
|
+
}
|
|
14685
|
+
}
|
|
14686
|
+
} catch {}
|
|
14687
|
+
throw e;
|
|
14688
|
+
}
|
|
14689
|
+
}
|
|
14690
|
+
async function fetchRemoteTip(dir, branch, transport, http3, cache2) {
|
|
14691
|
+
try {
|
|
14692
|
+
const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
|
|
14693
|
+
const result = await guardTrackingRef(dir, trackingRef, cache2, () => git3.fetch({
|
|
14549
14694
|
fs: gitFs,
|
|
14695
|
+
http: http3,
|
|
14550
14696
|
dir,
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14697
|
+
cache: cache2,
|
|
14698
|
+
remote: transport.remote,
|
|
14699
|
+
ref: trackingRef,
|
|
14700
|
+
remoteRef: branch,
|
|
14701
|
+
singleBranch: true,
|
|
14702
|
+
tags: false,
|
|
14703
|
+
...onAuthFor(transport.credential)
|
|
14704
|
+
}));
|
|
14705
|
+
return result.fetchHead ?? null;
|
|
14706
|
+
} catch (e) {
|
|
14707
|
+
const code = e?.code;
|
|
14708
|
+
if (code === "NoRefspecError")
|
|
14709
|
+
return null;
|
|
14710
|
+
if (code === "NotFoundError" || e?.data?.statusCode === 404) {
|
|
14711
|
+
const err = new Error("The online repository couldn't be accessed with the saved connection. Reconnect and try again.", { cause: e });
|
|
14712
|
+
err.code = "HttpError";
|
|
14713
|
+
err.data = { statusCode: 401 };
|
|
14714
|
+
throw err;
|
|
14562
14715
|
}
|
|
14563
|
-
|
|
14564
|
-
if (await hasPendingChanges(dir))
|
|
14565
|
-
await snapshot(CONVERGE_RESTORE_MESSAGE);
|
|
14716
|
+
throw e;
|
|
14566
14717
|
}
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
combinedFiles: [...combined].sort(),
|
|
14571
|
-
keptBothFiles
|
|
14572
|
-
};
|
|
14718
|
+
}
|
|
14719
|
+
function short(oid) {
|
|
14720
|
+
return oid ? oid.slice(0, 8) : "none";
|
|
14573
14721
|
}
|
|
14574
14722
|
// src/lib/remote-auth/sync.ts
|
|
14575
14723
|
function isPushRejected(e) {
|
|
@@ -14594,10 +14742,10 @@ function isDamagedObjectError(e) {
|
|
|
14594
14742
|
}
|
|
14595
14743
|
async function historyUnreadable(dir) {
|
|
14596
14744
|
try {
|
|
14597
|
-
const oid = await
|
|
14598
|
-
await
|
|
14599
|
-
await
|
|
14600
|
-
await
|
|
14745
|
+
const oid = await git4.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
|
|
14746
|
+
await git4.readCommit({ fs: gitFs, dir, oid });
|
|
14747
|
+
await git4.listFiles({ fs: gitFs, dir });
|
|
14748
|
+
await git4.listFiles({ fs: gitFs, dir, ref: "HEAD" });
|
|
14601
14749
|
return false;
|
|
14602
14750
|
} catch {
|
|
14603
14751
|
return true;
|
|
@@ -14692,7 +14840,7 @@ async function syncProject(options) {
|
|
|
14692
14840
|
step("fetch", { attempt: attempt + 1, branch, remote: transport.remote });
|
|
14693
14841
|
const remoteTip = await fetchRemoteTip(dir, branch, transport, http3, cache2);
|
|
14694
14842
|
step("local-ref", { fetched: remoteTip ?? "none" });
|
|
14695
|
-
let localTip = await
|
|
14843
|
+
let localTip = await git4.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14696
14844
|
if (push || remoteTip !== null && remoteTip !== localTip) {
|
|
14697
14845
|
step("snapshot-after-fetch");
|
|
14698
14846
|
const lateSnapshot = await snapshotBeforeAction({
|
|
@@ -14705,7 +14853,7 @@ async function syncProject(options) {
|
|
|
14705
14853
|
});
|
|
14706
14854
|
if (lateSnapshot) {
|
|
14707
14855
|
snapshotId = lateSnapshot;
|
|
14708
|
-
localTip = await
|
|
14856
|
+
localTip = await git4.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
14709
14857
|
}
|
|
14710
14858
|
}
|
|
14711
14859
|
logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
|
|
@@ -14739,8 +14887,8 @@ async function syncProject(options) {
|
|
|
14739
14887
|
pulled = true;
|
|
14740
14888
|
step("compare-trees");
|
|
14741
14889
|
const [before, after] = await Promise.all([
|
|
14742
|
-
|
|
14743
|
-
|
|
14890
|
+
git4.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
|
|
14891
|
+
git4.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
|
|
14744
14892
|
]);
|
|
14745
14893
|
filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
|
|
14746
14894
|
for (const f of converge.combinedFiles)
|
|
@@ -14770,7 +14918,7 @@ async function syncProject(options) {
|
|
|
14770
14918
|
}
|
|
14771
14919
|
step("push", { branch, local: tip, fetched: remoteTip ?? "none" });
|
|
14772
14920
|
try {
|
|
14773
|
-
await
|
|
14921
|
+
await git4.push({
|
|
14774
14922
|
fs: gitFs,
|
|
14775
14923
|
http: http3,
|
|
14776
14924
|
dir,
|
|
@@ -14812,50 +14960,562 @@ async function syncProject(options) {
|
|
|
14812
14960
|
});
|
|
14813
14961
|
};
|
|
14814
14962
|
try {
|
|
14815
|
-
return finish(await run2());
|
|
14816
|
-
} catch (e) {
|
|
14817
|
-
logger.warn(stage, `${stage} failed`, errorLogData(e));
|
|
14818
|
-
return finish(failureOutcome(e));
|
|
14963
|
+
return finish(await run2());
|
|
14964
|
+
} catch (e) {
|
|
14965
|
+
logger.warn(stage, `${stage} failed`, errorLogData(e));
|
|
14966
|
+
return finish(failureOutcome(e));
|
|
14967
|
+
}
|
|
14968
|
+
}
|
|
14969
|
+
async function refreshRemoteCopies(options) {
|
|
14970
|
+
const http3 = options.httpClient ?? defaultGitHttp;
|
|
14971
|
+
try {
|
|
14972
|
+
const dir = await repoDirFor(options.projectDir);
|
|
14973
|
+
const transport = await resolveTransport(dir, {
|
|
14974
|
+
...options.credential ? { credential: options.credential } : {},
|
|
14975
|
+
...options.tokenStore ? { tokenStore: options.tokenStore } : {}
|
|
14976
|
+
});
|
|
14977
|
+
await git4.fetch({
|
|
14978
|
+
fs: gitFs,
|
|
14979
|
+
http: http3,
|
|
14980
|
+
dir,
|
|
14981
|
+
remote: transport.remote,
|
|
14982
|
+
singleBranch: false,
|
|
14983
|
+
prune: true,
|
|
14984
|
+
tags: false,
|
|
14985
|
+
...onAuthFor(transport.credential)
|
|
14986
|
+
});
|
|
14987
|
+
return { refreshed: true };
|
|
14988
|
+
} catch (e) {
|
|
14989
|
+
const message = e?.message ?? "";
|
|
14990
|
+
if (message === MSG_NO_REMOTE || message === MSG_SSH_REMOTE) {
|
|
14991
|
+
return { refreshed: false, reason: "no-remote" };
|
|
14992
|
+
}
|
|
14993
|
+
const status = e?.data?.statusCode;
|
|
14994
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
14995
|
+
return { refreshed: false, reason: "auth" };
|
|
14996
|
+
}
|
|
14997
|
+
return { refreshed: false, reason: "offline" };
|
|
14998
|
+
}
|
|
14999
|
+
}
|
|
15000
|
+
|
|
15001
|
+
// src/lib/remote-auth/online-backup.ts
|
|
15002
|
+
var API_BASE3 = "https://api.github.com";
|
|
15003
|
+
var REQUEST_TIMEOUT_MS4 = 15000;
|
|
15004
|
+
var MAX_NAME_LENGTH = 100;
|
|
15005
|
+
var MSG = {
|
|
15006
|
+
notConnected: "Connect your GitHub account first (Settings > Accounts), then set up online backup.",
|
|
15007
|
+
auth: "GitHub didn't accept the saved connection. Reconnect GitHub in Settings > Accounts and try again.",
|
|
15008
|
+
offline: "Gutterpress couldn't reach GitHub. Check your internet connection and try again. Nothing was changed.",
|
|
15009
|
+
noHistory: "Turn on version history for this book first. Online backup copies the versions Gutterpress keeps on this computer.",
|
|
15010
|
+
alreadyHasRemote: "This book already has an online copy.",
|
|
15011
|
+
noName: "Give the online copy a name.",
|
|
15012
|
+
badName: "A repository name can only use letters, numbers, hyphens (-), underscores (_) and dots (.), and be at most 100 characters.",
|
|
15013
|
+
notFound: "Gutterpress couldn't find that repository on GitHub, or your account can't see it. Choose another one.",
|
|
15014
|
+
noPermission: "Your GitHub account isn't allowed to add to that repository (it may be archived or read-only). Choose another one.",
|
|
15015
|
+
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.",
|
|
15016
|
+
notEmpty: "That repository already has files in it, and online backup needs an empty one. Choose an empty repository, or create a new one.",
|
|
15017
|
+
pushFailed: "Your book couldn't be copied to GitHub. Your writing is safe on this computer. Please try again."
|
|
15018
|
+
};
|
|
15019
|
+
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.`;
|
|
15020
|
+
|
|
15021
|
+
class BackupFailure extends Error {
|
|
15022
|
+
reason;
|
|
15023
|
+
repository;
|
|
15024
|
+
constructor(reason, message, repository) {
|
|
15025
|
+
super(message);
|
|
15026
|
+
this.reason = reason;
|
|
15027
|
+
this.repository = repository;
|
|
15028
|
+
}
|
|
15029
|
+
}
|
|
15030
|
+
function isValidRepositoryName(name) {
|
|
15031
|
+
return name.length > 0 && name.length <= MAX_NAME_LENGTH && /^[A-Za-z0-9._-]+$/.test(name) && name !== "." && name !== "..";
|
|
15032
|
+
}
|
|
15033
|
+
function toRemoteRepository(r) {
|
|
15034
|
+
return {
|
|
15035
|
+
owner: r.owner.login,
|
|
15036
|
+
name: r.name,
|
|
15037
|
+
fullName: r.full_name,
|
|
15038
|
+
private: r.private,
|
|
15039
|
+
defaultBranch: r.default_branch,
|
|
15040
|
+
htmlUrl: r.html_url
|
|
15041
|
+
};
|
|
15042
|
+
}
|
|
15043
|
+
async function request(fetchImpl, method, path26, token, body) {
|
|
15044
|
+
return withFetchTimeout({ timeoutMs: REQUEST_TIMEOUT_MS4, offlineMessage: MSG.offline }, (signal) => fetchImpl(`${API_BASE3}${path26}`, {
|
|
15045
|
+
method,
|
|
15046
|
+
headers: {
|
|
15047
|
+
...githubApiHeaders(token),
|
|
15048
|
+
...body === undefined ? {} : { "Content-Type": "application/json" }
|
|
15049
|
+
},
|
|
15050
|
+
...body === undefined ? {} : { body: JSON.stringify(body) },
|
|
15051
|
+
signal
|
|
15052
|
+
})).catch((e) => {
|
|
15053
|
+
throw new BackupFailure("offline", e instanceof Error ? e.message : MSG.offline);
|
|
15054
|
+
});
|
|
15055
|
+
}
|
|
15056
|
+
async function createRepository(fetchImpl, token, name, isPrivate) {
|
|
15057
|
+
const res = await request(fetchImpl, "POST", "/user/repos", token, {
|
|
15058
|
+
name,
|
|
15059
|
+
private: isPrivate,
|
|
15060
|
+
auto_init: false,
|
|
15061
|
+
description: "Online backup made with Gutterpress"
|
|
15062
|
+
});
|
|
15063
|
+
if (res.status === 201 || res.status === 200)
|
|
15064
|
+
return await res.json();
|
|
15065
|
+
if (res.status === 401)
|
|
15066
|
+
throw new BackupFailure("auth", MSG.auth);
|
|
15067
|
+
if (res.status === 403)
|
|
15068
|
+
throw new BackupFailure("no-permission", MSG.noCreatePermission);
|
|
15069
|
+
if (res.status === 422) {
|
|
15070
|
+
const body = await res.json().catch(() => ({}));
|
|
15071
|
+
const taken = (body.errors ?? []).some((e) => /already exists/i.test(e.message ?? ""));
|
|
15072
|
+
throw new BackupFailure("name-taken", taken ? nameTaken(name) : MSG.badName);
|
|
15073
|
+
}
|
|
15074
|
+
throw new BackupFailure("push-failed", `GitHub couldn't create the repository (error ${res.status}). Please try again.`);
|
|
15075
|
+
}
|
|
15076
|
+
async function loadEmptyRepository(fetchImpl, credential, owner, name) {
|
|
15077
|
+
const path26 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
|
15078
|
+
const res = await request(fetchImpl, "GET", path26, credential.token);
|
|
15079
|
+
if (res.status === 401)
|
|
15080
|
+
throw new BackupFailure("auth", MSG.auth);
|
|
15081
|
+
if (res.status === 404)
|
|
15082
|
+
throw new BackupFailure("not-found", MSG.notFound);
|
|
15083
|
+
if (res.status === 403)
|
|
15084
|
+
throw new BackupFailure("no-permission", MSG.noPermission);
|
|
15085
|
+
if (!res.ok) {
|
|
15086
|
+
throw new BackupFailure("push-failed", `GitHub returned an unexpected error (error ${res.status}). Please try again.`);
|
|
15087
|
+
}
|
|
15088
|
+
const repo = await res.json();
|
|
15089
|
+
if (repo.archived || repo.permissions?.push === false) {
|
|
15090
|
+
throw new BackupFailure("no-permission", MSG.noPermission);
|
|
15091
|
+
}
|
|
15092
|
+
const branches = await listGitHubBranches(credential, owner, name, { fetchImpl }).catch((e) => {
|
|
15093
|
+
const msg = e instanceof Error ? e.message : "";
|
|
15094
|
+
throw new BackupFailure(/reconnect/i.test(msg) ? "auth" : /reach/i.test(msg) ? "offline" : "push-failed", msg || MSG.pushFailed);
|
|
15095
|
+
});
|
|
15096
|
+
if (branches.length > 0)
|
|
15097
|
+
throw new BackupFailure("not-empty", MSG.notEmpty, toRemoteRepository(repo));
|
|
15098
|
+
return repo;
|
|
15099
|
+
}
|
|
15100
|
+
async function run2(options) {
|
|
15101
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
15102
|
+
const credential = options.credential ?? await options.tokenStore?.get(GITHUB_HOST).catch(() => null) ?? undefined;
|
|
15103
|
+
if (!credential)
|
|
15104
|
+
throw new BackupFailure("not-connected", MSG.notConnected);
|
|
15105
|
+
const source = await detectProjectSource(options.projectDir);
|
|
15106
|
+
if (source.type !== "local-git-folder")
|
|
15107
|
+
throw new BackupFailure("no-history", MSG.noHistory);
|
|
15108
|
+
if (source.hasRemote)
|
|
15109
|
+
throw new BackupFailure("already-has-remote", MSG.alreadyHasRemote);
|
|
15110
|
+
const dir = await repoDirFor(options.projectDir);
|
|
15111
|
+
const head = await git5.resolveRef({ fs: gitFs, dir, ref: "HEAD" }).catch(() => null);
|
|
15112
|
+
if (!head)
|
|
15113
|
+
throw new BackupFailure("no-history", MSG.noHistory);
|
|
15114
|
+
const { target } = options;
|
|
15115
|
+
let repo;
|
|
15116
|
+
if (target.kind === "create") {
|
|
15117
|
+
const name = target.name.trim();
|
|
15118
|
+
if (!name)
|
|
15119
|
+
throw new BackupFailure("invalid-name", MSG.noName);
|
|
15120
|
+
if (!isValidRepositoryName(name))
|
|
15121
|
+
throw new BackupFailure("invalid-name", MSG.badName);
|
|
15122
|
+
repo = await createRepository(fetchImpl, credential.token, name, target.private ?? true);
|
|
15123
|
+
} else {
|
|
15124
|
+
repo = await loadEmptyRepository(fetchImpl, credential, target.owner, target.name);
|
|
15125
|
+
}
|
|
15126
|
+
const repository = toRemoteRepository(repo);
|
|
15127
|
+
if (!/^https?:\/\//.test(repo.clone_url)) {
|
|
15128
|
+
throw new BackupFailure("push-failed", MSG.pushFailed, repository);
|
|
15129
|
+
}
|
|
15130
|
+
await withRepoLock(dir, () => git5.addRemote({ fs: gitFs, dir, remote: "origin", url: repo.clone_url }));
|
|
15131
|
+
const sync = await syncProject({
|
|
15132
|
+
projectDir: options.projectDir,
|
|
15133
|
+
credential,
|
|
15134
|
+
...options.authorName ? { authorName: options.authorName } : {},
|
|
15135
|
+
...options.authorEmail ? { authorEmail: options.authorEmail } : {},
|
|
15136
|
+
...options.httpClient ? { httpClient: options.httpClient } : {},
|
|
15137
|
+
...options.logFile ? { logFile: options.logFile } : {}
|
|
15138
|
+
});
|
|
15139
|
+
if (sync.status === "synced" || sync.status === "up-to-date") {
|
|
15140
|
+
return { status: "connected", repository, sync };
|
|
15141
|
+
}
|
|
15142
|
+
await withRepoLock(dir, () => git5.deleteRemote({ fs: gitFs, dir, remote: "origin" })).catch(() => {});
|
|
15143
|
+
if (sync.status === "auth")
|
|
15144
|
+
throw new BackupFailure("auth", sync.message, repository);
|
|
15145
|
+
if (sync.status === "offline")
|
|
15146
|
+
throw new BackupFailure("offline", sync.message, repository);
|
|
15147
|
+
throw new BackupFailure("push-failed", sync.message || MSG.pushFailed, repository);
|
|
15148
|
+
}
|
|
15149
|
+
async function setUpOnlineBackup(options) {
|
|
15150
|
+
try {
|
|
15151
|
+
return await run2(options);
|
|
15152
|
+
} catch (e) {
|
|
15153
|
+
if (e instanceof BackupFailure) {
|
|
15154
|
+
return {
|
|
15155
|
+
status: "failed",
|
|
15156
|
+
reason: e.reason,
|
|
15157
|
+
message: e.message,
|
|
15158
|
+
...e.repository ? { repository: e.repository } : {}
|
|
15159
|
+
};
|
|
15160
|
+
}
|
|
15161
|
+
throw e;
|
|
15162
|
+
}
|
|
15163
|
+
}
|
|
15164
|
+
// src/lib/remote-auth/clone.ts
|
|
15165
|
+
import { mkdir as mkdir15, readdir as readdir9, rm as rm6 } from "node:fs/promises";
|
|
15166
|
+
import path26 from "node:path";
|
|
15167
|
+
import git6 from "isomorphic-git";
|
|
15168
|
+
function sanitizeCloneFolderName(name) {
|
|
15169
|
+
return String(name ?? "").replace(/[\\/]/g, "-").replace(/^\.+/, "").trim();
|
|
15170
|
+
}
|
|
15171
|
+
async function assertCloneTarget(dir) {
|
|
15172
|
+
try {
|
|
15173
|
+
const entries = await readdir9(dir);
|
|
15174
|
+
if (entries.length > 0) {
|
|
15175
|
+
throw new Error("That folder already has files in it. Choose an empty folder for the book.");
|
|
15176
|
+
}
|
|
15177
|
+
} catch (e) {
|
|
15178
|
+
if (e.code === "ENOENT")
|
|
15179
|
+
return;
|
|
15180
|
+
throw e;
|
|
15181
|
+
}
|
|
15182
|
+
}
|
|
15183
|
+
function friendlyCloneError(e) {
|
|
15184
|
+
if (isInsecureTransportError(e)) {
|
|
15185
|
+
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 });
|
|
15186
|
+
}
|
|
15187
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
15188
|
+
if (/401|403|auth|credential/i.test(msg)) {
|
|
15189
|
+
return new Error("GitHub didn't accept the connection for this repository. Reconnect GitHub and try again.", { cause: e });
|
|
15190
|
+
}
|
|
15191
|
+
if (/404|not found/i.test(msg)) {
|
|
15192
|
+
return new Error("That repository couldn't be found. It may have been renamed or you may no longer have access.", { cause: e });
|
|
15193
|
+
}
|
|
15194
|
+
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
|
|
15195
|
+
return new Error(OFFLINE_MESSAGE, { cause: e });
|
|
15196
|
+
}
|
|
15197
|
+
if (e instanceof Error && /already has files/i.test(e.message)) {
|
|
15198
|
+
return e;
|
|
15199
|
+
}
|
|
15200
|
+
return new Error("The book couldn't be downloaded from GitHub. Please try again.", { cause: e });
|
|
15201
|
+
}
|
|
15202
|
+
async function cloneRepository(options) {
|
|
15203
|
+
const { dir, onProgress, tokenStore } = options;
|
|
15204
|
+
if (!path26.isAbsolute(dir)) {
|
|
15205
|
+
throw new Error("cloneRepository requires an absolute destination path.");
|
|
15206
|
+
}
|
|
15207
|
+
let parsed;
|
|
15208
|
+
try {
|
|
15209
|
+
parsed = new URL(options.url);
|
|
15210
|
+
} catch {
|
|
15211
|
+
throw new Error("That repository address is not a valid web URL.");
|
|
15212
|
+
}
|
|
15213
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
15214
|
+
throw new Error("Only HTTPS repository addresses are supported. SSH addresses (git@…) can't be used here.");
|
|
15215
|
+
}
|
|
15216
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
15217
|
+
const credential = options.credential ?? urlCredential;
|
|
15218
|
+
if (urlCredential && tokenStore && !await tokenStore.get(urlCredential.host)) {
|
|
15219
|
+
await tokenStore.set(urlCredential.host, urlCredential);
|
|
15220
|
+
}
|
|
15221
|
+
return withRepoLock(dir, async () => {
|
|
15222
|
+
await assertCloneTarget(dir);
|
|
15223
|
+
const dirExistedBefore = gitFs.existsSync(dir);
|
|
15224
|
+
await mkdir15(dir, { recursive: true });
|
|
15225
|
+
try {
|
|
15226
|
+
await git6.clone({
|
|
15227
|
+
fs: gitFs,
|
|
15228
|
+
http: options.httpClient ?? defaultGitHttp,
|
|
15229
|
+
dir,
|
|
15230
|
+
url: cleanUrl,
|
|
15231
|
+
singleBranch: true,
|
|
15232
|
+
...options.branch ? { ref: options.branch } : {},
|
|
15233
|
+
...options.depth ? { depth: options.depth } : {},
|
|
15234
|
+
...onAuthFor(credential),
|
|
15235
|
+
...onProgress ? {
|
|
15236
|
+
onProgress: (p) => onProgress({
|
|
15237
|
+
phase: p.phase,
|
|
15238
|
+
loaded: p.loaded,
|
|
15239
|
+
...p.total ? { total: p.total } : {}
|
|
15240
|
+
})
|
|
15241
|
+
} : {}
|
|
15242
|
+
});
|
|
15243
|
+
} catch (e) {
|
|
15244
|
+
const friendly = friendlyCloneError(e);
|
|
15245
|
+
if (!dirExistedBefore) {
|
|
15246
|
+
try {
|
|
15247
|
+
await rm6(dir, { recursive: true, force: true });
|
|
15248
|
+
} catch {
|
|
15249
|
+
throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
|
|
15250
|
+
}
|
|
15251
|
+
}
|
|
15252
|
+
throw friendly;
|
|
15253
|
+
}
|
|
15254
|
+
let branch;
|
|
15255
|
+
try {
|
|
15256
|
+
branch = await git6.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
15257
|
+
} catch {
|
|
15258
|
+
branch = undefined;
|
|
15259
|
+
}
|
|
15260
|
+
return { projectDir: dir, ...branch ? { branch } : {} };
|
|
15261
|
+
});
|
|
15262
|
+
}
|
|
15263
|
+
// src/lib/remote-auth/test-access.ts
|
|
15264
|
+
import git7 from "isomorphic-git";
|
|
15265
|
+
import httpNode2 from "isomorphic-git/http/node";
|
|
15266
|
+
function isSshRemoteUrl(url) {
|
|
15267
|
+
const trimmed = url.trim();
|
|
15268
|
+
if (/^ssh:\/\//i.test(trimmed))
|
|
15269
|
+
return true;
|
|
15270
|
+
return /^[\w.-]+@[\w.][\w.-]*:/.test(trimmed) || /^[\w.-]+@\[/.test(trimmed);
|
|
15271
|
+
}
|
|
15272
|
+
var FAILURE_MESSAGES = {
|
|
15273
|
+
auth: "The Git server didn't accept the saved connection. Reconnect to this server with a current access token and try again.",
|
|
15274
|
+
"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.",
|
|
15275
|
+
unreachable: "Couldn't reach the Git server. Check your internet connection (and VPN, if this is a private server), then try again.",
|
|
15276
|
+
"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.",
|
|
15277
|
+
"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.",
|
|
15278
|
+
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).",
|
|
15279
|
+
unknown: "The connection test failed unexpectedly. The server may not be a Git server, or it may be temporarily unavailable."
|
|
15280
|
+
};
|
|
15281
|
+
function failure(reason) {
|
|
15282
|
+
return { ok: false, reason, message: FAILURE_MESSAGES[reason] };
|
|
15283
|
+
}
|
|
15284
|
+
function classifyRemoteAccessError(e) {
|
|
15285
|
+
if (isInsecureTransportError(e))
|
|
15286
|
+
return failure("insecure-transport");
|
|
15287
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
15288
|
+
if (/401|403|auth|credential|denied|cancel/i.test(msg))
|
|
15289
|
+
return failure("auth");
|
|
15290
|
+
if (/404|not found/i.test(msg))
|
|
15291
|
+
return failure("not-found");
|
|
15292
|
+
if (/certificate|self.signed|unable_to_verify|cert_|tls|ssl/i.test(msg) && !/fetch failed/i.test(msg)) {
|
|
15293
|
+
return failure("tls");
|
|
15294
|
+
}
|
|
15295
|
+
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|EHOSTUNREACH|network|fetch failed|timed out/i.test(msg)) {
|
|
15296
|
+
return failure("unreachable");
|
|
15297
|
+
}
|
|
15298
|
+
return failure("unknown");
|
|
15299
|
+
}
|
|
15300
|
+
async function testRemoteAccess(options) {
|
|
15301
|
+
if (isSshRemoteUrl(options.url))
|
|
15302
|
+
return failure("ssh-unsupported");
|
|
15303
|
+
let parsed;
|
|
15304
|
+
try {
|
|
15305
|
+
parsed = new URL(options.url);
|
|
15306
|
+
} catch {
|
|
15307
|
+
return failure("unknown");
|
|
15308
|
+
}
|
|
15309
|
+
if (!/^https?:$/.test(parsed.protocol))
|
|
15310
|
+
return failure("ssh-unsupported");
|
|
15311
|
+
const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
|
|
15312
|
+
const credential = options.credential ?? urlCredential;
|
|
15313
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
15314
|
+
let timer;
|
|
15315
|
+
const timeout = new Promise((resolve9) => {
|
|
15316
|
+
timer = setTimeout(() => resolve9(failure("unreachable")), timeoutMs);
|
|
15317
|
+
});
|
|
15318
|
+
const probe = (async () => {
|
|
15319
|
+
try {
|
|
15320
|
+
const refs = await git7.listServerRefs({
|
|
15321
|
+
http: options.httpClient ?? httpNode2,
|
|
15322
|
+
url: cleanUrl,
|
|
15323
|
+
protocolVersion: 1,
|
|
15324
|
+
symrefs: true,
|
|
15325
|
+
...onAuthFor(credential),
|
|
15326
|
+
...credential ? { onAuthFailure: () => ({ cancel: true }) } : {}
|
|
15327
|
+
});
|
|
15328
|
+
const head = refs.find((r) => r.ref === "HEAD" && r.target);
|
|
15329
|
+
const defaultBranch = head?.target?.replace(/^refs\/heads\//, "");
|
|
15330
|
+
return {
|
|
15331
|
+
ok: true,
|
|
15332
|
+
refCount: refs.filter((r) => r.ref !== "HEAD").length,
|
|
15333
|
+
...defaultBranch ? { defaultBranch } : {}
|
|
15334
|
+
};
|
|
15335
|
+
} catch (e) {
|
|
15336
|
+
return classifyRemoteAccessError(e);
|
|
15337
|
+
}
|
|
15338
|
+
})();
|
|
15339
|
+
try {
|
|
15340
|
+
return await Promise.race([probe, timeout]);
|
|
15341
|
+
} finally {
|
|
15342
|
+
clearTimeout(timer);
|
|
14819
15343
|
}
|
|
14820
15344
|
}
|
|
14821
|
-
|
|
14822
|
-
|
|
15345
|
+
// src/lib/remote-auth/generic-auth.ts
|
|
15346
|
+
function normalizeForgeHost(input) {
|
|
15347
|
+
return credentialHostKey(input);
|
|
15348
|
+
}
|
|
15349
|
+
function knownForgeTokenUrl(host) {
|
|
15350
|
+
const h = normalizeForgeHost(host);
|
|
15351
|
+
if (!h)
|
|
15352
|
+
return null;
|
|
15353
|
+
const name = h.split(":")[0];
|
|
15354
|
+
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
15355
|
+
return null;
|
|
15356
|
+
if (name === "bitbucket.org") {
|
|
15357
|
+
return "https://bitbucket.org/account/settings/app-passwords/";
|
|
15358
|
+
}
|
|
15359
|
+
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com")) {
|
|
15360
|
+
return "https://dev.azure.com/_usersSettings/tokens";
|
|
15361
|
+
}
|
|
15362
|
+
if (name.includes("gitlab")) {
|
|
15363
|
+
return `https://${h}/-/user_settings/personal_access_tokens`;
|
|
15364
|
+
}
|
|
15365
|
+
if (name.includes("gitea") || name.includes("forgejo")) {
|
|
15366
|
+
return `https://${h}/user/settings/applications`;
|
|
15367
|
+
}
|
|
15368
|
+
return null;
|
|
15369
|
+
}
|
|
15370
|
+
function connectError(result, host) {
|
|
15371
|
+
switch (result.reason) {
|
|
15372
|
+
case "auth":
|
|
15373
|
+
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.");
|
|
15374
|
+
case "ssh-unsupported":
|
|
15375
|
+
return new Error("Enter the server's web address (starting with https://), not an SSH address (git@…).");
|
|
15376
|
+
case "unreachable":
|
|
15377
|
+
return new Error(`Couldn't reach ${host}. Check the server address and your connection, then try again.`);
|
|
15378
|
+
case "tls":
|
|
15379
|
+
return new Error(result.message);
|
|
15380
|
+
case "not-found":
|
|
15381
|
+
return new Error("The server answered, but that repository wasn't found. Check the repository address and that the token has access to it.");
|
|
15382
|
+
default:
|
|
15383
|
+
return new Error(`Couldn't verify the connection to ${host}. Check the server address and try again.`);
|
|
15384
|
+
}
|
|
15385
|
+
}
|
|
15386
|
+
async function connectGenericHost(input, options = {}) {
|
|
15387
|
+
const host = normalizeForgeHost(input.host);
|
|
15388
|
+
if (!host) {
|
|
15389
|
+
throw new Error("Enter your Git server's web address (like https://git.example.com).");
|
|
15390
|
+
}
|
|
15391
|
+
const token = input.token?.trim();
|
|
15392
|
+
if (!token) {
|
|
15393
|
+
throw new Error("Paste an access token from your Git server to connect.");
|
|
15394
|
+
}
|
|
15395
|
+
const username = input.username?.trim() || undefined;
|
|
15396
|
+
const candidate = {
|
|
15397
|
+
host,
|
|
15398
|
+
kind: "token",
|
|
15399
|
+
token,
|
|
15400
|
+
...username ? { username } : {},
|
|
15401
|
+
label: username ? `${host} — ${username}` : host,
|
|
15402
|
+
createdAt: Date.now()
|
|
15403
|
+
};
|
|
15404
|
+
const probeUrl = input.repoUrl?.trim() || `https://${host}/`;
|
|
15405
|
+
const result = await testRemoteAccess({
|
|
15406
|
+
url: probeUrl,
|
|
15407
|
+
credential: candidate,
|
|
15408
|
+
...options.httpClient ? { httpClient: options.httpClient } : {},
|
|
15409
|
+
...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
|
|
15410
|
+
});
|
|
15411
|
+
if (!result.ok) {
|
|
15412
|
+
const rootProbe = !input.repoUrl?.trim();
|
|
15413
|
+
const inconclusiveButReachable = rootProbe && (result.reason === "not-found" || result.reason === "unknown");
|
|
15414
|
+
if (!inconclusiveButReachable)
|
|
15415
|
+
throw connectError(result, host);
|
|
15416
|
+
}
|
|
15417
|
+
return candidate;
|
|
15418
|
+
}
|
|
15419
|
+
// src/lib/remote-auth/diagnose.ts
|
|
15420
|
+
function parseRemoteOrigin(url) {
|
|
15421
|
+
const trimmed = url.trim();
|
|
15422
|
+
if (!trimmed)
|
|
15423
|
+
return { protocol: "none" };
|
|
15424
|
+
const scp = /^[\w.-]+@([\w.][\w.-]*):/.exec(trimmed);
|
|
15425
|
+
if (scp)
|
|
15426
|
+
return { protocol: "ssh", host: credentialHostKey(trimmed) };
|
|
14823
15427
|
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" };
|
|
15428
|
+
const parsed = new URL(trimmed);
|
|
15429
|
+
const host = credentialHostKey(trimmed);
|
|
15430
|
+
if (/^https?:$/.test(parsed.protocol))
|
|
15431
|
+
return { protocol: "https", host };
|
|
15432
|
+
if (/^ssh:$/.test(parsed.protocol) || isSshRemoteUrl(trimmed)) {
|
|
15433
|
+
return { protocol: "ssh", host };
|
|
14844
15434
|
}
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
15435
|
+
return { protocol: "none" };
|
|
15436
|
+
} catch {
|
|
15437
|
+
return { protocol: "none" };
|
|
15438
|
+
}
|
|
15439
|
+
}
|
|
15440
|
+
function forgeKindForHost(host) {
|
|
15441
|
+
const name = host.toLowerCase().split(":")[0];
|
|
15442
|
+
if (name === GITHUB_HOST || name.endsWith(`.${GITHUB_HOST}`))
|
|
15443
|
+
return "github";
|
|
15444
|
+
if (name === "bitbucket.org" || name.includes("bitbucket"))
|
|
15445
|
+
return "bitbucket";
|
|
15446
|
+
if (name === "dev.azure.com" || name.endsWith(".visualstudio.com"))
|
|
15447
|
+
return "azure";
|
|
15448
|
+
if (name.includes("gitlab"))
|
|
15449
|
+
return "gitlab";
|
|
15450
|
+
if (name.includes("forgejo"))
|
|
15451
|
+
return "forgejo";
|
|
15452
|
+
if (name.includes("gitea"))
|
|
15453
|
+
return "gitea";
|
|
15454
|
+
return "generic";
|
|
15455
|
+
}
|
|
15456
|
+
async function diagnoseProjectRemote(projectDir, options = {}) {
|
|
15457
|
+
const classification = options.source ?? await detectProjectSource(projectDir);
|
|
15458
|
+
const rawRemoteUrl = classification.type === "local-git-folder" ? classification.remoteUrl : undefined;
|
|
15459
|
+
const branch = classification.type === "local-git-folder" ? classification.branch : undefined;
|
|
15460
|
+
if (!rawRemoteUrl) {
|
|
15461
|
+
return {
|
|
15462
|
+
classification,
|
|
15463
|
+
remoteProtocol: "none",
|
|
15464
|
+
...branch ? { branch } : {},
|
|
15465
|
+
credentialPresent: false,
|
|
15466
|
+
provider: null,
|
|
15467
|
+
tokenSettingsUrl: null,
|
|
15468
|
+
canSync: false,
|
|
15469
|
+
guidance: "local-only"
|
|
15470
|
+
};
|
|
15471
|
+
}
|
|
15472
|
+
const { protocol, host } = parseRemoteOrigin(rawRemoteUrl);
|
|
15473
|
+
const urlExtraction = protocol === "https" ? extractUrlCredential(rawRemoteUrl) : null;
|
|
15474
|
+
const remoteUrl = urlExtraction ? urlExtraction.cleanUrl : rawRemoteUrl;
|
|
15475
|
+
const sanitizedClassification = classification.type === "local-git-folder" && classification.remoteUrl ? { ...classification, remoteUrl } : classification;
|
|
15476
|
+
let credentialPresent = false;
|
|
15477
|
+
if (host && options.tokenStore) {
|
|
15478
|
+
try {
|
|
15479
|
+
credentialPresent = await options.tokenStore.get(host) !== null;
|
|
15480
|
+
} catch {
|
|
15481
|
+
credentialPresent = false;
|
|
14848
15482
|
}
|
|
14849
|
-
return { refreshed: false, reason: "offline" };
|
|
14850
15483
|
}
|
|
15484
|
+
if (!credentialPresent && urlExtraction?.credential)
|
|
15485
|
+
credentialPresent = true;
|
|
15486
|
+
const provider = host && protocol !== "none" ? forgeKindForHost(host) : null;
|
|
15487
|
+
const canSync = protocol === "https" && credentialPresent;
|
|
15488
|
+
let guidance;
|
|
15489
|
+
if (protocol === "ssh")
|
|
15490
|
+
guidance = "ssh-use-own-tools";
|
|
15491
|
+
else if (protocol !== "https")
|
|
15492
|
+
guidance = "local-only";
|
|
15493
|
+
else if (credentialPresent)
|
|
15494
|
+
guidance = "ready-to-sync";
|
|
15495
|
+
else if (provider === "github")
|
|
15496
|
+
guidance = "connect-github-to-sync";
|
|
15497
|
+
else
|
|
15498
|
+
guidance = "https-connect-server";
|
|
15499
|
+
return {
|
|
15500
|
+
classification: sanitizedClassification,
|
|
15501
|
+
remoteUrl,
|
|
15502
|
+
...host ? { remoteHost: host } : {},
|
|
15503
|
+
remoteProtocol: protocol === "none" ? "none" : protocol,
|
|
15504
|
+
...branch ? { branch } : {},
|
|
15505
|
+
credentialPresent,
|
|
15506
|
+
provider,
|
|
15507
|
+
tokenSettingsUrl: host ? knownForgeTokenUrl(host) : null,
|
|
15508
|
+
canSync,
|
|
15509
|
+
guidance
|
|
15510
|
+
};
|
|
14851
15511
|
}
|
|
14852
15512
|
// src/lib/remote-auth/repair.ts
|
|
14853
|
-
import
|
|
14854
|
-
import { cp as cp5, mkdir as
|
|
14855
|
-
import
|
|
15513
|
+
import git8 from "isomorphic-git";
|
|
15514
|
+
import { cp as cp5, mkdir as mkdir16, rename as rename5, rm as rm7 } from "node:fs/promises";
|
|
15515
|
+
import path27 from "node:path";
|
|
14856
15516
|
var REPAIR_SNAPSHOT_MESSAGE = "Repaired online backup";
|
|
14857
15517
|
async function moveDir(from, to) {
|
|
14858
|
-
await
|
|
15518
|
+
await mkdir16(path27.dirname(to), { recursive: true });
|
|
14859
15519
|
try {
|
|
14860
15520
|
await rename5(from, to);
|
|
14861
15521
|
} catch (e) {
|
|
@@ -14868,14 +15528,14 @@ async function moveDir(from, to) {
|
|
|
14868
15528
|
async function repairOnlineBackup(options) {
|
|
14869
15529
|
const logger = resolveLogger(options.logFile, "repair");
|
|
14870
15530
|
const dir = await repoDirFor(options.projectDir);
|
|
14871
|
-
const oldGit =
|
|
14872
|
-
const movedGitTo =
|
|
14873
|
-
const freshDir =
|
|
15531
|
+
const oldGit = path27.join(dir, ".git");
|
|
15532
|
+
const movedGitTo = path27.join(options.backupDir, "git");
|
|
15533
|
+
const freshDir = path27.join(options.backupDir, "fresh-clone");
|
|
14874
15534
|
const { restoredFiles, credential } = await withRepoLock(dir, async () => {
|
|
14875
15535
|
const transport = await resolveTransport(dir, options);
|
|
14876
15536
|
let branch;
|
|
14877
15537
|
try {
|
|
14878
|
-
branch = await
|
|
15538
|
+
branch = await git8.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14879
15539
|
} catch {
|
|
14880
15540
|
branch = undefined;
|
|
14881
15541
|
}
|
|
@@ -14891,7 +15551,7 @@ async function repairOnlineBackup(options) {
|
|
|
14891
15551
|
logger.info("swap", "moving old history aside", { to: movedGitTo });
|
|
14892
15552
|
await moveDir(oldGit, movedGitTo);
|
|
14893
15553
|
try {
|
|
14894
|
-
await moveDir(
|
|
15554
|
+
await moveDir(path27.join(freshDir, ".git"), oldGit);
|
|
14895
15555
|
} catch (e) {
|
|
14896
15556
|
await rm7(oldGit, { recursive: true, force: true });
|
|
14897
15557
|
await moveDir(movedGitTo, oldGit);
|
|
@@ -14901,10 +15561,10 @@ async function repairOnlineBackup(options) {
|
|
|
14901
15561
|
const ref = fresh.branch ?? "HEAD";
|
|
14902
15562
|
let missing = [];
|
|
14903
15563
|
try {
|
|
14904
|
-
const tracked = await
|
|
14905
|
-
missing = tracked.filter((f) => !gitFs.existsSync(
|
|
15564
|
+
const tracked = await git8.listFiles({ fs: gitFs, dir, ref });
|
|
15565
|
+
missing = tracked.filter((f) => !gitFs.existsSync(path27.join(dir, f)));
|
|
14906
15566
|
if (missing.length > 0) {
|
|
14907
|
-
await
|
|
15567
|
+
await git8.checkout({ fs: gitFs, dir, ref, filepaths: missing, force: true });
|
|
14908
15568
|
logger.info("restore", "restored files missing on this computer", { files: missing });
|
|
14909
15569
|
}
|
|
14910
15570
|
} catch (e) {
|
|
@@ -14931,21 +15591,21 @@ async function repairOnlineBackup(options) {
|
|
|
14931
15591
|
return { outcome, movedGitTo, restoredFiles };
|
|
14932
15592
|
}
|
|
14933
15593
|
// src/lib/remote-auth/scorched-earth.ts
|
|
14934
|
-
import
|
|
14935
|
-
import { cp as cp6, mkdir as
|
|
14936
|
-
import
|
|
15594
|
+
import git9 from "isomorphic-git";
|
|
15595
|
+
import { cp as cp6, mkdir as mkdir17, readdir as readdir10, rm as rm8 } from "node:fs/promises";
|
|
15596
|
+
import path28 from "node:path";
|
|
14937
15597
|
async function emptyDir(dir) {
|
|
14938
15598
|
for (const entry of await readdir10(dir)) {
|
|
14939
|
-
await rm8(
|
|
15599
|
+
await rm8(path28.join(dir, entry), { recursive: true, force: true });
|
|
14940
15600
|
}
|
|
14941
15601
|
}
|
|
14942
15602
|
async function copyFilesBack(from, to) {
|
|
14943
|
-
const skip =
|
|
15603
|
+
const skip = path28.join(from, ".git");
|
|
14944
15604
|
for (const entry of await readdir10(from)) {
|
|
14945
|
-
const src =
|
|
15605
|
+
const src = path28.join(from, entry);
|
|
14946
15606
|
if (src === skip)
|
|
14947
15607
|
continue;
|
|
14948
|
-
await cp6(src,
|
|
15608
|
+
await cp6(src, path28.join(to, entry), { recursive: true, force: true });
|
|
14949
15609
|
}
|
|
14950
15610
|
}
|
|
14951
15611
|
async function scorchedEarth(options) {
|
|
@@ -14957,12 +15617,12 @@ async function scorchedEarth(options) {
|
|
|
14957
15617
|
const transport2 = await resolveTransport(dir, options);
|
|
14958
15618
|
let branch2;
|
|
14959
15619
|
try {
|
|
14960
|
-
branch2 = await
|
|
15620
|
+
branch2 = await git9.currentBranch({ fs: gitFs, dir }) ?? undefined;
|
|
14961
15621
|
} catch {
|
|
14962
15622
|
branch2 = undefined;
|
|
14963
15623
|
}
|
|
14964
15624
|
logger.info("backup", "copying folder", { to: backupDir });
|
|
14965
|
-
await
|
|
15625
|
+
await mkdir17(backupDir, { recursive: true });
|
|
14966
15626
|
await cp6(dir, backupDir, { recursive: true });
|
|
14967
15627
|
logger.info("delete", "emptying folder", { dir });
|
|
14968
15628
|
await emptyDir(dir);
|
|
@@ -14995,7 +15655,7 @@ async function scorchedEarth(options) {
|
|
|
14995
15655
|
}
|
|
14996
15656
|
// src/lib/publish/run-publish.ts
|
|
14997
15657
|
import { readdir as readdir12, stat as stat12 } from "node:fs/promises";
|
|
14998
|
-
import
|
|
15658
|
+
import path33 from "node:path";
|
|
14999
15659
|
|
|
15000
15660
|
// src/lib/publish/command-runner.ts
|
|
15001
15661
|
var CAPTURE_LIMIT = 64 * 1024;
|
|
@@ -15074,8 +15734,8 @@ async function resolveSwaCommand(req) {
|
|
|
15074
15734
|
const explicit = env.SWA_CLI_PATH?.trim();
|
|
15075
15735
|
if (explicit)
|
|
15076
15736
|
return explicit;
|
|
15077
|
-
const
|
|
15078
|
-
return await commandExists("swa",
|
|
15737
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15738
|
+
return await commandExists("swa", run3, req.deps.env) ? "swa" : null;
|
|
15079
15739
|
}
|
|
15080
15740
|
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
15741
|
var azureSwaProvider = {
|
|
@@ -15121,9 +15781,9 @@ var azureSwaProvider = {
|
|
|
15121
15781
|
throw new Error(SWA_INSTALL_HINT);
|
|
15122
15782
|
const cfg = req.config;
|
|
15123
15783
|
const deployEnv = cfg.env?.trim() || "production";
|
|
15124
|
-
const
|
|
15784
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15125
15785
|
req.deps.onProgress?.(`Deploying ${req.artifact.path} to Azure SWA (${deployEnv})…`);
|
|
15126
|
-
const result = await
|
|
15786
|
+
const result = await run3(swa, ["deploy", req.artifact.path, "--env", deployEnv], {
|
|
15127
15787
|
env: { SWA_CLI_DEPLOYMENT_TOKEN: resolved.credential.token },
|
|
15128
15788
|
onOutput: req.deps.onProgress
|
|
15129
15789
|
});
|
|
@@ -15144,25 +15804,25 @@ ${tail}` : ""}`);
|
|
|
15144
15804
|
};
|
|
15145
15805
|
|
|
15146
15806
|
// src/lib/publish/providers/guided.ts
|
|
15147
|
-
import { copyFile as copyFile2, mkdir as
|
|
15148
|
-
import
|
|
15807
|
+
import { copyFile as copyFile2, mkdir as mkdir18, writeFile as writeFile15 } from "node:fs/promises";
|
|
15808
|
+
import path29 from "node:path";
|
|
15149
15809
|
async function stageGuidedPackage(req, providerId, listingLines) {
|
|
15150
|
-
const packageDir =
|
|
15151
|
-
await
|
|
15152
|
-
await copyFile2(req.artifact.path,
|
|
15810
|
+
const packageDir = path29.join(path29.dirname(req.artifact.path), "publish", providerId);
|
|
15811
|
+
await mkdir18(packageDir, { recursive: true });
|
|
15812
|
+
await copyFile2(req.artifact.path, path29.join(packageDir, path29.basename(req.artifact.path)));
|
|
15153
15813
|
const { title, authors } = req.project;
|
|
15154
15814
|
const sheet = [
|
|
15155
15815
|
`# ${title || "Untitled"}`,
|
|
15156
15816
|
"",
|
|
15157
15817
|
`- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
|
|
15158
|
-
`- **File:** ${
|
|
15818
|
+
`- **File:** ${path29.basename(req.artifact.path)}`,
|
|
15159
15819
|
...listingLines,
|
|
15160
15820
|
"",
|
|
15161
15821
|
"Copy these details into the platform's listing form when uploading.",
|
|
15162
15822
|
""
|
|
15163
15823
|
].join(`
|
|
15164
15824
|
`);
|
|
15165
|
-
await
|
|
15825
|
+
await writeFile15(path29.join(packageDir, "LISTING.md"), sheet, "utf8");
|
|
15166
15826
|
return packageDir;
|
|
15167
15827
|
}
|
|
15168
15828
|
|
|
@@ -15225,9 +15885,9 @@ var drivethrurpgProvider = {
|
|
|
15225
15885
|
};
|
|
15226
15886
|
|
|
15227
15887
|
// src/lib/publish/providers/gdrive.ts
|
|
15228
|
-
import { mkdtemp as mkdtemp3, readFile as
|
|
15888
|
+
import { mkdtemp as mkdtemp3, readFile as readFile34, readdir as readdir11, rm as rm9, stat as stat9, writeFile as writeFile16 } from "node:fs/promises";
|
|
15229
15889
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15230
|
-
import
|
|
15890
|
+
import path30 from "node:path";
|
|
15231
15891
|
import { Zip, ZipDeflate } from "fflate";
|
|
15232
15892
|
|
|
15233
15893
|
// src/lib/publish/google-drive.ts
|
|
@@ -15612,34 +16272,34 @@ function zipEntriesNonBlocking(entries, opts) {
|
|
|
15612
16272
|
async function collectZipEntries(root, dir, out, onWarn) {
|
|
15613
16273
|
const entries = await readdir11(dir, { withFileTypes: true });
|
|
15614
16274
|
for (const entry of entries) {
|
|
15615
|
-
const full =
|
|
16275
|
+
const full = path30.join(dir, entry.name);
|
|
15616
16276
|
if (entry.isDirectory()) {
|
|
15617
16277
|
await collectZipEntries(root, full, out, onWarn);
|
|
15618
16278
|
} else if (entry.isFile()) {
|
|
15619
|
-
const rel =
|
|
15620
|
-
out[rel] = await
|
|
16279
|
+
const rel = path30.relative(root, full).split(path30.sep).join("/");
|
|
16280
|
+
out[rel] = await readFile34(full);
|
|
15621
16281
|
} else if (entry.isSymbolicLink()) {
|
|
15622
16282
|
const resolved = await stat9(full).catch(() => null);
|
|
15623
16283
|
if (resolved?.isFile()) {
|
|
15624
|
-
const rel =
|
|
15625
|
-
out[rel] = await
|
|
16284
|
+
const rel = path30.relative(root, full).split(path30.sep).join("/");
|
|
16285
|
+
out[rel] = await readFile34(full);
|
|
15626
16286
|
} else if (resolved?.isDirectory()) {
|
|
15627
16287
|
await collectZipEntries(root, full, out, onWarn);
|
|
15628
16288
|
} else {
|
|
15629
|
-
onWarn?.(`Skipped "${
|
|
16289
|
+
onWarn?.(`Skipped "${path30.relative(root, full)}" in the website export — it's a broken symlink.`);
|
|
15630
16290
|
}
|
|
15631
16291
|
} else {
|
|
15632
|
-
onWarn?.(`Skipped "${
|
|
16292
|
+
onWarn?.(`Skipped "${path30.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
|
|
15633
16293
|
}
|
|
15634
16294
|
}
|
|
15635
16295
|
}
|
|
15636
|
-
async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl =
|
|
16296
|
+
async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile16) {
|
|
15637
16297
|
const entries = {};
|
|
15638
16298
|
await collectZipEntries(exportDir, exportDir, entries, onWarn);
|
|
15639
16299
|
const fileName = `${bookSlug(title)}-website.zip`;
|
|
15640
|
-
const tmpRoot = await mkdtemp3(
|
|
16300
|
+
const tmpRoot = await mkdtemp3(path30.join(tmpdir3(), "gutterpress-gdrive-"));
|
|
15641
16301
|
try {
|
|
15642
|
-
const filePath =
|
|
16302
|
+
const filePath = path30.join(tmpRoot, fileName);
|
|
15643
16303
|
const zipped = await zipEntriesNonBlocking(entries, { level: 6 });
|
|
15644
16304
|
await writeArchiveImpl(filePath, zipped);
|
|
15645
16305
|
return {
|
|
@@ -15687,7 +16347,7 @@ var gdriveProvider = {
|
|
|
15687
16347
|
const isHtml = req.artifact.format === "html";
|
|
15688
16348
|
const source = isHtml ? await zipHtmlExport(req.artifact.path, req.project.title, (msg) => req.deps.onProgress?.(msg)) : {
|
|
15689
16349
|
filePath: req.artifact.path,
|
|
15690
|
-
fileName:
|
|
16350
|
+
fileName: path30.basename(req.artifact.path),
|
|
15691
16351
|
mimeType: "application/pdf",
|
|
15692
16352
|
cleanup: async () => {}
|
|
15693
16353
|
};
|
|
@@ -15737,8 +16397,8 @@ var gdriveProvider = {
|
|
|
15737
16397
|
};
|
|
15738
16398
|
|
|
15739
16399
|
// src/lib/publish/butler.ts
|
|
15740
|
-
import { chmod as chmod2, mkdir as
|
|
15741
|
-
import
|
|
16400
|
+
import { chmod as chmod2, mkdir as mkdir19, stat as stat10, writeFile as writeFile17 } from "node:fs/promises";
|
|
16401
|
+
import path31 from "node:path";
|
|
15742
16402
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
15743
16403
|
var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
|
|
15744
16404
|
function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
|
|
@@ -15775,8 +16435,8 @@ async function ensureButler(deps) {
|
|
|
15775
16435
|
}
|
|
15776
16436
|
if (await commandExists("butler", runCommand, deps.env))
|
|
15777
16437
|
return "butler";
|
|
15778
|
-
const cacheDir =
|
|
15779
|
-
const cached =
|
|
16438
|
+
const cacheDir = path31.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
|
|
16439
|
+
const cached = path31.join(cacheDir, butlerBinaryName());
|
|
15780
16440
|
if (await fileExists2(cached))
|
|
15781
16441
|
return cached;
|
|
15782
16442
|
const channel = butlerBrothChannel();
|
|
@@ -15798,14 +16458,14 @@ async function ensureButler(deps) {
|
|
|
15798
16458
|
return new Uint8Array(await response.arrayBuffer());
|
|
15799
16459
|
});
|
|
15800
16460
|
const files = unzipSync2(archive);
|
|
15801
|
-
await
|
|
16461
|
+
await mkdir19(cacheDir, { recursive: true });
|
|
15802
16462
|
let foundBinary = false;
|
|
15803
16463
|
for (const [name, data] of Object.entries(files)) {
|
|
15804
16464
|
if (name.endsWith("/"))
|
|
15805
16465
|
continue;
|
|
15806
|
-
const target =
|
|
15807
|
-
await
|
|
15808
|
-
if (
|
|
16466
|
+
const target = path31.join(cacheDir, path31.basename(name));
|
|
16467
|
+
await writeFile17(target, data);
|
|
16468
|
+
if (path31.basename(name) === butlerBinaryName()) {
|
|
15809
16469
|
await chmod2(target, 493).catch(() => {});
|
|
15810
16470
|
foundBinary = true;
|
|
15811
16471
|
}
|
|
@@ -15926,9 +16586,9 @@ var itchProvider = {
|
|
|
15926
16586
|
throw new Error("No itch.io API key found. Connect itch.io (or set BUTLER_API_KEY) first.");
|
|
15927
16587
|
}
|
|
15928
16588
|
const butler = await ensureButler(req.deps);
|
|
15929
|
-
const
|
|
16589
|
+
const run3 = req.deps.runCommand ?? defaultCommandRunner;
|
|
15930
16590
|
req.deps.onProgress?.(`Uploading to ${target}:${channel} with butler…`);
|
|
15931
|
-
const result = await
|
|
16591
|
+
const result = await run3(butler, ["push", req.artifact.path, `${target}:${channel}`], {
|
|
15932
16592
|
env: { BUTLER_API_KEY: resolved.credential.token },
|
|
15933
16593
|
onOutput: req.deps.onProgress
|
|
15934
16594
|
});
|
|
@@ -16001,8 +16661,8 @@ var kdpProvider = {
|
|
|
16001
16661
|
};
|
|
16002
16662
|
|
|
16003
16663
|
// src/lib/publish/providers/local-folder.ts
|
|
16004
|
-
import { cp as cp7, mkdir as
|
|
16005
|
-
import
|
|
16664
|
+
import { cp as cp7, mkdir as mkdir20, stat as stat11 } from "node:fs/promises";
|
|
16665
|
+
import path32 from "node:path";
|
|
16006
16666
|
var LOCAL_PUBLISH_DEFAULT_DIR = "dist";
|
|
16007
16667
|
var info7 = {
|
|
16008
16668
|
id: "local",
|
|
@@ -16018,7 +16678,7 @@ var info7 = {
|
|
|
16018
16678
|
};
|
|
16019
16679
|
function localPublishDir(req) {
|
|
16020
16680
|
const dir = typeof req.config.dir === "string" && req.config.dir.trim() ? req.config.dir.trim() : LOCAL_PUBLISH_DEFAULT_DIR;
|
|
16021
|
-
return
|
|
16681
|
+
return path32.resolve(req.project.projectDir, dir);
|
|
16022
16682
|
}
|
|
16023
16683
|
var localFolderProvider = {
|
|
16024
16684
|
info: info7,
|
|
@@ -16039,9 +16699,9 @@ var localFolderProvider = {
|
|
|
16039
16699
|
},
|
|
16040
16700
|
async upload(req) {
|
|
16041
16701
|
const dir = localPublishDir(req);
|
|
16042
|
-
await
|
|
16043
|
-
const dest = req.artifact.format === "html" ? dir :
|
|
16044
|
-
if (
|
|
16702
|
+
await mkdir20(dir, { recursive: true });
|
|
16703
|
+
const dest = req.artifact.format === "html" ? dir : path32.join(dir, path32.basename(req.artifact.path));
|
|
16704
|
+
if (path32.resolve(dest) !== path32.resolve(req.artifact.path)) {
|
|
16045
16705
|
await cp7(req.artifact.path, dest, { recursive: true });
|
|
16046
16706
|
}
|
|
16047
16707
|
return { kind: "published", detail: `Saved to ${dest}` };
|
|
@@ -16303,9 +16963,9 @@ async function resolvePublishRequest(options, deps) {
|
|
|
16303
16963
|
const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
|
|
16304
16964
|
const outDir = resolveOutputDir(manifestDir, config.title);
|
|
16305
16965
|
const effectiveFormat = resolvePublishFormat(provider.info, providerConfig);
|
|
16306
|
-
const defaultArtifact = effectiveFormat === "pdf" ?
|
|
16966
|
+
const defaultArtifact = effectiveFormat === "pdf" ? path33.join(outDir, artifactName(config.title, "pdf")) : outDir;
|
|
16307
16967
|
const artifact = {
|
|
16308
|
-
path: options.artifactPath ?
|
|
16968
|
+
path: options.artifactPath ? path33.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
|
|
16309
16969
|
format: effectiveFormat
|
|
16310
16970
|
};
|
|
16311
16971
|
return {
|
|
@@ -16369,7 +17029,7 @@ async function artifactIssues(artifact) {
|
|
|
16369
17029
|
var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
|
|
16370
17030
|
async function htmlDirIssues(dir) {
|
|
16371
17031
|
const issues = [];
|
|
16372
|
-
const isThere = async (rel) => stat12(
|
|
17032
|
+
const isThere = async (rel) => stat12(path33.join(dir, rel)).then(() => true, () => false);
|
|
16373
17033
|
if (!await isThere(BOOK_HTML)) {
|
|
16374
17034
|
issues.push({
|
|
16375
17035
|
severity: "error",
|
|
@@ -16488,17 +17148,17 @@ async function disconnectPublishCredential(key, deps, options = {}) {
|
|
|
16488
17148
|
}
|
|
16489
17149
|
}
|
|
16490
17150
|
// src/lib/publish/selections.ts
|
|
16491
|
-
import { chmod as chmod3, mkdir as
|
|
16492
|
-
import
|
|
17151
|
+
import { chmod as chmod3, mkdir as mkdir21, readFile as readFile35, writeFile as writeFile18 } from "node:fs/promises";
|
|
17152
|
+
import path34 from "node:path";
|
|
16493
17153
|
class PublishSelectionsStore {
|
|
16494
17154
|
filePath;
|
|
16495
17155
|
queue = Promise.resolve();
|
|
16496
17156
|
constructor(filePath) {
|
|
16497
|
-
this.filePath = filePath ??
|
|
17157
|
+
this.filePath = filePath ?? path34.join(defaultConfigDir(), "publish-selections.json");
|
|
16498
17158
|
}
|
|
16499
17159
|
async read() {
|
|
16500
17160
|
try {
|
|
16501
|
-
const raw = await
|
|
17161
|
+
const raw = await readFile35(this.filePath, "utf8");
|
|
16502
17162
|
const parsed = JSON.parse(raw);
|
|
16503
17163
|
if (parsed && typeof parsed === "object") {
|
|
16504
17164
|
return {
|
|
@@ -16511,19 +17171,19 @@ class PublishSelectionsStore {
|
|
|
16511
17171
|
return { version: 1, global: {}, projects: {} };
|
|
16512
17172
|
}
|
|
16513
17173
|
async write(data) {
|
|
16514
|
-
await
|
|
16515
|
-
await
|
|
17174
|
+
await mkdir21(path34.dirname(this.filePath), { recursive: true });
|
|
17175
|
+
await writeFile18(this.filePath, JSON.stringify(data, null, 2), {
|
|
16516
17176
|
encoding: "utf8",
|
|
16517
17177
|
mode: 384
|
|
16518
17178
|
});
|
|
16519
17179
|
await chmod3(this.filePath, 384);
|
|
16520
17180
|
}
|
|
16521
17181
|
enqueue(fn) {
|
|
16522
|
-
const
|
|
16523
|
-
this.queue =
|
|
17182
|
+
const run3 = this.queue.then(fn, fn);
|
|
17183
|
+
this.queue = run3.catch(() => {
|
|
16524
17184
|
return;
|
|
16525
17185
|
});
|
|
16526
|
-
return
|
|
17186
|
+
return run3;
|
|
16527
17187
|
}
|
|
16528
17188
|
resolve(providerId, projectDir) {
|
|
16529
17189
|
return this.enqueue(async () => {
|
|
@@ -16614,4 +17274,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
|
|
|
16614
17274
|
await writeManifestDoc(file, doc);
|
|
16615
17275
|
return settingsFromDoc(doc);
|
|
16616
17276
|
}
|
|
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,
|
|
17277
|
+
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 };
|