gutterpress 0.10.9 → 0.10.10-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -5
- package/dist/{README-jntqyj1z.md → README-3zk4tgnj.md} +4 -4
- package/dist/{README-6c7fda1s.md → README-7x5akcns.md} +5 -4
- package/dist/api/index.d.ts +7 -4
- package/dist/api/index.js +22 -12
- package/dist/{audit-fq7s1gsz.js → audit-wcmbgh39.js} +5 -5
- package/dist/{build-dyat3fqd.js → build-c1w6kqpv.js} +6 -6
- package/dist/{cli-ypfvnhag.js → cli-2rswy18t.js} +1 -1
- package/dist/{cli-w692g6dx.js → cli-4mtf9ajw.js} +127 -1
- package/dist/{cli-bzxqtvrk.js → cli-75qkn0k5.js} +1 -1
- package/dist/{cli-bx950tb4.js → cli-k8f9eb12.js} +22 -26
- package/dist/{cli-8pg731p5.js → cli-n0vgey6x.js} +4914 -4707
- package/dist/{cli-rh05byck.js → cli-scqp638v.js} +1 -1
- package/dist/cli.js +16 -16
- package/dist/{components-0mmswvvm.yaml → components-dhgv2sm7.yaml} +2 -2
- package/dist/{doctor-yr85n080.js → doctor-drv36btj.js} +2 -2
- package/dist/{engine-d46wgg99.js → engine-pt2acjb5.js} +2 -2
- package/dist/{engine-19mshh07.js → engine-qtmvaqt7.js} +1 -1
- package/dist/{ext-jrg8c48s.js → ext-xqx26xvy.js} +52 -8
- package/dist/{index-mey5vraz.js → index-1he93sjd.js} +918 -796
- package/dist/{index-zwcv1njy.js → index-5dgxeevv.js} +22 -26
- package/dist/{index-dhgzkhs7.js → index-a0e2253t.js} +1 -1
- package/dist/{index-dtrftayr.js → index-y01v2fe0.js} +127 -1
- package/dist/index.js +102 -15
- package/dist/lib/build-error.d.ts +6 -5
- package/dist/lib/build-preflight.d.ts +11 -1
- package/dist/lib/build-runner.d.ts +49 -16
- package/dist/lib/extension-import.d.ts +4 -4
- package/dist/lib/extension-manager.d.ts +3 -3
- package/dist/lib/extension-manifest.d.ts +91 -87
- package/dist/lib/extension-scaffold.d.ts +1 -1
- package/dist/lib/extension-search.d.ts +46 -0
- package/dist/lib/host-policy.d.ts +10 -12
- package/dist/lib/lint-runner.d.ts +16 -9
- package/dist/lib/npm-registry.d.ts +22 -0
- package/dist/lib/source-provider.d.ts +71 -0
- package/dist/lib/validation-exec.d.ts +11 -0
- package/dist/{lint-enasqbqx.js → lint-ejzmx14j.js} +5 -5
- package/dist/{new-r9c99wsn.js → new-g3q3h4s5.js} +6 -6
- package/dist/package-97gm31sg.json +10 -0
- package/dist/{package-rm9d2tht.json → package-9rv69pxm.json} +4 -1
- package/dist/package-aenwjew3.json +28 -0
- package/dist/package-gag6ykwn.json +10 -0
- package/dist/package-nrhkwf2m.json +10 -0
- package/dist/{plugin.test.js-npnfhyca.tpl → plugin.test.js-k2r87k04.tpl} +9 -8
- package/dist/{preflight-shk7vqbs.js → preflight-w5z2vfhx.js} +5 -5
- package/dist/{preview-52918mxk.js → preview-9s22dq77.js} +6 -6
- package/dist/{publish-8s60vhx7.js → publish-mt22ewpk.js} +5 -5
- package/dist/{source-provider-2j5x2hyw.js → source-provider-51g6mt6y.js} +7 -1
- package/dist/{source-provider-kn41jmbs.js → source-provider-jkppq9fc.js} +7 -1
- package/dist/{tokens-e4xk3x4v.css → tokens-2sracx1w.css} +1 -1
- package/dist/{validate-awccx32r.js → validate-kkn8rmy5.js} +5 -5
- package/package.json +1 -1
- package/dist/gutterpress-m98e2w2d.json +0 -8
- package/dist/gutterpress-x2ycdsb8.json +0 -17
- package/dist/theme-etj4gxck.json +0 -9
- package/dist/theme-s2rg7xe7.json +0 -9
- package/dist/theme-vktedy8q.json +0 -9
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
resolveChromiumExecutable,
|
|
19
19
|
run,
|
|
20
20
|
spawnCapture
|
|
21
|
-
} from "./index-
|
|
21
|
+
} from "./index-5dgxeevv.js";
|
|
22
22
|
import {
|
|
23
23
|
gitFs,
|
|
24
24
|
gitScopeFor,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
snapshotStagingMarkerPath,
|
|
30
30
|
snapshotWorkingTreeUnlocked,
|
|
31
31
|
withRepoLock
|
|
32
|
-
} from "./index-
|
|
32
|
+
} from "./index-y01v2fe0.js";
|
|
33
33
|
import {
|
|
34
34
|
detectProjectSource,
|
|
35
35
|
syncRemoteFor
|
|
@@ -58,6 +58,27 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
|
58
58
|
import path from "node:path";
|
|
59
59
|
import { exports as resolveExports } from "resolve.exports";
|
|
60
60
|
import { satisfies, validRange } from "semver";
|
|
61
|
+
|
|
62
|
+
// src/lib/npm-registry.ts
|
|
63
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
64
|
+
var ENV_OVERRIDE = "GUTTERPRESS_NPM_REGISTRY";
|
|
65
|
+
function npmRegistryUrl() {
|
|
66
|
+
const configured = process.env[ENV_OVERRIDE]?.trim();
|
|
67
|
+
if (!configured)
|
|
68
|
+
return DEFAULT_NPM_REGISTRY;
|
|
69
|
+
let scheme;
|
|
70
|
+
try {
|
|
71
|
+
scheme = new URL(configured).protocol;
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error(`Invalid ${ENV_OVERRIDE} URL "${configured}".`);
|
|
74
|
+
}
|
|
75
|
+
if (scheme !== "http:" && scheme !== "https:") {
|
|
76
|
+
throw new Error(`${ENV_OVERRIDE} must be an http(s) URL — got "${scheme}".`);
|
|
77
|
+
}
|
|
78
|
+
return configured.replace(/\/+$/, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/lib/plugin-vendor.ts
|
|
61
82
|
var PLUGINS_DIR = "plugins";
|
|
62
83
|
var VENDORED_NPM_DIR = "npm";
|
|
63
84
|
var VENDOR_RECEIPT_FILE = ".gutterpress-install.json";
|
|
@@ -529,7 +550,7 @@ async function verifyVendoredPlugin(projectDir, expectedName, expectedVersion) {
|
|
|
529
550
|
throw new Error(`Package receipt is missing provenance: ${pkg.name}@${pkg.version}`);
|
|
530
551
|
}
|
|
531
552
|
const tarball = new URL(pkg.tarball);
|
|
532
|
-
if (tarball.origin !==
|
|
553
|
+
if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
|
|
533
554
|
throw new Error(`Package receipt has an invalid tarball URL: ${pkg.name}@${pkg.version}`);
|
|
534
555
|
}
|
|
535
556
|
const packageDir = path.resolve(installRoot, ...relative.split("/"));
|
|
@@ -3637,7 +3658,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
|
|
|
3637
3658
|
|
|
3638
3659
|
// src/lib/markdown/plugins.ts
|
|
3639
3660
|
import { createHash as createHash3 } from "node:crypto";
|
|
3640
|
-
import { existsSync as
|
|
3661
|
+
import { existsSync as existsSync4, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
|
|
3641
3662
|
import { cp, link, mkdir as mkdir2, mkdtemp, readFile as readFile6, rm, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
3642
3663
|
import { tmpdir } from "node:os";
|
|
3643
3664
|
import { resolve as resolve3, join as join2, dirname as dirname2, basename, extname, relative, isAbsolute } from "node:path";
|
|
@@ -3663,25 +3684,46 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
|
|
|
3663
3684
|
}
|
|
3664
3685
|
|
|
3665
3686
|
// src/lib/extension-manifest.ts
|
|
3666
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
3667
3687
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
3668
3688
|
import path4 from "node:path";
|
|
3669
|
-
var EXTENSION_MANIFEST_FILENAME = "
|
|
3670
|
-
var
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3689
|
+
var EXTENSION_MANIFEST_FILENAME = "package.json";
|
|
3690
|
+
var GUTTERPRESS_KEY = "gutterpress";
|
|
3691
|
+
function object2(value) {
|
|
3692
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
3693
|
+
}
|
|
3694
|
+
function str(value) {
|
|
3695
|
+
return typeof value === "string" ? value : undefined;
|
|
3696
|
+
}
|
|
3697
|
+
function authorName(value) {
|
|
3698
|
+
return str(value) ?? str(object2(value)?.name);
|
|
3699
|
+
}
|
|
3700
|
+
function metaFromPackageJson(raw) {
|
|
3701
|
+
const pkg = object2(raw);
|
|
3702
|
+
if (!pkg)
|
|
3676
3703
|
return {};
|
|
3677
|
-
}
|
|
3704
|
+
const gp = object2(pkg[GUTTERPRESS_KEY]) ?? {};
|
|
3705
|
+
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords.filter((k) => typeof k === "string") : undefined;
|
|
3706
|
+
return {
|
|
3707
|
+
...str(pkg.name) ? { name: str(pkg.name) } : {},
|
|
3708
|
+
...authorName(pkg.author) ? { author: authorName(pkg.author) } : {},
|
|
3709
|
+
...str(pkg.description) ? { description: str(pkg.description) } : {},
|
|
3710
|
+
...keywords ? { keywords } : {},
|
|
3711
|
+
...str(pkg.main) ? { main: str(pkg.main) } : {},
|
|
3712
|
+
..."preview" in gp ? { preview: str(gp.preview) ?? null } : {},
|
|
3713
|
+
...Array.isArray(gp.styles) ? { styles: declaredList(gp.styles) } : {},
|
|
3714
|
+
...str(gp.tokensFile) ? { tokensFile: str(gp.tokensFile) } : {},
|
|
3715
|
+
...str(gp.markdown) ? { markdown: str(gp.markdown) } : {},
|
|
3716
|
+
...str(gp.components) ? { components: str(gp.components) } : {},
|
|
3717
|
+
...str(gp.snippets) ? { snippets: str(gp.snippets) } : {},
|
|
3718
|
+
...gp.engineStyles !== undefined ? { engineStyles: gp.engineStyles } : {}
|
|
3719
|
+
};
|
|
3678
3720
|
}
|
|
3679
3721
|
async function readExtensionMeta(dir) {
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3722
|
+
try {
|
|
3723
|
+
return metaFromPackageJson(JSON.parse(await readFile5(path4.join(dir, EXTENSION_MANIFEST_FILENAME), "utf8")));
|
|
3724
|
+
} catch {
|
|
3725
|
+
return {};
|
|
3683
3726
|
}
|
|
3684
|
-
return readJsonMetaFile(path4.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
|
|
3685
3727
|
}
|
|
3686
3728
|
function declaredList(value) {
|
|
3687
3729
|
return Array.isArray(value) ? value.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
|
|
@@ -3689,16 +3731,13 @@ function declaredList(value) {
|
|
|
3689
3731
|
function extensionStyleList(meta) {
|
|
3690
3732
|
return declaredList(meta.styles);
|
|
3691
3733
|
}
|
|
3692
|
-
function
|
|
3693
|
-
|
|
3694
|
-
if (declared.length > 0)
|
|
3695
|
-
return declared;
|
|
3696
|
-
return existsSync4(path4.join(dir, "theme.css")) ? ["theme.css"] : [];
|
|
3734
|
+
function extensionEntry(meta) {
|
|
3735
|
+
return meta.markdown?.trim() || meta.main?.trim() || undefined;
|
|
3697
3736
|
}
|
|
3698
|
-
function extensionCarries(meta
|
|
3737
|
+
function extensionCarries(meta) {
|
|
3699
3738
|
return {
|
|
3700
|
-
markdown: !!meta
|
|
3701
|
-
styles:
|
|
3739
|
+
markdown: !!extensionEntry(meta),
|
|
3740
|
+
styles: extensionStyleList(meta).length > 0,
|
|
3702
3741
|
snippets: !!meta.snippets?.trim(),
|
|
3703
3742
|
components: !!meta.components?.trim()
|
|
3704
3743
|
};
|
|
@@ -3710,9 +3749,10 @@ function assertExtensionContained(meta) {
|
|
|
3710
3749
|
if (meta.engineStyles !== undefined) {
|
|
3711
3750
|
throw new Error("The extension's metadata declares `engineStyles`, which was removed — " + "move its entries to the end of `styles`.");
|
|
3712
3751
|
}
|
|
3752
|
+
const entry = extensionEntry(meta);
|
|
3713
3753
|
const declared = [
|
|
3714
3754
|
...extensionStyleList(meta),
|
|
3715
|
-
...
|
|
3755
|
+
...entry ? [entry] : [],
|
|
3716
3756
|
...meta.components ? [meta.components] : [],
|
|
3717
3757
|
...meta.snippets ? [meta.snippets] : [],
|
|
3718
3758
|
...meta.tokensFile ? [meta.tokensFile] : []
|
|
@@ -3724,8 +3764,9 @@ function assertExtensionContained(meta) {
|
|
|
3724
3764
|
}
|
|
3725
3765
|
}
|
|
3726
3766
|
function resolveExtension(dir, meta, subject) {
|
|
3727
|
-
const
|
|
3728
|
-
const
|
|
3767
|
+
const entry = extensionEntry(meta);
|
|
3768
|
+
const styles = resolveDeclaredStyles(extensionStyleList(meta), dir, subject);
|
|
3769
|
+
const [markdown] = resolveDeclaredStyles(entry ? [entry] : undefined, dir, subject) ?? [];
|
|
3729
3770
|
const [components] = resolveDeclaredStyles(meta.components ? [meta.components] : undefined, dir, subject) ?? [];
|
|
3730
3771
|
const [snippets] = resolveDeclaredStyles(meta.snippets ? [meta.snippets] : undefined, dir, subject) ?? [];
|
|
3731
3772
|
return {
|
|
@@ -3973,9 +4014,9 @@ function isReceiptBoundRequireCall(node) {
|
|
|
3973
4014
|
return true;
|
|
3974
4015
|
if (callee.type !== "MemberExpression")
|
|
3975
4016
|
return false;
|
|
3976
|
-
const
|
|
4017
|
+
const object3 = callee.object;
|
|
3977
4018
|
const property = memberName(callee);
|
|
3978
|
-
return Boolean(
|
|
4019
|
+
return Boolean(object3 && (object3.type === "Identifier" && object3.name === "module" && property === "require" || object3.type === "Identifier" && object3.name === "require" && property === "resolve"));
|
|
3979
4020
|
}
|
|
3980
4021
|
function rewriteCommonJsRequires(source, absolute, pkg, tree) {
|
|
3981
4022
|
const ast = parseJavaScript(source, {
|
|
@@ -4391,6 +4432,22 @@ function extensionMetadata(meta) {
|
|
|
4391
4432
|
...meta.author ? { author: meta.author } : {}
|
|
4392
4433
|
};
|
|
4393
4434
|
}
|
|
4435
|
+
function staleManifestHint(dir) {
|
|
4436
|
+
const stale = ["gutterpress.json", "theme.json"].find((name) => existsSync4(join2(dir, name)));
|
|
4437
|
+
if (!stale)
|
|
4438
|
+
return "";
|
|
4439
|
+
return `
|
|
4440
|
+
|
|
4441
|
+
This folder still has a ${stale}. Gutterpress no longer reads it — ` + `describe the extension in package.json instead:
|
|
4442
|
+
{
|
|
4443
|
+
"name": "my-extension",
|
|
4444
|
+
"description": "What it does",
|
|
4445
|
+
"author": "You",
|
|
4446
|
+
"main": "plugin.js",
|
|
4447
|
+
"gutterpress": { "styles": ["theme.css"] }
|
|
4448
|
+
}
|
|
4449
|
+
(\`main\` only if the package carries a markdown-it plugin; \`gutterpress.styles\` only if it carries stylesheets.)`;
|
|
4450
|
+
}
|
|
4394
4451
|
async function loadExtensionFromDir(extensionDir, config, pluginRef) {
|
|
4395
4452
|
const meta = await readExtensionMeta(extensionDir);
|
|
4396
4453
|
assertExtensionContained(meta);
|
|
@@ -4398,7 +4455,7 @@ async function loadExtensionFromDir(extensionDir, config, pluginRef) {
|
|
|
4398
4455
|
const extensionStyles = resolved.styles ?? [];
|
|
4399
4456
|
const name = config.name ?? meta.name ?? pluginRef;
|
|
4400
4457
|
if (!resolved.markdown && extensionStyles.length === 0) {
|
|
4401
|
-
throw new Error(`Extension folder "${pluginRef}" declares
|
|
4458
|
+
throw new Error(`Extension folder "${pluginRef}" declares no markdown-it plugin (\`main\`) and no ` + "`gutterpress.styles` in its package.json — there is nothing to load. Point " + "`path` at a JS file directly for a plain plugin, or describe the package in package.json." + staleManifestHint(extensionDir));
|
|
4402
4459
|
}
|
|
4403
4460
|
if (!resolved.markdown) {
|
|
4404
4461
|
return {
|
|
@@ -4442,7 +4499,7 @@ async function loadPlugin(config, baseDir) {
|
|
|
4442
4499
|
}
|
|
4443
4500
|
if (config.path) {
|
|
4444
4501
|
const candidatePath = resolve3(baseDir, config.path);
|
|
4445
|
-
if (
|
|
4502
|
+
if (existsSync4(candidatePath) && statSync2(candidatePath).isDirectory()) {
|
|
4446
4503
|
try {
|
|
4447
4504
|
return await loadExtensionFromDir(candidatePath, config, pluginRef);
|
|
4448
4505
|
} catch (error2) {
|
|
@@ -4454,7 +4511,7 @@ async function loadPlugin(config, baseDir) {
|
|
|
4454
4511
|
try {
|
|
4455
4512
|
if (config.path) {
|
|
4456
4513
|
const pluginPath = resolve3(baseDir, config.path);
|
|
4457
|
-
if (!
|
|
4514
|
+
if (!existsSync4(pluginPath)) {
|
|
4458
4515
|
throw new Error(`Plugin file not found: ${pluginPath} (resolved from manifest entry path="${config.path}")`);
|
|
4459
4516
|
}
|
|
4460
4517
|
pluginModule = await loadCachedPathPluginModule(pluginPath);
|
|
@@ -4465,12 +4522,12 @@ async function loadPlugin(config, baseDir) {
|
|
|
4465
4522
|
pluginModule = loaded.module;
|
|
4466
4523
|
moduleDir = loaded.moduleDir;
|
|
4467
4524
|
pluginName = config.name;
|
|
4468
|
-
if (loaded.packageDir
|
|
4525
|
+
if (loaded.packageDir) {
|
|
4469
4526
|
const meta = await readExtensionMeta(loaded.packageDir);
|
|
4470
4527
|
assertExtensionContained(meta);
|
|
4471
|
-
const resolved = resolveExtension(loaded.packageDir, meta, `Plugin "${pluginRef}"`);
|
|
4528
|
+
const resolved = resolveExtension(loaded.packageDir, { ...meta, main: undefined }, `Plugin "${pluginRef}"`);
|
|
4472
4529
|
if (resolved.markdown && loaded.entryPath && resolve3(resolved.markdown) !== resolve3(loaded.entryPath)) {
|
|
4473
|
-
throw new Error(`its
|
|
4530
|
+
throw new Error(`its package.json declares gutterpress.markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's ` + "markdown-it plugin is its package entry, so declare that file or drop the field.");
|
|
4474
4531
|
}
|
|
4475
4532
|
packageMeta = meta;
|
|
4476
4533
|
packageStyles = resolved.styles ?? [];
|
|
@@ -4546,7 +4603,7 @@ function artifactName(title, format) {
|
|
|
4546
4603
|
}
|
|
4547
4604
|
|
|
4548
4605
|
// src/lib/ghostscript.ts
|
|
4549
|
-
import { existsSync as
|
|
4606
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
4550
4607
|
import { readFile as readFile7, writeFile as writeFile3, unlink as unlink2, rename, readdir as readdir4 } from "node:fs/promises";
|
|
4551
4608
|
import { join as join3, basename as basename2, resolve as resolve4 } from "node:path";
|
|
4552
4609
|
var WINDOWS_GHOSTSCRIPT_NAMES = ["gswin64c", "gswin32c", "gs"];
|
|
@@ -4581,7 +4638,7 @@ async function findWindowsGhostscriptInstall(env) {
|
|
|
4581
4638
|
for (const version of versions) {
|
|
4582
4639
|
for (const executable of ["gswin64c.exe", "gswin32c.exe"]) {
|
|
4583
4640
|
const candidate = join3(installRoot, version.name, "bin", executable);
|
|
4584
|
-
if (
|
|
4641
|
+
if (existsSync5(candidate))
|
|
4585
4642
|
return candidate;
|
|
4586
4643
|
}
|
|
4587
4644
|
}
|
|
@@ -4590,7 +4647,7 @@ async function findWindowsGhostscriptInstall(env) {
|
|
|
4590
4647
|
}
|
|
4591
4648
|
async function resolveGhostscript(targetPlatform = process.platform, env = process.env) {
|
|
4592
4649
|
const override = envValue(env, "GHOSTSCRIPT_PATH");
|
|
4593
|
-
if (override &&
|
|
4650
|
+
if (override && existsSync5(override))
|
|
4594
4651
|
return override;
|
|
4595
4652
|
const names = targetPlatform === "win32" ? WINDOWS_GHOSTSCRIPT_NAMES : POSIX_GHOSTSCRIPT_NAMES;
|
|
4596
4653
|
for (const name of names) {
|
|
@@ -4782,7 +4839,7 @@ import git from "isomorphic-git";
|
|
|
4782
4839
|
// package.json
|
|
4783
4840
|
var package_default = {
|
|
4784
4841
|
name: "gutterpress",
|
|
4785
|
-
version: "0.10.
|
|
4842
|
+
version: "0.10.10-alpha.2",
|
|
4786
4843
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
4787
4844
|
author: "itlackey",
|
|
4788
4845
|
license: "MPL-2.0",
|
|
@@ -5051,491 +5108,163 @@ async function writeBuildFingerprint(input) {
|
|
|
5051
5108
|
return outPath;
|
|
5052
5109
|
}
|
|
5053
5110
|
|
|
5054
|
-
// src/lib/
|
|
5055
|
-
import {
|
|
5056
|
-
import {
|
|
5111
|
+
// src/lib/validation-exec.ts
|
|
5112
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
5113
|
+
import { dirname as dirname4, join as join4, resolve as resolve8 } from "node:path";
|
|
5057
5114
|
|
|
5058
|
-
// src/
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
const urls = [];
|
|
5066
|
-
const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
|
5067
|
-
let m;
|
|
5068
|
-
while (m = re.exec(value))
|
|
5069
|
-
urls.push(m[2] ?? "");
|
|
5070
|
-
return urls;
|
|
5115
|
+
// src/checks/formatter.ts
|
|
5116
|
+
function formatReport(report, format = "text") {
|
|
5117
|
+
if (format === "json") {
|
|
5118
|
+
formatJson(report);
|
|
5119
|
+
} else {
|
|
5120
|
+
formatText(report);
|
|
5121
|
+
}
|
|
5071
5122
|
}
|
|
5072
|
-
function
|
|
5073
|
-
const
|
|
5074
|
-
|
|
5123
|
+
function formatText(report) {
|
|
5124
|
+
for (const r of report.infos) {
|
|
5125
|
+
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5126
|
+
log.info(`${loc}${r.message}`);
|
|
5127
|
+
if (r.detail)
|
|
5128
|
+
log.info(` ${r.detail}`);
|
|
5129
|
+
}
|
|
5130
|
+
for (const r of report.warnings) {
|
|
5131
|
+
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5132
|
+
log.warn(`${loc}${r.message}`);
|
|
5133
|
+
if (r.detail)
|
|
5134
|
+
log.warn(` ${r.detail}`);
|
|
5135
|
+
}
|
|
5136
|
+
for (const r of report.errors) {
|
|
5137
|
+
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5138
|
+
log.error(`${loc}${r.message}`);
|
|
5139
|
+
if (r.detail)
|
|
5140
|
+
log.error(` ${r.detail}`);
|
|
5141
|
+
}
|
|
5142
|
+
const { summary } = report;
|
|
5143
|
+
if (summary.errors > 0) {
|
|
5144
|
+
log.error(`VALIDATION FAILED (${summary.errors} error${summary.errors > 1 ? "s" : ""})`);
|
|
5145
|
+
} else if (summary.warnings > 0) {
|
|
5146
|
+
log.warn("VALIDATION PASSED (with warnings)");
|
|
5147
|
+
} else {
|
|
5148
|
+
log.success("VALIDATION PASSED");
|
|
5149
|
+
}
|
|
5075
5150
|
}
|
|
5076
|
-
|
|
5077
|
-
"
|
|
5078
|
-
"backdrop-filter",
|
|
5079
|
-
"mix-blend-mode",
|
|
5080
|
-
"background-blend-mode",
|
|
5081
|
-
"animation",
|
|
5082
|
-
"animation-name",
|
|
5083
|
-
"transition",
|
|
5084
|
-
"will-change",
|
|
5085
|
-
"clip-path"
|
|
5086
|
-
]);
|
|
5087
|
-
var marginBoxAtRuleNames = new Set([
|
|
5088
|
-
"top-left-corner",
|
|
5089
|
-
"top-left",
|
|
5090
|
-
"top-center",
|
|
5091
|
-
"top-right",
|
|
5092
|
-
"top-right-corner",
|
|
5093
|
-
"bottom-left-corner",
|
|
5094
|
-
"bottom-left",
|
|
5095
|
-
"bottom-center",
|
|
5096
|
-
"bottom-right",
|
|
5097
|
-
"bottom-right-corner",
|
|
5098
|
-
"left-top",
|
|
5099
|
-
"left-middle",
|
|
5100
|
-
"left-bottom",
|
|
5101
|
-
"right-top",
|
|
5102
|
-
"right-middle",
|
|
5103
|
-
"right-bottom"
|
|
5104
|
-
]);
|
|
5105
|
-
var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
|
|
5106
|
-
function isInPageBox(decl) {
|
|
5107
|
-
const page = decl.parent;
|
|
5108
|
-
return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
|
|
5151
|
+
function targetPrefix(target) {
|
|
5152
|
+
return target ? `[${target}] ` : "";
|
|
5109
5153
|
}
|
|
5110
|
-
function
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
return isInPageBox(decl);
|
|
5154
|
+
function formatJson(report) {
|
|
5155
|
+
console.log(JSON.stringify({
|
|
5156
|
+
results: report.results,
|
|
5157
|
+
summary: report.summary,
|
|
5158
|
+
passed: report.passed
|
|
5159
|
+
}, null, 2));
|
|
5117
5160
|
}
|
|
5118
|
-
function
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
if (
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5161
|
+
function formatLocation(file, line, column) {
|
|
5162
|
+
if (!file)
|
|
5163
|
+
return "";
|
|
5164
|
+
let loc = `${file}`;
|
|
5165
|
+
if (line != null) {
|
|
5166
|
+
loc += `:${line}`;
|
|
5167
|
+
if (column != null)
|
|
5168
|
+
loc += `:${column}`;
|
|
5169
|
+
}
|
|
5170
|
+
return `${loc}: `;
|
|
5126
5171
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5172
|
+
|
|
5173
|
+
// src/checks/registry.ts
|
|
5174
|
+
var checks = new Map;
|
|
5175
|
+
function registerCheck(check) {
|
|
5176
|
+
checks.set(check.id, check);
|
|
5177
|
+
return () => {
|
|
5178
|
+
if (checks.get(check.id) === check)
|
|
5179
|
+
checks.delete(check.id);
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
function getChecks(filter) {
|
|
5183
|
+
let result = Array.from(checks.values());
|
|
5184
|
+
if (filter?.category) {
|
|
5185
|
+
const cats = Array.isArray(filter.category) ? filter.category : [filter.category];
|
|
5186
|
+
result = result.filter((c) => cats.includes(c.category));
|
|
5187
|
+
}
|
|
5188
|
+
if (filter?.phase) {
|
|
5189
|
+
result = result.filter((c) => c.phase === filter.phase);
|
|
5190
|
+
}
|
|
5191
|
+
if (filter?.ids) {
|
|
5192
|
+
const idSet = new Set(filter.ids);
|
|
5193
|
+
result = result.filter((c) => idSet.has(c.id));
|
|
5194
|
+
}
|
|
5195
|
+
return result;
|
|
5196
|
+
}
|
|
5197
|
+
function getCheckById(id) {
|
|
5198
|
+
return checks.get(id);
|
|
5199
|
+
}
|
|
5200
|
+
function getKnownCategories() {
|
|
5201
|
+
const categories = new Set;
|
|
5202
|
+
for (const check of checks.values()) {
|
|
5203
|
+
categories.add(check.category);
|
|
5204
|
+
}
|
|
5205
|
+
return Array.from(categories);
|
|
5206
|
+
}
|
|
5207
|
+
function resolveCheckSelectors(selectors) {
|
|
5208
|
+
const allChecks = Array.from(checks.values());
|
|
5209
|
+
const resolved = [];
|
|
5210
|
+
const unmatched = [];
|
|
5211
|
+
const seen = new Set;
|
|
5212
|
+
for (const rawSelector of selectors) {
|
|
5213
|
+
const selector = rawSelector.trim();
|
|
5214
|
+
if (!selector)
|
|
5215
|
+
continue;
|
|
5216
|
+
const matcher = selector.includes("*") ? selectorToRegex(selector) : undefined;
|
|
5217
|
+
let matchedAny = false;
|
|
5218
|
+
for (const check of allChecks) {
|
|
5219
|
+
const matched = matcher ? matcher.test(check.id) : check.id === selector;
|
|
5220
|
+
if (!matched)
|
|
5221
|
+
continue;
|
|
5222
|
+
matchedAny = true;
|
|
5223
|
+
if (seen.has(check.id))
|
|
5224
|
+
continue;
|
|
5225
|
+
seen.add(check.id);
|
|
5226
|
+
resolved.push(check.id);
|
|
5227
|
+
}
|
|
5228
|
+
if (!matchedAny)
|
|
5229
|
+
unmatched.push(selector);
|
|
5230
|
+
}
|
|
5231
|
+
return { resolved, unmatched };
|
|
5232
|
+
}
|
|
5233
|
+
function selectorToRegex(selector) {
|
|
5234
|
+
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
5235
|
+
return new RegExp(`^${escaped}$`);
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
// src/checks/policy.ts
|
|
5239
|
+
var INSPECT_FAILED_CODE = "inspect-failed";
|
|
5240
|
+
function isCheckEnabled(check, config) {
|
|
5241
|
+
const entry = config.validate.checks[check.id];
|
|
5242
|
+
if (entry === false)
|
|
5134
5243
|
return false;
|
|
5135
|
-
|
|
5136
|
-
const decls = siblings.filter((n) => n.type === "decl");
|
|
5137
|
-
if (decls.some((d) => ["height", "block-size"].includes(d.prop.toLowerCase())))
|
|
5244
|
+
if (typeof entry === "object" && entry.enabled === false)
|
|
5138
5245
|
return false;
|
|
5139
|
-
if (
|
|
5246
|
+
if (check.enabledWhen && !check.enabledWhen(config))
|
|
5140
5247
|
return false;
|
|
5141
5248
|
return true;
|
|
5142
5249
|
}
|
|
5143
|
-
function
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
"box-shadow": new Set(["none"]),
|
|
5162
|
-
outline: new Set(["none"]),
|
|
5163
|
-
"outline-style": new Set(["none"]),
|
|
5164
|
-
"will-change": new Set(["auto", "none"]),
|
|
5165
|
-
"clip-path": new Set(["none"]),
|
|
5166
|
-
transition: new Set(["none"]),
|
|
5167
|
-
animation: new Set(["none"]),
|
|
5168
|
-
"animation-name": new Set(["none"]),
|
|
5169
|
-
overflow: new Set(["visible"]),
|
|
5170
|
-
"overflow-x": new Set(["visible"]),
|
|
5171
|
-
"overflow-y": new Set(["visible"])
|
|
5172
|
-
};
|
|
5173
|
-
function isInertValue(prop, value) {
|
|
5174
|
-
const v = value.trim().toLowerCase();
|
|
5175
|
-
return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
|
|
5176
|
-
}
|
|
5177
|
-
function createsStackingContext(decl) {
|
|
5178
|
-
const p = decl.prop.toLowerCase();
|
|
5179
|
-
const v = decl.value.trim().toLowerCase();
|
|
5180
|
-
if (isInertValue(p, v))
|
|
5181
|
-
return false;
|
|
5182
|
-
switch (p) {
|
|
5183
|
-
case "isolation":
|
|
5184
|
-
return v === "isolate";
|
|
5185
|
-
case "contain":
|
|
5186
|
-
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
5187
|
-
case "z-index":
|
|
5188
|
-
case "opacity":
|
|
5189
|
-
case "mix-blend-mode":
|
|
5190
|
-
case "filter":
|
|
5191
|
-
case "backdrop-filter":
|
|
5192
|
-
case "transform":
|
|
5193
|
-
case "perspective":
|
|
5194
|
-
case "will-change":
|
|
5195
|
-
return true;
|
|
5196
|
-
default:
|
|
5197
|
-
return false;
|
|
5198
|
-
}
|
|
5199
|
-
}
|
|
5200
|
-
function clipsDescendants(decl) {
|
|
5201
|
-
const p = decl.prop.toLowerCase();
|
|
5202
|
-
if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
|
|
5203
|
-
return false;
|
|
5204
|
-
const v = decl.value.trim().toLowerCase();
|
|
5205
|
-
return v !== "" && !isInertValue(p, v);
|
|
5206
|
-
}
|
|
5207
|
-
function nodeLoc(node) {
|
|
5208
|
-
return {
|
|
5209
|
-
line: node.source?.start?.line ?? 1,
|
|
5210
|
-
column: node.source?.start?.column ?? 1
|
|
5211
|
-
};
|
|
5212
|
-
}
|
|
5213
|
-
function checkCss(css, from) {
|
|
5214
|
-
let root;
|
|
5215
|
-
try {
|
|
5216
|
-
root = postcss2.parse(css, from ? { from } : undefined);
|
|
5217
|
-
} catch (err) {
|
|
5218
|
-
const e = err;
|
|
5219
|
-
return [
|
|
5220
|
-
{
|
|
5221
|
-
rule: ruleSyntax,
|
|
5222
|
-
severity: "error",
|
|
5223
|
-
message: `CSS syntax error: ${e.reason ?? e.message ?? "unparseable"}`,
|
|
5224
|
-
line: e.line ?? 1,
|
|
5225
|
-
column: e.column ?? 1
|
|
5226
|
-
}
|
|
5227
|
-
];
|
|
5228
|
-
}
|
|
5229
|
-
const warnings = [];
|
|
5230
|
-
const reportRemoteUrls = (value, node) => {
|
|
5231
|
-
for (const url of extractUrls(value)) {
|
|
5232
|
-
if (isRemoteUrl(url)) {
|
|
5233
|
-
warnings.push({
|
|
5234
|
-
rule: ruleRemoteUrls,
|
|
5235
|
-
severity: "error",
|
|
5236
|
-
message: `Remote URL is not allowed in print CSS: ${url}`,
|
|
5237
|
-
...nodeLoc(node)
|
|
5238
|
-
});
|
|
5239
|
-
}
|
|
5240
|
-
}
|
|
5241
|
-
};
|
|
5242
|
-
root.walkDecls((decl) => reportRemoteUrls(decl.value || "", decl));
|
|
5243
|
-
root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
|
|
5244
|
-
root.walkDecls((decl) => {
|
|
5245
|
-
const prop = decl.prop.toLowerCase();
|
|
5246
|
-
if (isInertValue(prop, decl.value))
|
|
5247
|
-
return;
|
|
5248
|
-
if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
|
|
5249
|
-
warnings.push({
|
|
5250
|
-
rule: ruleRiskyProps,
|
|
5251
|
-
severity: "warning",
|
|
5252
|
-
message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored — the chrome renders square/unshadowed. Anything that would paint outside the box (box-shadow, outline, filter) or make it a stacking context (transform, opacity, mix-blend-mode) is dropped; text-shadow, border, border-radius, padding and background gradients all work inside the box.`,
|
|
5253
|
-
...nodeLoc(decl)
|
|
5254
|
-
});
|
|
5255
|
-
} else if (prop === "filter") {
|
|
5256
|
-
warnings.push({
|
|
5257
|
-
rule: ruleRiskyProps,
|
|
5258
|
-
severity: "warning",
|
|
5259
|
-
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").`,
|
|
5260
|
-
...nodeLoc(decl)
|
|
5261
|
-
});
|
|
5262
|
-
} else if (riskyProperties.has(prop)) {
|
|
5263
|
-
warnings.push({
|
|
5264
|
-
rule: ruleRiskyProps,
|
|
5265
|
-
severity: "warning",
|
|
5266
|
-
message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
|
|
5267
|
-
...nodeLoc(decl)
|
|
5268
|
-
});
|
|
5269
|
-
} else if (isDroppedPageGradient(decl)) {
|
|
5270
|
-
warnings.push({
|
|
5271
|
-
rule: ruleRiskyProps,
|
|
5272
|
-
severity: "warning",
|
|
5273
|
-
message: `"${decl.prop}: ${decl.value}" on @page paints nothing in print — Chromium drops a gradient in the @page box's own background, with no error and a valid-looking PDF. A solid colour there paints the whole sheet; the same gradient also works on "html" (it covers the content area, not the sheet) or in a margin box.`,
|
|
5274
|
-
...nodeLoc(decl)
|
|
5275
|
-
});
|
|
5276
|
-
} else if (isUnpairedFitContentWidth(decl)) {
|
|
5277
|
-
warnings.push({
|
|
5278
|
-
rule: ruleRiskyProps,
|
|
5279
|
-
severity: "warning",
|
|
5280
|
-
message: `"${decl.prop}: fit-content" in a @page margin box collapses only the inline axis — the box stays stretched to the full margin band, so a chip/pill renders as a tall rectangle around its text. Add "height: fit-content" (align-self / vertical-align do nothing here).`,
|
|
5281
|
-
...nodeLoc(decl)
|
|
5282
|
-
});
|
|
5283
|
-
}
|
|
5284
|
-
});
|
|
5285
|
-
root.walkRules((rule) => {
|
|
5286
|
-
if (rule.parent?.type === "atrule") {
|
|
5287
|
-
const at = rule.parent.name.toLowerCase();
|
|
5288
|
-
if (at === "page" || marginBoxAtRuleNames.has(at))
|
|
5289
|
-
return;
|
|
5290
|
-
}
|
|
5291
|
-
if (!targetsPageWrapper(rule.selector))
|
|
5292
|
-
return;
|
|
5293
|
-
rule.walkDecls((decl) => {
|
|
5294
|
-
if (clipsDescendants(decl)) {
|
|
5295
|
-
warnings.push({
|
|
5296
|
-
rule: rulePageContainment,
|
|
5297
|
-
severity: "warning",
|
|
5298
|
-
message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" clips out-of-flow descendants: a .gp-bleed plate is cut back to this box's width instead of reaching the sheet edge, and a .gp-behind image is cut the same way. Scope it with :has() (e.g. "${rule.selector}:not(:has(.gp-bleed, .gp-pin))") or drop it. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
|
|
5299
|
-
...nodeLoc(decl)
|
|
5300
|
-
});
|
|
5301
|
-
} else if (createsStackingContext(decl)) {
|
|
5302
|
-
warnings.push({
|
|
5303
|
-
rule: rulePageContainment,
|
|
5304
|
-
severity: "warning",
|
|
5305
|
-
message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" makes the page box a stacking context, so a .gp-behind image is trapped inside it and paints ABOVE the page's text instead of under it. Core keeps .page/.spread at "position: relative; z-index: auto" for this reason. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
|
|
5306
|
-
...nodeLoc(decl)
|
|
5307
|
-
});
|
|
5308
|
-
}
|
|
5309
|
-
});
|
|
5310
|
-
});
|
|
5311
|
-
return warnings;
|
|
5312
|
-
}
|
|
5313
|
-
|
|
5314
|
-
// src/lib/lint-runner.ts
|
|
5315
|
-
async function runLint(opts = {}) {
|
|
5316
|
-
const { glob } = await import("glob");
|
|
5317
|
-
const { manifest, manifestDir } = await loadManifestWithPath(opts.manifest, {
|
|
5318
|
-
explicit: opts.manifest !== undefined
|
|
5319
|
-
});
|
|
5320
|
-
const resolved = resolveConfig({}, manifest);
|
|
5321
|
-
let files;
|
|
5322
|
-
if (opts.files) {
|
|
5323
|
-
files = await glob([opts.files], { nodir: true, ignore: ["**/*.min.css"] });
|
|
5324
|
-
} else {
|
|
5325
|
-
const relStyles = await resolveActiveStyles(manifestDir, resolved.styles);
|
|
5326
|
-
const projectFiles = relStyles.map((rel) => resolve5(manifestDir, rel));
|
|
5327
|
-
let pluginStylePaths = opts.pluginStylePaths;
|
|
5328
|
-
if (pluginStylePaths === undefined) {
|
|
5329
|
-
({ pluginStylePaths } = await loadPluginsWithCss(resolved.extensions, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`)));
|
|
5330
|
-
}
|
|
5331
|
-
files = [...projectFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
|
|
5332
|
-
}
|
|
5333
|
-
if (files.length === 0) {
|
|
5334
|
-
log.warn("No CSS files found to lint");
|
|
5335
|
-
return { ok: true, riskyCount: 0, filesLinted: 0 };
|
|
5336
|
-
}
|
|
5337
|
-
log.info(`Linting ${files.length} CSS file(s)`);
|
|
5338
|
-
let errorCount = 0;
|
|
5339
|
-
let riskyCount = 0;
|
|
5340
|
-
let linted = 0;
|
|
5341
|
-
for (const file of files) {
|
|
5342
|
-
let css;
|
|
5343
|
-
try {
|
|
5344
|
-
css = await readFile9(file, "utf8");
|
|
5345
|
-
} catch (err) {
|
|
5346
|
-
log.error(` ${file}`);
|
|
5347
|
-
log.error(` cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`);
|
|
5348
|
-
errorCount++;
|
|
5349
|
-
continue;
|
|
5350
|
-
}
|
|
5351
|
-
linted++;
|
|
5352
|
-
const warnings = checkCss(css, file);
|
|
5353
|
-
const errors = warnings.filter((w) => w.severity === "error");
|
|
5354
|
-
const risky = warnings.filter((w) => w.rule === ruleRiskyProps);
|
|
5355
|
-
riskyCount += risky.length;
|
|
5356
|
-
if (errors.length > 0) {
|
|
5357
|
-
log.error(` ${file}`);
|
|
5358
|
-
for (const w of errors) {
|
|
5359
|
-
log.error(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
|
|
5360
|
-
}
|
|
5361
|
-
errorCount += errors.length;
|
|
5362
|
-
}
|
|
5363
|
-
if (risky.length > 0) {
|
|
5364
|
-
log.warn(` ${file}`);
|
|
5365
|
-
for (const w of risky) {
|
|
5366
|
-
log.warn(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
|
|
5367
|
-
}
|
|
5368
|
-
}
|
|
5369
|
-
}
|
|
5370
|
-
if (errorCount > 0) {
|
|
5371
|
-
log.error("CSS lint errors found");
|
|
5372
|
-
return { ok: false, riskyCount, filesLinted: linted };
|
|
5373
|
-
}
|
|
5374
|
-
if (riskyCount > 0) {
|
|
5375
|
-
log.warn(`${riskyCount} risky print properties found (may cause rasterization)`);
|
|
5376
|
-
} else {
|
|
5377
|
-
log.success("CSS lint passed");
|
|
5378
|
-
}
|
|
5379
|
-
return { ok: true, riskyCount, filesLinted: linted };
|
|
5380
|
-
}
|
|
5381
|
-
|
|
5382
|
-
// src/lib/validation-exec.ts
|
|
5383
|
-
import { existsSync as existsSync11 } from "node:fs";
|
|
5384
|
-
import { dirname as dirname4, join as join4, resolve as resolve9 } from "node:path";
|
|
5385
|
-
|
|
5386
|
-
// src/checks/formatter.ts
|
|
5387
|
-
function formatReport(report, format = "text") {
|
|
5388
|
-
if (format === "json") {
|
|
5389
|
-
formatJson(report);
|
|
5390
|
-
} else {
|
|
5391
|
-
formatText(report);
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
function formatText(report) {
|
|
5395
|
-
for (const r of report.infos) {
|
|
5396
|
-
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5397
|
-
log.info(`${loc}${r.message}`);
|
|
5398
|
-
if (r.detail)
|
|
5399
|
-
log.info(` ${r.detail}`);
|
|
5400
|
-
}
|
|
5401
|
-
for (const r of report.warnings) {
|
|
5402
|
-
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5403
|
-
log.warn(`${loc}${r.message}`);
|
|
5404
|
-
if (r.detail)
|
|
5405
|
-
log.warn(` ${r.detail}`);
|
|
5406
|
-
}
|
|
5407
|
-
for (const r of report.errors) {
|
|
5408
|
-
const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
|
|
5409
|
-
log.error(`${loc}${r.message}`);
|
|
5410
|
-
if (r.detail)
|
|
5411
|
-
log.error(` ${r.detail}`);
|
|
5412
|
-
}
|
|
5413
|
-
const { summary } = report;
|
|
5414
|
-
if (summary.errors > 0) {
|
|
5415
|
-
log.error(`VALIDATION FAILED (${summary.errors} error${summary.errors > 1 ? "s" : ""})`);
|
|
5416
|
-
} else if (summary.warnings > 0) {
|
|
5417
|
-
log.warn("VALIDATION PASSED (with warnings)");
|
|
5418
|
-
} else {
|
|
5419
|
-
log.success("VALIDATION PASSED");
|
|
5420
|
-
}
|
|
5421
|
-
}
|
|
5422
|
-
function targetPrefix(target) {
|
|
5423
|
-
return target ? `[${target}] ` : "";
|
|
5424
|
-
}
|
|
5425
|
-
function formatJson(report) {
|
|
5426
|
-
console.log(JSON.stringify({
|
|
5427
|
-
results: report.results,
|
|
5428
|
-
summary: report.summary,
|
|
5429
|
-
passed: report.passed
|
|
5430
|
-
}, null, 2));
|
|
5431
|
-
}
|
|
5432
|
-
function formatLocation(file, line, column) {
|
|
5433
|
-
if (!file)
|
|
5434
|
-
return "";
|
|
5435
|
-
let loc = `${file}`;
|
|
5436
|
-
if (line != null) {
|
|
5437
|
-
loc += `:${line}`;
|
|
5438
|
-
if (column != null)
|
|
5439
|
-
loc += `:${column}`;
|
|
5440
|
-
}
|
|
5441
|
-
return `${loc}: `;
|
|
5442
|
-
}
|
|
5443
|
-
|
|
5444
|
-
// src/checks/registry.ts
|
|
5445
|
-
var checks = new Map;
|
|
5446
|
-
function registerCheck(check) {
|
|
5447
|
-
checks.set(check.id, check);
|
|
5448
|
-
return () => {
|
|
5449
|
-
if (checks.get(check.id) === check)
|
|
5450
|
-
checks.delete(check.id);
|
|
5451
|
-
};
|
|
5452
|
-
}
|
|
5453
|
-
function getChecks(filter) {
|
|
5454
|
-
let result = Array.from(checks.values());
|
|
5455
|
-
if (filter?.category) {
|
|
5456
|
-
const cats = Array.isArray(filter.category) ? filter.category : [filter.category];
|
|
5457
|
-
result = result.filter((c) => cats.includes(c.category));
|
|
5458
|
-
}
|
|
5459
|
-
if (filter?.phase) {
|
|
5460
|
-
result = result.filter((c) => c.phase === filter.phase);
|
|
5461
|
-
}
|
|
5462
|
-
if (filter?.ids) {
|
|
5463
|
-
const idSet = new Set(filter.ids);
|
|
5464
|
-
result = result.filter((c) => idSet.has(c.id));
|
|
5465
|
-
}
|
|
5466
|
-
return result;
|
|
5467
|
-
}
|
|
5468
|
-
function getCheckById(id) {
|
|
5469
|
-
return checks.get(id);
|
|
5470
|
-
}
|
|
5471
|
-
function getKnownCategories() {
|
|
5472
|
-
const categories = new Set;
|
|
5473
|
-
for (const check of checks.values()) {
|
|
5474
|
-
categories.add(check.category);
|
|
5475
|
-
}
|
|
5476
|
-
return Array.from(categories);
|
|
5477
|
-
}
|
|
5478
|
-
function resolveCheckSelectors(selectors) {
|
|
5479
|
-
const allChecks = Array.from(checks.values());
|
|
5480
|
-
const resolved = [];
|
|
5481
|
-
const unmatched = [];
|
|
5482
|
-
const seen = new Set;
|
|
5483
|
-
for (const rawSelector of selectors) {
|
|
5484
|
-
const selector = rawSelector.trim();
|
|
5485
|
-
if (!selector)
|
|
5486
|
-
continue;
|
|
5487
|
-
const matcher = selector.includes("*") ? selectorToRegex(selector) : undefined;
|
|
5488
|
-
let matchedAny = false;
|
|
5489
|
-
for (const check of allChecks) {
|
|
5490
|
-
const matched = matcher ? matcher.test(check.id) : check.id === selector;
|
|
5491
|
-
if (!matched)
|
|
5492
|
-
continue;
|
|
5493
|
-
matchedAny = true;
|
|
5494
|
-
if (seen.has(check.id))
|
|
5495
|
-
continue;
|
|
5496
|
-
seen.add(check.id);
|
|
5497
|
-
resolved.push(check.id);
|
|
5498
|
-
}
|
|
5499
|
-
if (!matchedAny)
|
|
5500
|
-
unmatched.push(selector);
|
|
5501
|
-
}
|
|
5502
|
-
return { resolved, unmatched };
|
|
5503
|
-
}
|
|
5504
|
-
function selectorToRegex(selector) {
|
|
5505
|
-
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
5506
|
-
return new RegExp(`^${escaped}$`);
|
|
5507
|
-
}
|
|
5508
|
-
|
|
5509
|
-
// src/checks/policy.ts
|
|
5510
|
-
var INSPECT_FAILED_CODE = "inspect-failed";
|
|
5511
|
-
function isCheckEnabled(check, config) {
|
|
5512
|
-
const entry = config.validate.checks[check.id];
|
|
5513
|
-
if (entry === false)
|
|
5514
|
-
return false;
|
|
5515
|
-
if (typeof entry === "object" && entry.enabled === false)
|
|
5516
|
-
return false;
|
|
5517
|
-
if (check.enabledWhen && !check.enabledWhen(config))
|
|
5518
|
-
return false;
|
|
5519
|
-
return true;
|
|
5520
|
-
}
|
|
5521
|
-
function selectChecks(opts, config) {
|
|
5522
|
-
const unmatched = [];
|
|
5523
|
-
let checks2;
|
|
5524
|
-
if (opts.only && opts.only.length > 0) {
|
|
5525
|
-
const resolved = resolveCheckSelectors(opts.only);
|
|
5526
|
-
checks2 = getChecks({ ids: resolved.resolved });
|
|
5527
|
-
unmatched.push(...resolved.unmatched);
|
|
5528
|
-
} else {
|
|
5529
|
-
checks2 = getChecks({ category: opts.category, phase: opts.phase });
|
|
5530
|
-
}
|
|
5531
|
-
if (opts.skip && opts.skip.length > 0) {
|
|
5532
|
-
const resolved = resolveCheckSelectors(opts.skip);
|
|
5533
|
-
const skipSet = new Set(resolved.resolved);
|
|
5534
|
-
checks2 = checks2.filter((c) => !skipSet.has(c.id));
|
|
5535
|
-
unmatched.push(...resolved.unmatched);
|
|
5536
|
-
}
|
|
5537
|
-
checks2 = checks2.filter((c) => isCheckEnabled(c, config));
|
|
5538
|
-
return { checks: checks2, unmatched };
|
|
5250
|
+
function selectChecks(opts, config) {
|
|
5251
|
+
const unmatched = [];
|
|
5252
|
+
let checks2;
|
|
5253
|
+
if (opts.only && opts.only.length > 0) {
|
|
5254
|
+
const resolved = resolveCheckSelectors(opts.only);
|
|
5255
|
+
checks2 = getChecks({ ids: resolved.resolved });
|
|
5256
|
+
unmatched.push(...resolved.unmatched);
|
|
5257
|
+
} else {
|
|
5258
|
+
checks2 = getChecks({ category: opts.category, phase: opts.phase });
|
|
5259
|
+
}
|
|
5260
|
+
if (opts.skip && opts.skip.length > 0) {
|
|
5261
|
+
const resolved = resolveCheckSelectors(opts.skip);
|
|
5262
|
+
const skipSet = new Set(resolved.resolved);
|
|
5263
|
+
checks2 = checks2.filter((c) => !skipSet.has(c.id));
|
|
5264
|
+
unmatched.push(...resolved.unmatched);
|
|
5265
|
+
}
|
|
5266
|
+
checks2 = checks2.filter((c) => isCheckEnabled(c, config));
|
|
5267
|
+
return { checks: checks2, unmatched };
|
|
5539
5268
|
}
|
|
5540
5269
|
function finding(checkId, init) {
|
|
5541
5270
|
return { checkId, ...init };
|
|
@@ -5551,7 +5280,7 @@ function inspectionFailed(checkId, message, extra = {}) {
|
|
|
5551
5280
|
}
|
|
5552
5281
|
|
|
5553
5282
|
// src/lib/pdf-inspect.ts
|
|
5554
|
-
import { stat, readFile as
|
|
5283
|
+
import { stat, readFile as readFile9 } from "node:fs/promises";
|
|
5555
5284
|
import { getDocumentProxy } from "unpdf";
|
|
5556
5285
|
import * as pdfjs from "unpdf/pdfjs";
|
|
5557
5286
|
function getOps(mod) {
|
|
@@ -5606,7 +5335,7 @@ async function loadPdf(path7) {
|
|
|
5606
5335
|
}
|
|
5607
5336
|
if (hit)
|
|
5608
5337
|
destroyEntry(hit);
|
|
5609
|
-
const docPromise =
|
|
5338
|
+
const docPromise = readFile9(path7).then((buf) => getDocumentProxy(new Uint8Array(buf)));
|
|
5610
5339
|
docCache.delete(path7);
|
|
5611
5340
|
docCache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
|
|
5612
5341
|
evictLru();
|
|
@@ -5804,12 +5533,12 @@ function getOpPass(doc) {
|
|
|
5804
5533
|
return p;
|
|
5805
5534
|
}
|
|
5806
5535
|
function getImagePixelSize(page, name, timeoutMs = 2000) {
|
|
5807
|
-
return new Promise((
|
|
5536
|
+
return new Promise((resolve5) => {
|
|
5808
5537
|
let done = false;
|
|
5809
5538
|
const finish = (v) => {
|
|
5810
5539
|
if (!done) {
|
|
5811
5540
|
done = true;
|
|
5812
|
-
|
|
5541
|
+
resolve5(v);
|
|
5813
5542
|
}
|
|
5814
5543
|
};
|
|
5815
5544
|
const t = setTimeout(() => finish(null), timeoutMs);
|
|
@@ -6065,9 +5794,9 @@ var check3 = {
|
|
|
6065
5794
|
registerCheck(check3);
|
|
6066
5795
|
|
|
6067
5796
|
// src/lib/pdf-parse.ts
|
|
6068
|
-
import { readFile as
|
|
5797
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
6069
5798
|
async function readPdfBytes(pdfPath) {
|
|
6070
|
-
return
|
|
5799
|
+
return readFile10(pdfPath, "latin1");
|
|
6071
5800
|
}
|
|
6072
5801
|
function parseInkCov(out) {
|
|
6073
5802
|
const lines = out.split(/\r?\n/).filter((l) => l.includes("CMYK"));
|
|
@@ -6638,21 +6367,21 @@ var check15 = {
|
|
|
6638
6367
|
registerCheck(check15);
|
|
6639
6368
|
|
|
6640
6369
|
// src/checks/source/markdownlint.ts
|
|
6641
|
-
import { readFile as
|
|
6370
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
6642
6371
|
import { lint } from "markdownlint/sync";
|
|
6643
6372
|
import { parse as parseYaml2 } from "yaml";
|
|
6644
6373
|
|
|
6645
6374
|
// src/checks/source/config-file.ts
|
|
6646
|
-
import { existsSync as
|
|
6647
|
-
import { resolve as
|
|
6375
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
6376
|
+
import { resolve as resolve5 } from "node:path";
|
|
6648
6377
|
function findConfigFile(inputDir, names, explicit) {
|
|
6649
6378
|
if (explicit) {
|
|
6650
|
-
const p =
|
|
6651
|
-
return
|
|
6379
|
+
const p = resolve5(inputDir, explicit);
|
|
6380
|
+
return existsSync6(p) ? p : null;
|
|
6652
6381
|
}
|
|
6653
6382
|
for (const name of names) {
|
|
6654
|
-
const p =
|
|
6655
|
-
if (
|
|
6383
|
+
const p = resolve5(inputDir, name);
|
|
6384
|
+
if (existsSync6(p))
|
|
6656
6385
|
return p;
|
|
6657
6386
|
}
|
|
6658
6387
|
return null;
|
|
@@ -6671,7 +6400,7 @@ function stripJsonComments(s) {
|
|
|
6671
6400
|
return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
6672
6401
|
}
|
|
6673
6402
|
async function loadConfig(configPath) {
|
|
6674
|
-
const raw = await
|
|
6403
|
+
const raw = await readFile11(configPath, "utf8");
|
|
6675
6404
|
let parsed;
|
|
6676
6405
|
if (configPath.endsWith(".json")) {
|
|
6677
6406
|
parsed = JSON.parse(raw);
|
|
@@ -6680,94 +6409,352 @@ async function loadConfig(configPath) {
|
|
|
6680
6409
|
} else {
|
|
6681
6410
|
parsed = parseYaml2(raw);
|
|
6682
6411
|
}
|
|
6683
|
-
if (parsed && typeof parsed === "object") {
|
|
6684
|
-
const obj = parsed;
|
|
6685
|
-
if (obj.config && typeof obj.config === "object") {
|
|
6686
|
-
return obj.config;
|
|
6412
|
+
if (parsed && typeof parsed === "object") {
|
|
6413
|
+
const obj = parsed;
|
|
6414
|
+
if (obj.config && typeof obj.config === "object") {
|
|
6415
|
+
return obj.config;
|
|
6416
|
+
}
|
|
6417
|
+
return obj;
|
|
6418
|
+
}
|
|
6419
|
+
return {};
|
|
6420
|
+
}
|
|
6421
|
+
var check16 = {
|
|
6422
|
+
id: "source.markdownlint",
|
|
6423
|
+
name: "Markdownlint",
|
|
6424
|
+
description: "Runs markdownlint with project config to validate Markdown files",
|
|
6425
|
+
category: "source",
|
|
6426
|
+
phase: "pre-build",
|
|
6427
|
+
async run(ctx) {
|
|
6428
|
+
const sourceConfig = ctx.config.validate.source;
|
|
6429
|
+
if (sourceConfig.markdownlint === false)
|
|
6430
|
+
return [];
|
|
6431
|
+
const files = ctx.markdownFiles;
|
|
6432
|
+
if (!files || files.length === 0)
|
|
6433
|
+
return [];
|
|
6434
|
+
const configPath = typeof sourceConfig.markdownlint === "string" ? sourceConfig.markdownlint : null;
|
|
6435
|
+
const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES, configPath);
|
|
6436
|
+
if (!resolvedConfig && !configPath)
|
|
6437
|
+
return [];
|
|
6438
|
+
const config = resolvedConfig ? await loadConfig(resolvedConfig) : { default: true };
|
|
6439
|
+
const results = lint({ files, config });
|
|
6440
|
+
const out = [];
|
|
6441
|
+
for (const [file, violations] of Object.entries(results)) {
|
|
6442
|
+
if (!Array.isArray(violations))
|
|
6443
|
+
continue;
|
|
6444
|
+
for (const v of violations) {
|
|
6445
|
+
out.push({
|
|
6446
|
+
checkId: check16.id,
|
|
6447
|
+
severity: "warning",
|
|
6448
|
+
message: `${v.ruleDescription} (${v.ruleNames.join("/")})`,
|
|
6449
|
+
file,
|
|
6450
|
+
line: v.lineNumber,
|
|
6451
|
+
column: v.errorRange?.[0],
|
|
6452
|
+
code: v.ruleNames[0]
|
|
6453
|
+
});
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
return out;
|
|
6457
|
+
}
|
|
6458
|
+
};
|
|
6459
|
+
registerCheck(check16);
|
|
6460
|
+
|
|
6461
|
+
// src/checks/source/htmlhint.ts
|
|
6462
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
6463
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
6464
|
+
import htmlhint from "htmlhint";
|
|
6465
|
+
var { HTMLHint } = htmlhint;
|
|
6466
|
+
var CONFIG_NAMES2 = [".htmlhintrc"];
|
|
6467
|
+
var check17 = {
|
|
6468
|
+
id: "source.htmlhint",
|
|
6469
|
+
name: "HTMLHint",
|
|
6470
|
+
description: "Runs htmlhint with project config to validate generated HTML",
|
|
6471
|
+
category: "source",
|
|
6472
|
+
phase: "pre-build",
|
|
6473
|
+
async run(ctx) {
|
|
6474
|
+
const sourceConfig = ctx.config.validate.source;
|
|
6475
|
+
if (sourceConfig.htmlhint === false)
|
|
6476
|
+
return [];
|
|
6477
|
+
if (!ctx.htmlPath || !existsSync7(ctx.htmlPath))
|
|
6478
|
+
return [];
|
|
6479
|
+
const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
|
|
6480
|
+
const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
|
|
6481
|
+
if (!resolvedConfig && !configPath)
|
|
6482
|
+
return [];
|
|
6483
|
+
const ruleset = resolvedConfig ? JSON.parse(await readFile12(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
|
|
6484
|
+
const html = await readFile12(ctx.htmlPath, "utf8");
|
|
6485
|
+
const messages = HTMLHint.verify(html, ruleset);
|
|
6486
|
+
return messages.map((m) => ({
|
|
6487
|
+
checkId: check17.id,
|
|
6488
|
+
severity: m.type === "error" ? "error" : "warning",
|
|
6489
|
+
message: `${m.rule.id}: ${m.message}`,
|
|
6490
|
+
file: ctx.htmlPath,
|
|
6491
|
+
line: m.line,
|
|
6492
|
+
column: m.col
|
|
6493
|
+
}));
|
|
6494
|
+
}
|
|
6495
|
+
};
|
|
6496
|
+
registerCheck(check17);
|
|
6497
|
+
|
|
6498
|
+
// src/checks/source/stylelint.ts
|
|
6499
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
6500
|
+
|
|
6501
|
+
// src/lib/printsafe.ts
|
|
6502
|
+
import postcss2 from "postcss";
|
|
6503
|
+
var ruleRemoteUrls = "printsafe/no-remote-urls";
|
|
6504
|
+
var ruleRiskyProps = "printsafe/no-risky-print-effects";
|
|
6505
|
+
var ruleSyntax = "printsafe/syntax-error";
|
|
6506
|
+
var rulePageContainment = "printsafe/page-containment";
|
|
6507
|
+
function extractUrls(value) {
|
|
6508
|
+
const urls = [];
|
|
6509
|
+
const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
|
6510
|
+
let m;
|
|
6511
|
+
while (m = re.exec(value))
|
|
6512
|
+
urls.push(m[2] ?? "");
|
|
6513
|
+
return urls;
|
|
6514
|
+
}
|
|
6515
|
+
function isRemoteUrl(url) {
|
|
6516
|
+
const lower = url.trim().toLowerCase();
|
|
6517
|
+
return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("//");
|
|
6518
|
+
}
|
|
6519
|
+
var riskyProperties = new Set([
|
|
6520
|
+
"filter",
|
|
6521
|
+
"backdrop-filter",
|
|
6522
|
+
"mix-blend-mode",
|
|
6523
|
+
"background-blend-mode",
|
|
6524
|
+
"animation",
|
|
6525
|
+
"animation-name",
|
|
6526
|
+
"transition",
|
|
6527
|
+
"will-change",
|
|
6528
|
+
"clip-path"
|
|
6529
|
+
]);
|
|
6530
|
+
var marginBoxAtRuleNames = new Set([
|
|
6531
|
+
"top-left-corner",
|
|
6532
|
+
"top-left",
|
|
6533
|
+
"top-center",
|
|
6534
|
+
"top-right",
|
|
6535
|
+
"top-right-corner",
|
|
6536
|
+
"bottom-left-corner",
|
|
6537
|
+
"bottom-left",
|
|
6538
|
+
"bottom-center",
|
|
6539
|
+
"bottom-right",
|
|
6540
|
+
"bottom-right-corner",
|
|
6541
|
+
"left-top",
|
|
6542
|
+
"left-middle",
|
|
6543
|
+
"left-bottom",
|
|
6544
|
+
"right-top",
|
|
6545
|
+
"right-middle",
|
|
6546
|
+
"right-bottom"
|
|
6547
|
+
]);
|
|
6548
|
+
var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
|
|
6549
|
+
function isInPageBox(decl) {
|
|
6550
|
+
const page = decl.parent;
|
|
6551
|
+
return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
|
|
6552
|
+
}
|
|
6553
|
+
function isDroppedPageGradient(decl) {
|
|
6554
|
+
const prop = decl.prop.toLowerCase();
|
|
6555
|
+
if (prop !== "background" && prop !== "background-image")
|
|
6556
|
+
return false;
|
|
6557
|
+
if (!/(^|[\s,(])[\w-]*gradient\(/i.test(decl.value))
|
|
6558
|
+
return false;
|
|
6559
|
+
return isInPageBox(decl);
|
|
6560
|
+
}
|
|
6561
|
+
function isInPageMarginBox(decl) {
|
|
6562
|
+
const box = decl.parent;
|
|
6563
|
+
if (!box || box.type !== "atrule")
|
|
6564
|
+
return false;
|
|
6565
|
+
if (!marginBoxAtRuleNames.has(box.name.toLowerCase()))
|
|
6566
|
+
return false;
|
|
6567
|
+
const page = box.parent;
|
|
6568
|
+
return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
|
|
6569
|
+
}
|
|
6570
|
+
function isUnpairedFitContentWidth(decl) {
|
|
6571
|
+
const prop = decl.prop.toLowerCase();
|
|
6572
|
+
if (prop !== "width" && prop !== "inline-size")
|
|
6573
|
+
return false;
|
|
6574
|
+
if (!/\bfit-content\b/i.test(decl.value))
|
|
6575
|
+
return false;
|
|
6576
|
+
if (!isInPageMarginBox(decl))
|
|
6577
|
+
return false;
|
|
6578
|
+
const siblings = decl.parent.nodes ?? [];
|
|
6579
|
+
const decls = siblings.filter((n) => n.type === "decl");
|
|
6580
|
+
if (decls.some((d) => ["height", "block-size"].includes(d.prop.toLowerCase())))
|
|
6581
|
+
return false;
|
|
6582
|
+
if (decls.some((d) => /gradient\(/i.test(d.value)))
|
|
6583
|
+
return false;
|
|
6584
|
+
return true;
|
|
6585
|
+
}
|
|
6586
|
+
function targetsPageWrapper(selector) {
|
|
6587
|
+
if (!/\.(page|spread)(?![\w-])/.test(selector))
|
|
6588
|
+
return false;
|
|
6589
|
+
return !/\bgp-(bleed|pin)\b/.test(selector);
|
|
6590
|
+
}
|
|
6591
|
+
var cssWideResetKeywords = new Set(["initial", "unset", "revert", "revert-layer"]);
|
|
6592
|
+
var inertValues = {
|
|
6593
|
+
"z-index": new Set(["auto"]),
|
|
6594
|
+
opacity: new Set(["1", "100%"]),
|
|
6595
|
+
"mix-blend-mode": new Set(["normal"]),
|
|
6596
|
+
"background-blend-mode": new Set(["normal"]),
|
|
6597
|
+
filter: new Set(["none"]),
|
|
6598
|
+
"backdrop-filter": new Set(["none"]),
|
|
6599
|
+
transform: new Set(["none"]),
|
|
6600
|
+
rotate: new Set(["none"]),
|
|
6601
|
+
translate: new Set(["none"]),
|
|
6602
|
+
scale: new Set(["none"]),
|
|
6603
|
+
perspective: new Set(["none"]),
|
|
6604
|
+
"box-shadow": new Set(["none"]),
|
|
6605
|
+
outline: new Set(["none"]),
|
|
6606
|
+
"outline-style": new Set(["none"]),
|
|
6607
|
+
"will-change": new Set(["auto", "none"]),
|
|
6608
|
+
"clip-path": new Set(["none"]),
|
|
6609
|
+
transition: new Set(["none"]),
|
|
6610
|
+
animation: new Set(["none"]),
|
|
6611
|
+
"animation-name": new Set(["none"]),
|
|
6612
|
+
overflow: new Set(["visible"]),
|
|
6613
|
+
"overflow-x": new Set(["visible"]),
|
|
6614
|
+
"overflow-y": new Set(["visible"])
|
|
6615
|
+
};
|
|
6616
|
+
function isInertValue(prop, value) {
|
|
6617
|
+
const v = value.trim().toLowerCase();
|
|
6618
|
+
return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
|
|
6619
|
+
}
|
|
6620
|
+
function createsStackingContext(decl) {
|
|
6621
|
+
const p = decl.prop.toLowerCase();
|
|
6622
|
+
const v = decl.value.trim().toLowerCase();
|
|
6623
|
+
if (isInertValue(p, v))
|
|
6624
|
+
return false;
|
|
6625
|
+
switch (p) {
|
|
6626
|
+
case "isolation":
|
|
6627
|
+
return v === "isolate";
|
|
6628
|
+
case "contain":
|
|
6629
|
+
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
6630
|
+
case "z-index":
|
|
6631
|
+
case "opacity":
|
|
6632
|
+
case "mix-blend-mode":
|
|
6633
|
+
case "filter":
|
|
6634
|
+
case "backdrop-filter":
|
|
6635
|
+
case "transform":
|
|
6636
|
+
case "perspective":
|
|
6637
|
+
case "will-change":
|
|
6638
|
+
return true;
|
|
6639
|
+
default:
|
|
6640
|
+
return false;
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
function clipsDescendants(decl) {
|
|
6644
|
+
const p = decl.prop.toLowerCase();
|
|
6645
|
+
if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
|
|
6646
|
+
return false;
|
|
6647
|
+
const v = decl.value.trim().toLowerCase();
|
|
6648
|
+
return v !== "" && !isInertValue(p, v);
|
|
6649
|
+
}
|
|
6650
|
+
function nodeLoc(node) {
|
|
6651
|
+
return {
|
|
6652
|
+
line: node.source?.start?.line ?? 1,
|
|
6653
|
+
column: node.source?.start?.column ?? 1
|
|
6654
|
+
};
|
|
6655
|
+
}
|
|
6656
|
+
function checkCss(css, from) {
|
|
6657
|
+
let root;
|
|
6658
|
+
try {
|
|
6659
|
+
root = postcss2.parse(css, from ? { from } : undefined);
|
|
6660
|
+
} catch (err) {
|
|
6661
|
+
const e = err;
|
|
6662
|
+
return [
|
|
6663
|
+
{
|
|
6664
|
+
rule: ruleSyntax,
|
|
6665
|
+
severity: "error",
|
|
6666
|
+
message: `CSS syntax error: ${e.reason ?? e.message ?? "unparseable"}`,
|
|
6667
|
+
line: e.line ?? 1,
|
|
6668
|
+
column: e.column ?? 1
|
|
6669
|
+
}
|
|
6670
|
+
];
|
|
6671
|
+
}
|
|
6672
|
+
const warnings = [];
|
|
6673
|
+
const reportRemoteUrls = (value, node) => {
|
|
6674
|
+
for (const url of extractUrls(value)) {
|
|
6675
|
+
if (isRemoteUrl(url)) {
|
|
6676
|
+
warnings.push({
|
|
6677
|
+
rule: ruleRemoteUrls,
|
|
6678
|
+
severity: "error",
|
|
6679
|
+
message: `Remote URL is not allowed in print CSS: ${url}`,
|
|
6680
|
+
...nodeLoc(node)
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6687
6683
|
}
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6684
|
+
};
|
|
6685
|
+
root.walkDecls((decl) => reportRemoteUrls(decl.value || "", decl));
|
|
6686
|
+
root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
|
|
6687
|
+
root.walkDecls((decl) => {
|
|
6688
|
+
const prop = decl.prop.toLowerCase();
|
|
6689
|
+
if (isInertValue(prop, decl.value))
|
|
6690
|
+
return;
|
|
6691
|
+
if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
|
|
6692
|
+
warnings.push({
|
|
6693
|
+
rule: ruleRiskyProps,
|
|
6694
|
+
severity: "warning",
|
|
6695
|
+
message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored — the chrome renders square/unshadowed. Anything that would paint outside the box (box-shadow, outline, filter) or make it a stacking context (transform, opacity, mix-blend-mode) is dropped; text-shadow, border, border-radius, padding and background gradients all work inside the box.`,
|
|
6696
|
+
...nodeLoc(decl)
|
|
6697
|
+
});
|
|
6698
|
+
} else if (prop === "filter") {
|
|
6699
|
+
warnings.push({
|
|
6700
|
+
rule: ruleRiskyProps,
|
|
6701
|
+
severity: "warning",
|
|
6702
|
+
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").`,
|
|
6703
|
+
...nodeLoc(decl)
|
|
6704
|
+
});
|
|
6705
|
+
} else if (riskyProperties.has(prop)) {
|
|
6706
|
+
warnings.push({
|
|
6707
|
+
rule: ruleRiskyProps,
|
|
6708
|
+
severity: "warning",
|
|
6709
|
+
message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
|
|
6710
|
+
...nodeLoc(decl)
|
|
6711
|
+
});
|
|
6712
|
+
} else if (isDroppedPageGradient(decl)) {
|
|
6713
|
+
warnings.push({
|
|
6714
|
+
rule: ruleRiskyProps,
|
|
6715
|
+
severity: "warning",
|
|
6716
|
+
message: `"${decl.prop}: ${decl.value}" on @page paints nothing in print — Chromium drops a gradient in the @page box's own background, with no error and a valid-looking PDF. A solid colour there paints the whole sheet; the same gradient also works on "html" (it covers the content area, not the sheet) or in a margin box.`,
|
|
6717
|
+
...nodeLoc(decl)
|
|
6718
|
+
});
|
|
6719
|
+
} else if (isUnpairedFitContentWidth(decl)) {
|
|
6720
|
+
warnings.push({
|
|
6721
|
+
rule: ruleRiskyProps,
|
|
6722
|
+
severity: "warning",
|
|
6723
|
+
message: `"${decl.prop}: fit-content" in a @page margin box collapses only the inline axis — the box stays stretched to the full margin band, so a chip/pill renders as a tall rectangle around its text. Add "height: fit-content" (align-self / vertical-align do nothing here).`,
|
|
6724
|
+
...nodeLoc(decl)
|
|
6725
|
+
});
|
|
6726
|
+
}
|
|
6727
|
+
});
|
|
6728
|
+
root.walkRules((rule) => {
|
|
6729
|
+
if (rule.parent?.type === "atrule") {
|
|
6730
|
+
const at = rule.parent.name.toLowerCase();
|
|
6731
|
+
if (at === "page" || marginBoxAtRuleNames.has(at))
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
if (!targetsPageWrapper(rule.selector))
|
|
6735
|
+
return;
|
|
6736
|
+
rule.walkDecls((decl) => {
|
|
6737
|
+
if (clipsDescendants(decl)) {
|
|
6738
|
+
warnings.push({
|
|
6739
|
+
rule: rulePageContainment,
|
|
6718
6740
|
severity: "warning",
|
|
6719
|
-
message:
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6741
|
+
message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" clips out-of-flow descendants: a .gp-bleed plate is cut back to this box's width instead of reaching the sheet edge, and a .gp-behind image is cut the same way. Scope it with :has() (e.g. "${rule.selector}:not(:has(.gp-bleed, .gp-pin))") or drop it. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
|
|
6742
|
+
...nodeLoc(decl)
|
|
6743
|
+
});
|
|
6744
|
+
} else if (createsStackingContext(decl)) {
|
|
6745
|
+
warnings.push({
|
|
6746
|
+
rule: rulePageContainment,
|
|
6747
|
+
severity: "warning",
|
|
6748
|
+
message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" makes the page box a stacking context, so a .gp-behind image is trapped inside it and paints ABOVE the page's text instead of under it. Core keeps .page/.spread at "position: relative; z-index: auto" for this reason. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
|
|
6749
|
+
...nodeLoc(decl)
|
|
6724
6750
|
});
|
|
6725
6751
|
}
|
|
6726
|
-
}
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
}
|
|
6730
|
-
registerCheck(check16);
|
|
6731
|
-
|
|
6732
|
-
// src/checks/source/htmlhint.ts
|
|
6733
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
6734
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
6735
|
-
import htmlhint from "htmlhint";
|
|
6736
|
-
var { HTMLHint } = htmlhint;
|
|
6737
|
-
var CONFIG_NAMES2 = [".htmlhintrc"];
|
|
6738
|
-
var check17 = {
|
|
6739
|
-
id: "source.htmlhint",
|
|
6740
|
-
name: "HTMLHint",
|
|
6741
|
-
description: "Runs htmlhint with project config to validate generated HTML",
|
|
6742
|
-
category: "source",
|
|
6743
|
-
phase: "pre-build",
|
|
6744
|
-
async run(ctx) {
|
|
6745
|
-
const sourceConfig = ctx.config.validate.source;
|
|
6746
|
-
if (sourceConfig.htmlhint === false)
|
|
6747
|
-
return [];
|
|
6748
|
-
if (!ctx.htmlPath || !existsSync8(ctx.htmlPath))
|
|
6749
|
-
return [];
|
|
6750
|
-
const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
|
|
6751
|
-
const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
|
|
6752
|
-
if (!resolvedConfig && !configPath)
|
|
6753
|
-
return [];
|
|
6754
|
-
const ruleset = resolvedConfig ? JSON.parse(await readFile13(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
|
|
6755
|
-
const html = await readFile13(ctx.htmlPath, "utf8");
|
|
6756
|
-
const messages = HTMLHint.verify(html, ruleset);
|
|
6757
|
-
return messages.map((m) => ({
|
|
6758
|
-
checkId: check17.id,
|
|
6759
|
-
severity: m.type === "error" ? "error" : "warning",
|
|
6760
|
-
message: `${m.rule.id}: ${m.message}`,
|
|
6761
|
-
file: ctx.htmlPath,
|
|
6762
|
-
line: m.line,
|
|
6763
|
-
column: m.col
|
|
6764
|
-
}));
|
|
6765
|
-
}
|
|
6766
|
-
};
|
|
6767
|
-
registerCheck(check17);
|
|
6752
|
+
});
|
|
6753
|
+
});
|
|
6754
|
+
return warnings;
|
|
6755
|
+
}
|
|
6768
6756
|
|
|
6769
6757
|
// src/checks/source/stylelint.ts
|
|
6770
|
-
import { readFile as readFile14 } from "node:fs/promises";
|
|
6771
6758
|
var check18 = {
|
|
6772
6759
|
id: "source.stylelint",
|
|
6773
6760
|
name: "Print-safety CSS",
|
|
@@ -6785,8 +6772,14 @@ var check18 = {
|
|
|
6785
6772
|
for (const file of files) {
|
|
6786
6773
|
let css;
|
|
6787
6774
|
try {
|
|
6788
|
-
css = await
|
|
6789
|
-
} catch {
|
|
6775
|
+
css = await readFile13(file, "utf8");
|
|
6776
|
+
} catch (err) {
|
|
6777
|
+
results.push({
|
|
6778
|
+
checkId: check18.id,
|
|
6779
|
+
severity: "error",
|
|
6780
|
+
message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
|
|
6781
|
+
file
|
|
6782
|
+
});
|
|
6790
6783
|
continue;
|
|
6791
6784
|
}
|
|
6792
6785
|
for (const w of checkCss(css, file)) {
|
|
@@ -6806,12 +6799,12 @@ var check18 = {
|
|
|
6806
6799
|
registerCheck(check18);
|
|
6807
6800
|
|
|
6808
6801
|
// src/checks/source/css-ownership.ts
|
|
6809
|
-
import { readFile as
|
|
6802
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
6810
6803
|
import postcss3 from "postcss";
|
|
6811
6804
|
import { parse as parseYaml3 } from "yaml";
|
|
6812
6805
|
var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
|
|
6813
6806
|
async function loadContract(configPath) {
|
|
6814
|
-
const raw = await
|
|
6807
|
+
const raw = await readFile14(configPath, "utf8");
|
|
6815
6808
|
const parsed = parseYaml3(raw);
|
|
6816
6809
|
if (parsed && typeof parsed === "object")
|
|
6817
6810
|
return parsed;
|
|
@@ -6962,7 +6955,7 @@ var check19 = {
|
|
|
6962
6955
|
continue;
|
|
6963
6956
|
let css;
|
|
6964
6957
|
try {
|
|
6965
|
-
css = await
|
|
6958
|
+
css = await readFile14(absFile, "utf8");
|
|
6966
6959
|
} catch {
|
|
6967
6960
|
continue;
|
|
6968
6961
|
}
|
|
@@ -7004,11 +6997,11 @@ var check19 = {
|
|
|
7004
6997
|
registerCheck(check19);
|
|
7005
6998
|
|
|
7006
6999
|
// src/checks/source/local-refs.ts
|
|
7007
|
-
import { existsSync as
|
|
7008
|
-
import { dirname as dirname3, resolve as
|
|
7000
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
7001
|
+
import { dirname as dirname3, resolve as resolve6 } from "node:path";
|
|
7009
7002
|
|
|
7010
7003
|
// src/checks/source/local-ref-parser.ts
|
|
7011
|
-
import { readFile as
|
|
7004
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
7012
7005
|
function newlineOffsets(content) {
|
|
7013
7006
|
const offsets = [];
|
|
7014
7007
|
for (let i = 0;i < content.length; i++)
|
|
@@ -7077,7 +7070,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
|
|
|
7077
7070
|
for (const file of files) {
|
|
7078
7071
|
let refs;
|
|
7079
7072
|
try {
|
|
7080
|
-
refs = collectRenderedLocalRefs(await
|
|
7073
|
+
refs = collectRenderedLocalRefs(await readFile15(file, "utf8"));
|
|
7081
7074
|
} catch {
|
|
7082
7075
|
results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
|
|
7083
7076
|
continue;
|
|
@@ -7127,13 +7120,13 @@ function filesystemRef(ref) {
|
|
|
7127
7120
|
}
|
|
7128
7121
|
function localRefExists(ref, kind, sourceFile, projectRoot) {
|
|
7129
7122
|
const cleaned = filesystemRef(ref);
|
|
7130
|
-
const fileRelative =
|
|
7131
|
-
const rootRelative =
|
|
7123
|
+
const fileRelative = resolve6(dirname3(sourceFile), cleaned);
|
|
7124
|
+
const rootRelative = resolve6(projectRoot, cleaned);
|
|
7132
7125
|
switch (kind) {
|
|
7133
7126
|
case "image":
|
|
7134
|
-
return
|
|
7127
|
+
return existsSync8(rootRelative);
|
|
7135
7128
|
case "link":
|
|
7136
|
-
return
|
|
7129
|
+
return existsSync8(fileRelative);
|
|
7137
7130
|
}
|
|
7138
7131
|
}
|
|
7139
7132
|
registerCheck(check20);
|
|
@@ -7428,7 +7421,7 @@ var check21 = {
|
|
|
7428
7421
|
registerCheck(check21);
|
|
7429
7422
|
|
|
7430
7423
|
// src/checks/source/accessibility-alt-text.ts
|
|
7431
|
-
import { readFile as
|
|
7424
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
7432
7425
|
var check22 = {
|
|
7433
7426
|
id: "source.accessibility.alt-text",
|
|
7434
7427
|
name: "Image Alt Text",
|
|
@@ -7442,7 +7435,7 @@ var check22 = {
|
|
|
7442
7435
|
const results = [];
|
|
7443
7436
|
for (const file of files) {
|
|
7444
7437
|
try {
|
|
7445
|
-
const content = await
|
|
7438
|
+
const content = await readFile16(file, "utf8");
|
|
7446
7439
|
const lines = content.split(`
|
|
7447
7440
|
`);
|
|
7448
7441
|
let inFence = false;
|
|
@@ -7486,7 +7479,7 @@ function extractImageAlts(line) {
|
|
|
7486
7479
|
registerCheck(check22);
|
|
7487
7480
|
|
|
7488
7481
|
// src/checks/source/accessibility-heading-order.ts
|
|
7489
|
-
import { readFile as
|
|
7482
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
7490
7483
|
var check23 = {
|
|
7491
7484
|
id: "source.accessibility.heading-order",
|
|
7492
7485
|
name: "Heading Order",
|
|
@@ -7500,7 +7493,7 @@ var check23 = {
|
|
|
7500
7493
|
const results = [];
|
|
7501
7494
|
for (const file of files) {
|
|
7502
7495
|
try {
|
|
7503
|
-
const content = await
|
|
7496
|
+
const content = await readFile17(file, "utf8");
|
|
7504
7497
|
const lines = content.split(`
|
|
7505
7498
|
`);
|
|
7506
7499
|
let inFence = false;
|
|
@@ -7536,7 +7529,7 @@ var check23 = {
|
|
|
7536
7529
|
registerCheck(check23);
|
|
7537
7530
|
|
|
7538
7531
|
// src/checks/source/layout-markers.ts
|
|
7539
|
-
import { readFile as
|
|
7532
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
7540
7533
|
var check24 = {
|
|
7541
7534
|
id: "source.markdown.layout-markers",
|
|
7542
7535
|
name: "Layout Markers",
|
|
@@ -7554,7 +7547,7 @@ var check24 = {
|
|
|
7554
7547
|
const md = createMarkdownRenderer(plugins);
|
|
7555
7548
|
for (const file of files) {
|
|
7556
7549
|
try {
|
|
7557
|
-
const content = await
|
|
7550
|
+
const content = await readFile18(file, "utf8");
|
|
7558
7551
|
const env = {};
|
|
7559
7552
|
md.render(content, env);
|
|
7560
7553
|
for (const w of env.layoutWarnings ?? []) {
|
|
@@ -7576,7 +7569,7 @@ var check24 = {
|
|
|
7576
7569
|
registerCheck(check24);
|
|
7577
7570
|
|
|
7578
7571
|
// src/checks/source/merge-markers.ts
|
|
7579
|
-
import { readFile as
|
|
7572
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
7580
7573
|
import path8 from "node:path";
|
|
7581
7574
|
|
|
7582
7575
|
// src/checks/asset/extensions.ts
|
|
@@ -7618,7 +7611,7 @@ var check25 = {
|
|
|
7618
7611
|
for (const file of textFiles) {
|
|
7619
7612
|
let content;
|
|
7620
7613
|
try {
|
|
7621
|
-
content = await
|
|
7614
|
+
content = await readFile19(file, "utf8");
|
|
7622
7615
|
} catch {
|
|
7623
7616
|
results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
|
|
7624
7617
|
continue;
|
|
@@ -7681,7 +7674,7 @@ registerCheck(check25);
|
|
|
7681
7674
|
import { stat as stat3 } from "node:fs/promises";
|
|
7682
7675
|
|
|
7683
7676
|
// src/lib/image-inspect.ts
|
|
7684
|
-
import { stat as stat2, readFile as
|
|
7677
|
+
import { stat as stat2, readFile as readFile20 } from "node:fs/promises";
|
|
7685
7678
|
var DEFAULT_DPI = 72;
|
|
7686
7679
|
var cache = new Map;
|
|
7687
7680
|
async function inspectImage(path9) {
|
|
@@ -7696,7 +7689,7 @@ async function inspectImage(path9) {
|
|
|
7696
7689
|
return hit.info;
|
|
7697
7690
|
let info2 = null;
|
|
7698
7691
|
try {
|
|
7699
|
-
const buf = await
|
|
7692
|
+
const buf = await readFile20(path9);
|
|
7700
7693
|
info2 = parseImage(buf);
|
|
7701
7694
|
} catch {
|
|
7702
7695
|
info2 = null;
|
|
@@ -8155,8 +8148,8 @@ var check31 = {
|
|
|
8155
8148
|
registerCheck(check31);
|
|
8156
8149
|
|
|
8157
8150
|
// src/checks/asset/font-license.ts
|
|
8158
|
-
import { existsSync as
|
|
8159
|
-
import { resolve as
|
|
8151
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
8152
|
+
import { resolve as resolve7 } from "node:path";
|
|
8160
8153
|
var LICENSE_NAMES = [
|
|
8161
8154
|
"LICENSE",
|
|
8162
8155
|
"LICENSE.txt",
|
|
@@ -8186,12 +8179,12 @@ var check32 = {
|
|
|
8186
8179
|
ignore: [...ASSET_SCAN_IGNORE_GLOBS]
|
|
8187
8180
|
});
|
|
8188
8181
|
for (const m of matches) {
|
|
8189
|
-
fontDirs.add(
|
|
8182
|
+
fontDirs.add(resolve7(m, ".."));
|
|
8190
8183
|
}
|
|
8191
8184
|
}
|
|
8192
8185
|
const results = [];
|
|
8193
8186
|
for (const fontDir of fontDirs) {
|
|
8194
|
-
const hasLicense = LICENSE_NAMES.some((name) =>
|
|
8187
|
+
const hasLicense = LICENSE_NAMES.some((name) => existsSync9(resolve7(fontDir, name)));
|
|
8195
8188
|
if (!hasLicense) {
|
|
8196
8189
|
results.push({
|
|
8197
8190
|
checkId: check32.id,
|
|
@@ -8256,7 +8249,7 @@ var check33 = {
|
|
|
8256
8249
|
registerCheck(check33);
|
|
8257
8250
|
|
|
8258
8251
|
// src/checks/heuristic/section-density.ts
|
|
8259
|
-
import { readFile as
|
|
8252
|
+
import { readFile as readFile21 } from "node:fs/promises";
|
|
8260
8253
|
var check34 = {
|
|
8261
8254
|
id: "heuristic.chunking.section-density",
|
|
8262
8255
|
name: "Section Density",
|
|
@@ -8273,7 +8266,7 @@ var check34 = {
|
|
|
8273
8266
|
const results = [];
|
|
8274
8267
|
for (const file of files) {
|
|
8275
8268
|
try {
|
|
8276
|
-
const content = await
|
|
8269
|
+
const content = await readFile21(file, "utf8");
|
|
8277
8270
|
const lines = content.split(`
|
|
8278
8271
|
`);
|
|
8279
8272
|
let currentSectionStart = 0;
|
|
@@ -8542,13 +8535,13 @@ async function sharedAssetDirs(manifestDir, relStyles, inputDir) {
|
|
|
8542
8535
|
} catch {
|
|
8543
8536
|
return [];
|
|
8544
8537
|
}
|
|
8545
|
-
const projectRoot =
|
|
8538
|
+
const projectRoot = resolve8(inputDir);
|
|
8546
8539
|
const dirs = new Set;
|
|
8547
8540
|
for (const file of closure) {
|
|
8548
8541
|
const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
|
|
8549
8542
|
if (!SHIPPED_ASSET_EXTS.has(ext))
|
|
8550
8543
|
continue;
|
|
8551
|
-
const dir = dirname4(
|
|
8544
|
+
const dir = dirname4(resolve8(file));
|
|
8552
8545
|
if (escapesProjectRoot(projectRoot, dir))
|
|
8553
8546
|
dirs.add(dir);
|
|
8554
8547
|
}
|
|
@@ -8709,11 +8702,11 @@ function planRuns(opts) {
|
|
|
8709
8702
|
async function executeValidation(args) {
|
|
8710
8703
|
const manifestPath = typeof args.manifest === "string" ? args.manifest : undefined;
|
|
8711
8704
|
const { manifest, manifestDir } = await loadManifestWithPath(manifestPath ?? args.input ?? undefined, { explicit: manifestPath !== undefined });
|
|
8712
|
-
const config = resolveConfig({}, manifest);
|
|
8705
|
+
const config = resolveConfig(args.skipStylelint ? { validate: { source: { stylelint: false } } } : {}, manifest);
|
|
8713
8706
|
const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
|
|
8714
|
-
const pdfPath = typeof args.pdf === "string" ?
|
|
8715
|
-
const inputDir = typeof args.input === "string" ?
|
|
8716
|
-
if (pdfPath && !
|
|
8707
|
+
const pdfPath = typeof args.pdf === "string" ? resolve8(args.pdf) : undefined;
|
|
8708
|
+
const inputDir = typeof args.input === "string" ? resolve8(args.input) : undefined;
|
|
8709
|
+
if (pdfPath && !existsSync10(pdfPath)) {
|
|
8717
8710
|
throw new Error(`File not found: ${pdfPath}`);
|
|
8718
8711
|
}
|
|
8719
8712
|
const categories = resolveCategoryArg(typeof args.category === "string" ? args.category : undefined);
|
|
@@ -8744,7 +8737,7 @@ async function executeValidation(args) {
|
|
|
8744
8737
|
const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
|
|
8745
8738
|
markdownFiles = relMarkdown.map((f) => join4(manifestDir, canonicalChapterId(f)));
|
|
8746
8739
|
const relStyles = await resolveActiveStyles(manifestDir, config.styles);
|
|
8747
|
-
const projectCssFiles = relStyles.map((rel) =>
|
|
8740
|
+
const projectCssFiles = relStyles.map((rel) => resolve8(manifestDir, rel));
|
|
8748
8741
|
let pluginStylePaths = args.pluginStylePaths;
|
|
8749
8742
|
if (pluginStylePaths === undefined) {
|
|
8750
8743
|
({ pluginStylePaths } = await loadPluginsWithCss(config.extensions, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for validation — ${err.message}`)));
|
|
@@ -8752,7 +8745,7 @@ async function executeValidation(args) {
|
|
|
8752
8745
|
cssFiles = [...projectCssFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
|
|
8753
8746
|
assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
|
|
8754
8747
|
const possibleHtml = join4(outDir, BOOK_HTML_FILENAME);
|
|
8755
|
-
if (
|
|
8748
|
+
if (existsSync10(possibleHtml)) {
|
|
8756
8749
|
htmlPath = possibleHtml;
|
|
8757
8750
|
}
|
|
8758
8751
|
}
|
|
@@ -8901,12 +8894,12 @@ function computeGates(format, opts, config) {
|
|
|
8901
8894
|
if (opts.skipLint || opts.skipPreValidate || opts.skipPostValidate) {
|
|
8902
8895
|
log.info("Validation/lint flags ignored for --format html (no validation phases apply)");
|
|
8903
8896
|
}
|
|
8904
|
-
return {
|
|
8897
|
+
return { skipStylelint: false, preValidate: false, postValidate: false };
|
|
8905
8898
|
}
|
|
8906
|
-
const
|
|
8899
|
+
const skipStylelint = !!opts.skipLint || config.lint.enabled === false;
|
|
8907
8900
|
const preValidate = !opts.skipPreValidate && config.validate.enabled !== false;
|
|
8908
8901
|
const postValidate = format === "pdfx" && !opts.skipPostValidate && config.validate.enabled !== false;
|
|
8909
|
-
return {
|
|
8902
|
+
return { skipStylelint, preValidate, postValidate };
|
|
8910
8903
|
}
|
|
8911
8904
|
|
|
8912
8905
|
// src/lib/build-runner.ts
|
|
@@ -8974,24 +8967,15 @@ async function loadBuildPlugins(ctx) {
|
|
|
8974
8967
|
}
|
|
8975
8968
|
async function runQualityGates(ctx) {
|
|
8976
8969
|
const { gates, opts, renderDir } = ctx;
|
|
8977
|
-
const pluginStylePaths = gates.
|
|
8978
|
-
if (gates.lint) {
|
|
8979
|
-
log.info("Lint: CSS print-safety");
|
|
8980
|
-
const lintResult = await runLint({
|
|
8981
|
-
manifest: opts.manifestPath ?? renderDir,
|
|
8982
|
-
pluginStylePaths
|
|
8983
|
-
});
|
|
8984
|
-
if (!lintResult.ok) {
|
|
8985
|
-
throw new BuildError("CSS lint failed", 2);
|
|
8986
|
-
}
|
|
8987
|
-
}
|
|
8970
|
+
const pluginStylePaths = gates.preValidate ? (await loadBuildPlugins(ctx)).pluginStylePaths : undefined;
|
|
8988
8971
|
if (gates.preValidate) {
|
|
8989
8972
|
log.info("Pre-build validation");
|
|
8990
8973
|
const result = await executeAndReport({
|
|
8991
8974
|
input: renderDir,
|
|
8992
8975
|
phase: "pre-build",
|
|
8993
8976
|
manifest: opts.manifestPath,
|
|
8994
|
-
pluginStylePaths
|
|
8977
|
+
pluginStylePaths,
|
|
8978
|
+
skipStylelint: gates.skipStylelint
|
|
8995
8979
|
}, "text");
|
|
8996
8980
|
for (const finding2 of result.execution.report.results) {
|
|
8997
8981
|
if (finding2.checkId === "source.markdown.layout-markers" && finding2.file && finding2.code !== "inspect-failed") {
|
|
@@ -9078,7 +9062,7 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
|
|
|
9078
9062
|
recordedOutputDir: fingerprint.outputDir
|
|
9079
9063
|
});
|
|
9080
9064
|
await publishBuild(ctx, artifactName2);
|
|
9081
|
-
const delivered = ctx.target.kind
|
|
9065
|
+
const delivered = ctx.target.kind === "project" || ctx.target.kind === "directory" && ctx.format === "html";
|
|
9082
9066
|
const fingerprintPath = delivered ? path9.join(ctx.outDir, path9.basename(workFingerprint)) : null;
|
|
9083
9067
|
log.success(wroteMessage);
|
|
9084
9068
|
if (fingerprintPath)
|
|
@@ -9091,6 +9075,16 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
|
|
|
9091
9075
|
diagnostics
|
|
9092
9076
|
};
|
|
9093
9077
|
}
|
|
9078
|
+
async function publishToDirectory(workDir, targetDir, format, artifactName2) {
|
|
9079
|
+
await fsp2.mkdir(targetDir, { recursive: true });
|
|
9080
|
+
if (format !== "html") {
|
|
9081
|
+
if (!artifactName2)
|
|
9082
|
+
throw new BuildError("No artifact to write for this format", 1);
|
|
9083
|
+
await fsp2.copyFile(path9.join(workDir, artifactName2), path9.join(targetDir, artifactName2));
|
|
9084
|
+
return;
|
|
9085
|
+
}
|
|
9086
|
+
await fsp2.cp(workDir, targetDir, { recursive: true, force: true });
|
|
9087
|
+
}
|
|
9094
9088
|
async function publishBuild(ctx, artifactName2) {
|
|
9095
9089
|
const { workDir, target } = ctx;
|
|
9096
9090
|
if (target.kind === "file") {
|
|
@@ -9101,8 +9095,7 @@ async function publishBuild(ctx, artifactName2) {
|
|
|
9101
9095
|
return;
|
|
9102
9096
|
}
|
|
9103
9097
|
if (target.kind === "directory") {
|
|
9104
|
-
await
|
|
9105
|
-
await fsp2.cp(workDir, target.dir, { recursive: true, force: true });
|
|
9098
|
+
await publishToDirectory(workDir, target.dir, ctx.format, artifactName2);
|
|
9106
9099
|
return;
|
|
9107
9100
|
}
|
|
9108
9101
|
for (const name of await listPdfs(target.dir)) {
|
|
@@ -9156,7 +9149,7 @@ class PdfOutput {
|
|
|
9156
9149
|
const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
|
|
9157
9150
|
await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
|
|
9158
9151
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
9159
|
-
const { buildNativePdf } = await import("./engine-
|
|
9152
|
+
const { buildNativePdf } = await import("./engine-pt2acjb5.js");
|
|
9160
9153
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
9161
9154
|
title: config.title,
|
|
9162
9155
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
@@ -9250,7 +9243,7 @@ async function runBuild(opts) {
|
|
|
9250
9243
|
import path11 from "path";
|
|
9251
9244
|
import { tmpdir as tmpdir2 } from "os";
|
|
9252
9245
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
9253
|
-
import { readdir as readdir5, readFile as
|
|
9246
|
+
import { readdir as readdir5, readFile as readFile22, writeFile as writeFile5 } from "node:fs/promises";
|
|
9254
9247
|
|
|
9255
9248
|
// src/utils/file-utils.ts
|
|
9256
9249
|
import { promises as fs3 } from "fs";
|
|
@@ -9655,8 +9648,8 @@ function createFileWatcher(state) {
|
|
|
9655
9648
|
immediatePending = false;
|
|
9656
9649
|
state.isRebuilding = true;
|
|
9657
9650
|
let resolveInFlight;
|
|
9658
|
-
const inFlight = new Promise((
|
|
9659
|
-
resolveInFlight =
|
|
9651
|
+
const inFlight = new Promise((resolve9) => {
|
|
9652
|
+
resolveInFlight = resolve9;
|
|
9660
9653
|
});
|
|
9661
9654
|
state.rebuildPromise = inFlight;
|
|
9662
9655
|
try {
|
|
@@ -9760,7 +9753,7 @@ async function cleanupOrphanTempDirs() {
|
|
|
9760
9753
|
const dir = path11.join(TEMP_DIR_BASE, entry);
|
|
9761
9754
|
const pidFile = path11.join(dir, PID_FILE_NAME);
|
|
9762
9755
|
try {
|
|
9763
|
-
const raw = await
|
|
9756
|
+
const raw = await readFile22(pidFile, "utf8");
|
|
9764
9757
|
const pid = parseInt(raw.trim(), 10);
|
|
9765
9758
|
if (!isProcessAlive(pid)) {
|
|
9766
9759
|
debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
|
|
@@ -9805,10 +9798,10 @@ async function restartPreview(newInputPath, state) {
|
|
|
9805
9798
|
}
|
|
9806
9799
|
async function withTimeout(p, ms, label) {
|
|
9807
9800
|
let timer;
|
|
9808
|
-
const timeout = new Promise((
|
|
9801
|
+
const timeout = new Promise((resolve9) => {
|
|
9809
9802
|
timer = setTimeout(() => {
|
|
9810
9803
|
debug(`shutdownServer: ${label} exceeded ${ms}ms — continuing`);
|
|
9811
|
-
|
|
9804
|
+
resolve9(undefined);
|
|
9812
9805
|
}, ms);
|
|
9813
9806
|
});
|
|
9814
9807
|
try {
|
|
@@ -9866,7 +9859,7 @@ function createServerState(inputPath, tempDir, config, options) {
|
|
|
9866
9859
|
import http from "node:http";
|
|
9867
9860
|
import net from "node:net";
|
|
9868
9861
|
import { createHash as createHash6, randomUUID } from "node:crypto";
|
|
9869
|
-
import { readFile as
|
|
9862
|
+
import { readFile as readFile23, stat as stat4 } from "node:fs/promises";
|
|
9870
9863
|
import path13 from "path";
|
|
9871
9864
|
import { WebSocket, WebSocketServer } from "ws";
|
|
9872
9865
|
|
|
@@ -9887,13 +9880,13 @@ function buildOpenPathSpawnSpec(filePath, platform = process.platform) {
|
|
|
9887
9880
|
}
|
|
9888
9881
|
}
|
|
9889
9882
|
function openPath(filePath) {
|
|
9890
|
-
return new Promise((
|
|
9883
|
+
return new Promise((resolve9, reject) => {
|
|
9891
9884
|
const { cmd, args, options } = buildOpenPathSpawnSpec(filePath);
|
|
9892
9885
|
const child = spawn(cmd, args, options);
|
|
9893
9886
|
child.once("error", reject);
|
|
9894
9887
|
child.once("spawn", () => {
|
|
9895
9888
|
child.unref();
|
|
9896
|
-
|
|
9889
|
+
resolve9();
|
|
9897
9890
|
});
|
|
9898
9891
|
});
|
|
9899
9892
|
}
|
|
@@ -10124,7 +10117,7 @@ iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}<
|
|
|
10124
10117
|
</body></html>`;
|
|
10125
10118
|
}
|
|
10126
10119
|
function probePort(port, host) {
|
|
10127
|
-
return new Promise((
|
|
10120
|
+
return new Promise((resolve9) => {
|
|
10128
10121
|
const srv = net.createServer();
|
|
10129
10122
|
srv.unref();
|
|
10130
10123
|
let settled = false;
|
|
@@ -10132,7 +10125,7 @@ function probePort(port, host) {
|
|
|
10132
10125
|
if (settled)
|
|
10133
10126
|
return;
|
|
10134
10127
|
settled = true;
|
|
10135
|
-
|
|
10128
|
+
resolve9(result);
|
|
10136
10129
|
};
|
|
10137
10130
|
srv.once("error", (error2) => finish({ available: false, error: error2 }));
|
|
10138
10131
|
try {
|
|
@@ -10184,7 +10177,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
|
|
|
10184
10177
|
}
|
|
10185
10178
|
let data;
|
|
10186
10179
|
try {
|
|
10187
|
-
data = await
|
|
10180
|
+
data = await readFile23(filePath);
|
|
10188
10181
|
} catch {
|
|
10189
10182
|
res.writeHead(404);
|
|
10190
10183
|
res.end("Not Found");
|
|
@@ -10234,7 +10227,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
|
|
|
10234
10227
|
let data;
|
|
10235
10228
|
if (!validator || validator.stamp !== stamp) {
|
|
10236
10229
|
try {
|
|
10237
|
-
data = await
|
|
10230
|
+
data = await readFile23(absPath);
|
|
10238
10231
|
} catch {
|
|
10239
10232
|
await serveStatic(absPath, res);
|
|
10240
10233
|
return;
|
|
@@ -10457,7 +10450,7 @@ async function createPreviewServer(state, port) {
|
|
|
10457
10450
|
socket.destroy();
|
|
10458
10451
|
}
|
|
10459
10452
|
});
|
|
10460
|
-
await new Promise((
|
|
10453
|
+
await new Promise((resolve9) => server.listen(port, state.options.host, resolve9));
|
|
10461
10454
|
const boundPort = server.address().port;
|
|
10462
10455
|
const serverUrl = `http://localhost:${boundPort}`;
|
|
10463
10456
|
info(`Preview server running at ${serverUrl}`);
|
|
@@ -10498,11 +10491,11 @@ async function createPreviewServer(state, port) {
|
|
|
10498
10491
|
}
|
|
10499
10492
|
clients.clear();
|
|
10500
10493
|
wss.close();
|
|
10501
|
-
await new Promise((
|
|
10494
|
+
await new Promise((resolve9) => {
|
|
10502
10495
|
if (typeof server.closeAllConnections === "function") {
|
|
10503
10496
|
server.closeAllConnections();
|
|
10504
10497
|
}
|
|
10505
|
-
server.close(() =>
|
|
10498
|
+
server.close(() => resolve9());
|
|
10506
10499
|
});
|
|
10507
10500
|
},
|
|
10508
10501
|
broadcastReload() {
|
|
@@ -10575,7 +10568,7 @@ async function startPreviewServer(options) {
|
|
|
10575
10568
|
import { platform, arch, release } from "node:os";
|
|
10576
10569
|
|
|
10577
10570
|
// src/lib/remote-auth/token-store.ts
|
|
10578
|
-
import { chmod, mkdir as mkdir5, readFile as
|
|
10571
|
+
import { chmod, mkdir as mkdir5, readFile as readFile24, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
|
|
10579
10572
|
import os3 from "node:os";
|
|
10580
10573
|
import path14 from "node:path";
|
|
10581
10574
|
function credentialHostKey(hostOrUrl) {
|
|
@@ -10619,7 +10612,7 @@ class FileTokenStore {
|
|
|
10619
10612
|
async read() {
|
|
10620
10613
|
let raw;
|
|
10621
10614
|
try {
|
|
10622
|
-
raw = await
|
|
10615
|
+
raw = await readFile24(this.filePath, "utf8");
|
|
10623
10616
|
} catch (e) {
|
|
10624
10617
|
if (e?.code === "ENOENT") {
|
|
10625
10618
|
return { version: 1, credentials: {} };
|
|
@@ -10819,25 +10812,25 @@ async function getSystemDiagnostics() {
|
|
|
10819
10812
|
};
|
|
10820
10813
|
}
|
|
10821
10814
|
// src/lib/project-scaffold.ts
|
|
10822
|
-
import { access, copyFile, cp as cp3, mkdir as mkdir9, readdir as readdir6, readFile as
|
|
10823
|
-
import { constants as FS, existsSync as
|
|
10815
|
+
import { access, copyFile, cp as cp3, mkdir as mkdir9, readdir as readdir6, readFile as readFile28, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
|
|
10816
|
+
import { constants as FS, existsSync as existsSync13 } from "node:fs";
|
|
10824
10817
|
import path18 from "node:path";
|
|
10825
10818
|
|
|
10826
10819
|
// src/lib/manifest-doc.ts
|
|
10827
|
-
import { existsSync as
|
|
10820
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
10828
10821
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10829
|
-
import { mkdir as mkdir6, open, readFile as
|
|
10822
|
+
import { mkdir as mkdir6, open, readFile as readFile25, rename as rename3, rm as rm2 } from "node:fs/promises";
|
|
10830
10823
|
import path15 from "node:path";
|
|
10831
10824
|
import { parseDocument, isSeq, YAMLSeq } from "yaml";
|
|
10832
10825
|
function resolveManifestPath(projectDir) {
|
|
10833
|
-
const existing = MANIFEST_FILENAMES.find((name) =>
|
|
10826
|
+
const existing = MANIFEST_FILENAMES.find((name) => existsSync11(path15.join(projectDir, name)));
|
|
10834
10827
|
return path15.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
|
|
10835
10828
|
}
|
|
10836
10829
|
async function loadManifestDoc(projectDir) {
|
|
10837
10830
|
const file = resolveManifestPath(projectDir);
|
|
10838
10831
|
let text = "";
|
|
10839
10832
|
try {
|
|
10840
|
-
text = await
|
|
10833
|
+
text = await readFile25(file, "utf8");
|
|
10841
10834
|
} catch (error2) {
|
|
10842
10835
|
if (error2.code !== "ENOENT")
|
|
10843
10836
|
throw error2;
|
|
@@ -10884,8 +10877,8 @@ function scalarString(item) {
|
|
|
10884
10877
|
}
|
|
10885
10878
|
|
|
10886
10879
|
// src/lib/extension-manager.ts
|
|
10887
|
-
import { cp as cp2, mkdir as mkdir8, readFile as
|
|
10888
|
-
import { existsSync as
|
|
10880
|
+
import { cp as cp2, mkdir as mkdir8, readFile as readFile27, rm as rm4, stat as stat5 } from "node:fs/promises";
|
|
10881
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
10889
10882
|
import path17 from "node:path";
|
|
10890
10883
|
import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
|
|
10891
10884
|
|
|
@@ -10897,7 +10890,7 @@ import {
|
|
|
10897
10890
|
mkdir as mkdir7,
|
|
10898
10891
|
mkdtemp as mkdtemp2,
|
|
10899
10892
|
open as open2,
|
|
10900
|
-
readFile as
|
|
10893
|
+
readFile as readFile26,
|
|
10901
10894
|
realpath as realpath2,
|
|
10902
10895
|
rename as rename4,
|
|
10903
10896
|
rm as rm3
|
|
@@ -10942,7 +10935,6 @@ async function withFetchTimeout(options, run2) {
|
|
|
10942
10935
|
}
|
|
10943
10936
|
|
|
10944
10937
|
// src/lib/npm-plugin-installer.ts
|
|
10945
|
-
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
10946
10938
|
var DEFAULT_LIMITS = {
|
|
10947
10939
|
metadataBytes: 15 * 1024 * 1024,
|
|
10948
10940
|
packageTarballBytes: 50 * 1024 * 1024,
|
|
@@ -10964,11 +10956,11 @@ class PackageUnavailableError extends Error {
|
|
|
10964
10956
|
function isOptionalAvailabilityError(error2) {
|
|
10965
10957
|
return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
|
|
10966
10958
|
}
|
|
10967
|
-
function
|
|
10959
|
+
function object3(value) {
|
|
10968
10960
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10969
10961
|
}
|
|
10970
10962
|
function stringMap2(value) {
|
|
10971
|
-
const map =
|
|
10963
|
+
const map = object3(value);
|
|
10972
10964
|
if (!map)
|
|
10973
10965
|
return {};
|
|
10974
10966
|
const result = {};
|
|
@@ -11038,7 +11030,7 @@ async function fetchPackument(ctx, name) {
|
|
|
11038
11030
|
timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
|
|
11039
11031
|
offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
11040
11032
|
}, async (signal) => {
|
|
11041
|
-
const response = await ctx.fetch(`${
|
|
11033
|
+
const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
|
|
11042
11034
|
signal,
|
|
11043
11035
|
redirect: "error",
|
|
11044
11036
|
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
@@ -11061,7 +11053,7 @@ async function fetchPackument(ctx, name) {
|
|
|
11061
11053
|
}
|
|
11062
11054
|
let metadata = null;
|
|
11063
11055
|
try {
|
|
11064
|
-
metadata =
|
|
11056
|
+
metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
|
|
11065
11057
|
} catch {}
|
|
11066
11058
|
if (!metadata || metadata.name !== name) {
|
|
11067
11059
|
throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
|
|
@@ -11117,15 +11109,15 @@ async function selectPackage(ctx, name, selector) {
|
|
|
11117
11109
|
throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
|
|
11118
11110
|
}
|
|
11119
11111
|
const metadata = await fetchPackument(ctx, name);
|
|
11120
|
-
const versions =
|
|
11121
|
-
const tags =
|
|
11112
|
+
const versions = object3(metadata.versions);
|
|
11113
|
+
const tags = object3(metadata["dist-tags"]);
|
|
11122
11114
|
if (!versions || !tags)
|
|
11123
11115
|
throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
|
|
11124
11116
|
let version;
|
|
11125
11117
|
if (!selector) {
|
|
11126
11118
|
if (typeof tags.latest === "string")
|
|
11127
11119
|
version = tags.latest;
|
|
11128
|
-
} else if (
|
|
11120
|
+
} else if (object3(versions[selector])) {
|
|
11129
11121
|
version = selector;
|
|
11130
11122
|
} else if (typeof tags[selector] === "string") {
|
|
11131
11123
|
version = tags[selector];
|
|
@@ -11135,11 +11127,11 @@ async function selectPackage(ctx, name, selector) {
|
|
|
11135
11127
|
if (!version || !isExactNpmVersion(version)) {
|
|
11136
11128
|
throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
|
|
11137
11129
|
}
|
|
11138
|
-
const selected =
|
|
11130
|
+
const selected = object3(versions[version]);
|
|
11139
11131
|
if (!selected || selected.name !== name || selected.version !== version) {
|
|
11140
11132
|
throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
|
|
11141
11133
|
}
|
|
11142
|
-
const dist =
|
|
11134
|
+
const dist = object3(selected.dist);
|
|
11143
11135
|
if (!dist || typeof dist.tarball !== "string") {
|
|
11144
11136
|
throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
|
|
11145
11137
|
}
|
|
@@ -11149,7 +11141,7 @@ async function selectPackage(ctx, name, selector) {
|
|
|
11149
11141
|
} catch {
|
|
11150
11142
|
throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
|
|
11151
11143
|
}
|
|
11152
|
-
if (tarball.origin !==
|
|
11144
|
+
if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
|
|
11153
11145
|
throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
|
|
11154
11146
|
}
|
|
11155
11147
|
return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
|
|
@@ -11296,7 +11288,7 @@ async function readInstalledManifest(packageDir, selected) {
|
|
|
11296
11288
|
const info2 = await lstat2(packageJsonPath);
|
|
11297
11289
|
if (!info2.isFile() || info2.isSymbolicLink())
|
|
11298
11290
|
throw new Error("not a regular file");
|
|
11299
|
-
manifest =
|
|
11291
|
+
manifest = object3(JSON.parse(await readFile26(packageJsonPath, "utf8")));
|
|
11300
11292
|
} catch (error2) {
|
|
11301
11293
|
throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
|
|
11302
11294
|
}
|
|
@@ -11408,14 +11400,14 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
|
|
|
11408
11400
|
for (const optionalName of Object.keys(optionalDependencies))
|
|
11409
11401
|
delete dependencies[optionalName];
|
|
11410
11402
|
const peerDependencies = stringMap2(manifest.peerDependencies);
|
|
11411
|
-
const peerMeta =
|
|
11403
|
+
const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
|
|
11412
11404
|
for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
|
|
11413
11405
|
delete peerDependencies[existing];
|
|
11414
11406
|
}
|
|
11415
11407
|
const nextAncestors = [...ancestors, record];
|
|
11416
11408
|
const requiredEntries = [
|
|
11417
11409
|
...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
|
|
11418
|
-
...Object.entries(peerDependencies).filter(([dependency]) =>
|
|
11410
|
+
...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
|
|
11419
11411
|
].sort((a, b) => a.dependency.localeCompare(b.dependency));
|
|
11420
11412
|
for (const { dependency, range } of requiredEntries) {
|
|
11421
11413
|
const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
|
|
@@ -11433,7 +11425,7 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
|
|
|
11433
11425
|
}
|
|
11434
11426
|
}
|
|
11435
11427
|
for (const [dependency, range] of Object.entries(peerDependencies)) {
|
|
11436
|
-
if (
|
|
11428
|
+
if (object3(peerMeta[dependency])?.optional === true) {
|
|
11437
11429
|
ctx.skipped.push({
|
|
11438
11430
|
from: relativePath,
|
|
11439
11431
|
name: dependency,
|
|
@@ -11714,21 +11706,18 @@ var NO_CARRIES = {
|
|
|
11714
11706
|
snippets: false,
|
|
11715
11707
|
components: false
|
|
11716
11708
|
};
|
|
11717
|
-
function
|
|
11718
|
-
return existsSync13(path17.join(dir, EXTENSION_MANIFEST_FILENAME)) || existsSync13(path17.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
|
|
11719
|
-
}
|
|
11720
|
-
async function describeFolder(dir, base, fallbackLabel) {
|
|
11709
|
+
async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
|
|
11721
11710
|
const meta = await readExtensionMeta(dir);
|
|
11722
|
-
const styles =
|
|
11711
|
+
const styles = extensionStyleList(meta);
|
|
11723
11712
|
return {
|
|
11724
11713
|
...base,
|
|
11725
|
-
label: meta.name?.trim()
|
|
11714
|
+
label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
|
|
11726
11715
|
...meta.description?.trim() ? { description: meta.description.trim() } : {},
|
|
11727
11716
|
...meta.author?.trim() ? { author: meta.author.trim() } : {},
|
|
11728
11717
|
...meta.preview !== undefined ? { preview: meta.preview } : {},
|
|
11729
11718
|
...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
|
|
11730
11719
|
...styles.length > 0 ? { styles } : {},
|
|
11731
|
-
carries: extensionCarries(meta
|
|
11720
|
+
carries: extensionCarries(meta),
|
|
11732
11721
|
dir
|
|
11733
11722
|
};
|
|
11734
11723
|
}
|
|
@@ -11801,10 +11790,14 @@ async function describeEntry(projectDir, raw) {
|
|
|
11801
11790
|
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11802
11791
|
}
|
|
11803
11792
|
const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
|
|
11804
|
-
if (!
|
|
11793
|
+
if (!existsSync12(path17.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
|
|
11805
11794
|
return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
|
|
11806
11795
|
}
|
|
11807
|
-
|
|
11796
|
+
const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
|
|
11797
|
+
return withWarnings({
|
|
11798
|
+
...described,
|
|
11799
|
+
carries: { ...described.carries, markdown: true }
|
|
11800
|
+
});
|
|
11808
11801
|
}
|
|
11809
11802
|
async function listProjectExtensions(projectDir) {
|
|
11810
11803
|
const { doc } = await loadManifestDoc(projectDir);
|
|
@@ -12005,7 +11998,7 @@ async function listBuiltInStyleSets() {
|
|
|
12005
11998
|
const meta = await readExtensionMeta(path17.join(assets, "themes", id));
|
|
12006
11999
|
out.push({
|
|
12007
12000
|
id,
|
|
12008
|
-
name: meta.name?.trim() ||
|
|
12001
|
+
name: prettify(meta.name?.trim() || id),
|
|
12009
12002
|
description: meta.description?.trim() || ""
|
|
12010
12003
|
});
|
|
12011
12004
|
}
|
|
@@ -12014,7 +12007,7 @@ async function listBuiltInStyleSets() {
|
|
|
12014
12007
|
async function uniqueExtensionId(projectDir, base) {
|
|
12015
12008
|
const slug = slugify(base) || "extension";
|
|
12016
12009
|
let candidate = slug;
|
|
12017
|
-
for (let n = 2;
|
|
12010
|
+
for (let n = 2;existsSync12(path17.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
|
|
12018
12011
|
candidate = `${slug}-${n}`;
|
|
12019
12012
|
}
|
|
12020
12013
|
return candidate;
|
|
@@ -12025,7 +12018,7 @@ async function addBuiltInStyleSet(projectDir, id) {
|
|
|
12025
12018
|
}
|
|
12026
12019
|
return withMutationLock(projectDir, async () => {
|
|
12027
12020
|
const dest = path17.join(projectDir, EXTENSIONS_DIR, id);
|
|
12028
|
-
if (!
|
|
12021
|
+
if (!existsSync12(dest)) {
|
|
12029
12022
|
const src = path17.join(await getAssetsDir(), "themes", id);
|
|
12030
12023
|
await mkdir8(path17.dirname(dest), { recursive: true });
|
|
12031
12024
|
await cp2(src, dest, { recursive: true });
|
|
@@ -12067,7 +12060,7 @@ async function readExtensionCss(projectDir, use) {
|
|
|
12067
12060
|
throw new Error(`Extension "${use}" declares no stylesheets.`);
|
|
12068
12061
|
const parts = [];
|
|
12069
12062
|
for (const sheet of sheets)
|
|
12070
|
-
parts.push(await
|
|
12063
|
+
parts.push(await readFile27(sheet, "utf8"));
|
|
12071
12064
|
return parts.join(`
|
|
12072
12065
|
|
|
12073
12066
|
`);
|
|
@@ -12204,7 +12197,7 @@ async function scaffoldProject(options) {
|
|
|
12204
12197
|
} catch (e) {
|
|
12205
12198
|
throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
|
|
12206
12199
|
}
|
|
12207
|
-
const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(
|
|
12200
|
+
const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(existsSync13) ?? path18.join(projectDir, MANIFEST_FILENAMES[0]);
|
|
12208
12201
|
const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
|
|
12209
12202
|
const substitutions = {
|
|
12210
12203
|
"{{TITLE}}": escapeYamlScalar(name),
|
|
@@ -12244,7 +12237,7 @@ async function scaffoldProject(options) {
|
|
|
12244
12237
|
let versionHistoryError;
|
|
12245
12238
|
if (requested === "local-git") {
|
|
12246
12239
|
try {
|
|
12247
|
-
const { providerFor } = await import("./source-provider-
|
|
12240
|
+
const { providerFor } = await import("./source-provider-jkppq9fc.js");
|
|
12248
12241
|
const provider = providerFor({ type: "local-folder", path: projectDir });
|
|
12249
12242
|
await provider.initVersionHistory({
|
|
12250
12243
|
projectDir,
|
|
@@ -12288,7 +12281,7 @@ async function adoptFolder(options) {
|
|
|
12288
12281
|
if (!st.isDirectory()) {
|
|
12289
12282
|
throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
|
|
12290
12283
|
}
|
|
12291
|
-
if (MANIFEST_FILENAMES.some((name) =>
|
|
12284
|
+
if (MANIFEST_FILENAMES.some((name) => existsSync13(path18.join(dir, name)))) {
|
|
12292
12285
|
throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
|
|
12293
12286
|
}
|
|
12294
12287
|
const template = options.template ?? "book";
|
|
@@ -12307,7 +12300,7 @@ async function adoptFolder(options) {
|
|
|
12307
12300
|
const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
|
|
12308
12301
|
await mkdir9(path18.join(dir, "styles"), { recursive: true });
|
|
12309
12302
|
const bookCssPath = path18.join(dir, "styles", "book.css");
|
|
12310
|
-
const hadOwnStylesheet =
|
|
12303
|
+
const hadOwnStylesheet = existsSync13(bookCssPath);
|
|
12311
12304
|
if (!hadOwnStylesheet) {
|
|
12312
12305
|
await writeFile7(bookCssPath, starterOverridesCss(themeId), "utf8");
|
|
12313
12306
|
}
|
|
@@ -12341,7 +12334,7 @@ styles:
|
|
|
12341
12334
|
let versionHistoryError;
|
|
12342
12335
|
if (requested === "local-git") {
|
|
12343
12336
|
try {
|
|
12344
|
-
const { providerFor } = await import("./source-provider-
|
|
12337
|
+
const { providerFor } = await import("./source-provider-jkppq9fc.js");
|
|
12345
12338
|
const provider = providerFor({ type: "local-folder", path: dir });
|
|
12346
12339
|
await provider.initVersionHistory({
|
|
12347
12340
|
projectDir: dir,
|
|
@@ -12368,7 +12361,7 @@ async function ensureGitignoreHasDist(projectDir) {
|
|
|
12368
12361
|
const gitignorePath = path18.join(projectDir, ".gitignore");
|
|
12369
12362
|
let existing = null;
|
|
12370
12363
|
try {
|
|
12371
|
-
existing = await
|
|
12364
|
+
existing = await readFile28(gitignorePath, "utf8");
|
|
12372
12365
|
} catch {}
|
|
12373
12366
|
if (existing === null) {
|
|
12374
12367
|
await writeFile7(gitignorePath, `dist/
|
|
@@ -12388,7 +12381,7 @@ async function ensureGitignoreHasDist(projectDir) {
|
|
|
12388
12381
|
async function firstSourceFile(manifestPath) {
|
|
12389
12382
|
let text;
|
|
12390
12383
|
try {
|
|
12391
|
-
text = await
|
|
12384
|
+
text = await readFile28(manifestPath, "utf8");
|
|
12392
12385
|
} catch {
|
|
12393
12386
|
return;
|
|
12394
12387
|
}
|
|
@@ -12400,21 +12393,20 @@ async function firstSourceFile(manifestPath) {
|
|
|
12400
12393
|
return m ? m[1].trim() : undefined;
|
|
12401
12394
|
}
|
|
12402
12395
|
async function fillTemplateFile(filePath, substitutions) {
|
|
12403
|
-
let text = await
|
|
12396
|
+
let text = await readFile28(filePath, "utf8");
|
|
12404
12397
|
for (const [token, value] of Object.entries(substitutions)) {
|
|
12405
12398
|
text = text.split(token).join(value);
|
|
12406
12399
|
}
|
|
12407
12400
|
await writeFile7(filePath, text, "utf8");
|
|
12408
12401
|
}
|
|
12409
12402
|
// src/lib/extension-scaffold.ts
|
|
12410
|
-
import { access as access2, mkdir as mkdir10, readFile as
|
|
12403
|
+
import { access as access2, mkdir as mkdir10, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
|
|
12411
12404
|
import { constants as FS2 } from "node:fs";
|
|
12412
12405
|
import path19 from "node:path";
|
|
12413
12406
|
var EXTENSION_KINDS = ["plugin", "theme"];
|
|
12414
12407
|
var RESERVED_PREFIX = "gp-";
|
|
12415
12408
|
var TEMPLATE_FILES = {
|
|
12416
12409
|
plugin: [
|
|
12417
|
-
["gutterpress.json", "gutterpress.json"],
|
|
12418
12410
|
["package.json", "package.json"],
|
|
12419
12411
|
["README.md", "README.md"],
|
|
12420
12412
|
["plugin.js.tpl", "plugin.js"],
|
|
@@ -12425,7 +12417,7 @@ var TEMPLATE_FILES = {
|
|
|
12425
12417
|
["test/plugin.test.js.tpl", "test/plugin.test.js"]
|
|
12426
12418
|
],
|
|
12427
12419
|
theme: [
|
|
12428
|
-
["
|
|
12420
|
+
["package.json", "package.json"],
|
|
12429
12421
|
["README.md", "README.md"],
|
|
12430
12422
|
["components.yaml", "components.yaml"],
|
|
12431
12423
|
["snippets/callout.md", "snippets/callout.md"],
|
|
@@ -12525,7 +12517,7 @@ async function scaffoldExtension(options) {
|
|
|
12525
12517
|
const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
|
|
12526
12518
|
const dest = path19.join(extensionDir, relDest);
|
|
12527
12519
|
await mkdir10(path19.dirname(dest), { recursive: true });
|
|
12528
|
-
const text = await
|
|
12520
|
+
const text = await readFile29(src, "utf8");
|
|
12529
12521
|
const table = relDest.endsWith(".json") ? jsonSafe : plain;
|
|
12530
12522
|
await writeFile8(dest, substitute(text, table), "utf8");
|
|
12531
12523
|
written.push(relDest);
|
|
@@ -12535,7 +12527,7 @@ async function scaffoldExtension(options) {
|
|
|
12535
12527
|
}
|
|
12536
12528
|
return {
|
|
12537
12529
|
extensionDir,
|
|
12538
|
-
manifestPath: path19.join(extensionDir, "
|
|
12530
|
+
manifestPath: path19.join(extensionDir, "package.json"),
|
|
12539
12531
|
kind,
|
|
12540
12532
|
slug,
|
|
12541
12533
|
prefix,
|
|
@@ -12551,8 +12543,8 @@ function substitute(text, table) {
|
|
|
12551
12543
|
return out;
|
|
12552
12544
|
}
|
|
12553
12545
|
// src/lib/project-templates.ts
|
|
12554
|
-
import { cp as cp4, mkdir as mkdir11, readFile as
|
|
12555
|
-
import { constants as FS3, existsSync as
|
|
12546
|
+
import { cp as cp4, mkdir as mkdir11, readFile as readFile30, readdir as readdir7, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
|
|
12547
|
+
import { constants as FS3, existsSync as existsSync14 } from "node:fs";
|
|
12556
12548
|
import { access as access3 } from "node:fs/promises";
|
|
12557
12549
|
import path20 from "node:path";
|
|
12558
12550
|
import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
|
|
@@ -12563,7 +12555,7 @@ var BUILT_IN_TEMPLATE_IDS = [
|
|
|
12563
12555
|
];
|
|
12564
12556
|
async function readTemplateChoices(manifestPath) {
|
|
12565
12557
|
try {
|
|
12566
|
-
const text = await
|
|
12558
|
+
const text = await readFile30(manifestPath, "utf8");
|
|
12567
12559
|
const doc = parseDocument2(text);
|
|
12568
12560
|
const out = {};
|
|
12569
12561
|
const preset = doc.get("preset");
|
|
@@ -12653,11 +12645,11 @@ function commonAncestorDir(absFiles) {
|
|
|
12653
12645
|
}
|
|
12654
12646
|
function pickVendorDir(templateDir, base) {
|
|
12655
12647
|
const preferred = path20.basename(base) || VENDOR_DIR;
|
|
12656
|
-
if (!
|
|
12648
|
+
if (!existsSync14(path20.join(templateDir, preferred)))
|
|
12657
12649
|
return preferred;
|
|
12658
12650
|
for (let n = 1;; n++) {
|
|
12659
12651
|
const candidate = `${preferred}-${n}`;
|
|
12660
|
-
if (!
|
|
12652
|
+
if (!existsSync14(path20.join(templateDir, candidate)))
|
|
12661
12653
|
return candidate;
|
|
12662
12654
|
}
|
|
12663
12655
|
}
|
|
@@ -12684,7 +12676,7 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
|
|
|
12684
12676
|
const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
|
|
12685
12677
|
const base = commonAncestorDir(externalFiles);
|
|
12686
12678
|
const vendorDir = pickVendorDir(templateDir, base);
|
|
12687
|
-
await Promise.all(externalFiles.filter((abs) =>
|
|
12679
|
+
await Promise.all(externalFiles.filter((abs) => existsSync14(abs)).map(async (abs) => {
|
|
12688
12680
|
const dest = path20.join(templateDir, vendorDir, path20.relative(base, abs));
|
|
12689
12681
|
await mkdir11(path20.dirname(dest), { recursive: true });
|
|
12690
12682
|
await cp4(abs, dest, { recursive: true });
|
|
@@ -12745,7 +12737,7 @@ async function saveProjectAsTemplate(options) {
|
|
|
12745
12737
|
async function retokeniseManifest(manifestPath) {
|
|
12746
12738
|
let text;
|
|
12747
12739
|
try {
|
|
12748
|
-
text = await
|
|
12740
|
+
text = await readFile30(manifestPath, "utf8");
|
|
12749
12741
|
} catch {
|
|
12750
12742
|
return;
|
|
12751
12743
|
}
|
|
@@ -12766,7 +12758,7 @@ async function listCustomTemplates(templatesRoot) {
|
|
|
12766
12758
|
const dir = path20.join(templatesRoot, entry.name);
|
|
12767
12759
|
let label = prettify(entry.name);
|
|
12768
12760
|
try {
|
|
12769
|
-
const meta = JSON.parse(await
|
|
12761
|
+
const meta = JSON.parse(await readFile30(path20.join(dir, TEMPLATE_META_FILE), "utf8"));
|
|
12770
12762
|
if (typeof meta.label === "string" && meta.label)
|
|
12771
12763
|
label = meta.label;
|
|
12772
12764
|
} catch {}
|
|
@@ -12792,7 +12784,7 @@ async function importTemplateFromFolder(options) {
|
|
|
12792
12784
|
return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
|
|
12793
12785
|
}
|
|
12794
12786
|
// src/lib/snippets.ts
|
|
12795
|
-
import { readdir as readdir8, readFile as
|
|
12787
|
+
import { readdir as readdir8, readFile as readFile31, writeFile as writeFile10, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
|
|
12796
12788
|
import path21 from "node:path";
|
|
12797
12789
|
var SNIPPETS_DIR = "snippets";
|
|
12798
12790
|
var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
@@ -12834,7 +12826,7 @@ async function scanSnippetFiles(dir) {
|
|
|
12834
12826
|
continue;
|
|
12835
12827
|
let body = "";
|
|
12836
12828
|
try {
|
|
12837
|
-
body = await
|
|
12829
|
+
body = await readFile31(path21.join(dir, fileName), "utf8");
|
|
12838
12830
|
} catch {
|
|
12839
12831
|
continue;
|
|
12840
12832
|
}
|
|
@@ -12852,7 +12844,7 @@ async function listSnippets(projectDir) {
|
|
|
12852
12844
|
return files.map((file) => ({ ...file, source: { kind: "project" } }));
|
|
12853
12845
|
}
|
|
12854
12846
|
async function readSnippet(projectDir, fileName) {
|
|
12855
|
-
return
|
|
12847
|
+
return readFile31(resolveSnippetPath(projectDir, fileName), "utf8");
|
|
12856
12848
|
}
|
|
12857
12849
|
async function saveSnippet(projectDir, name, body) {
|
|
12858
12850
|
const stem = slugify(name);
|
|
@@ -12911,11 +12903,11 @@ async function readExtensionSnippet(projectDir, source, fileName) {
|
|
|
12911
12903
|
if (!dir) {
|
|
12912
12904
|
throw new Error(`Extension "${source.ref}" does not declare a usable snippets folder.`);
|
|
12913
12905
|
}
|
|
12914
|
-
return
|
|
12906
|
+
return readFile31(resolveSafeChildFile(dir, fileName), "utf8");
|
|
12915
12907
|
}
|
|
12916
12908
|
// src/lib/extension-import.ts
|
|
12917
|
-
import { existsSync as
|
|
12918
|
-
import { cp as cp5, mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as
|
|
12909
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
12910
|
+
import { cp as cp5, mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile32, rm as rm6, writeFile as writeFile11 } from "node:fs/promises";
|
|
12919
12911
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
12920
12912
|
import path22 from "node:path";
|
|
12921
12913
|
import { unzipSync } from "fflate";
|
|
@@ -12950,7 +12942,7 @@ function classifyThemeCssFindings(findings) {
|
|
|
12950
12942
|
const warnings = reject ? [] : findings.filter((f) => f.rule !== ruleSyntax);
|
|
12951
12943
|
return { reject, warnings };
|
|
12952
12944
|
}
|
|
12953
|
-
var KNOWN_THEME_FILES = new Set(["theme.css",
|
|
12945
|
+
var KNOWN_THEME_FILES = new Set(["theme.css", EXTENSION_MANIFEST_FILENAME]);
|
|
12954
12946
|
var ALLOWED_ASSET_EXTS = new Set([
|
|
12955
12947
|
".woff",
|
|
12956
12948
|
".woff2",
|
|
@@ -12985,10 +12977,10 @@ function mb2(n) {
|
|
|
12985
12977
|
}
|
|
12986
12978
|
async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallbackName) {
|
|
12987
12979
|
const cssPath = path22.join(sourceDir, "theme.css");
|
|
12988
|
-
if (!
|
|
12980
|
+
if (!existsSync15(cssPath)) {
|
|
12989
12981
|
throw new Error("A theme must contain a theme.css file.");
|
|
12990
12982
|
}
|
|
12991
|
-
const css = await
|
|
12983
|
+
const css = await readFile32(cssPath, "utf8");
|
|
12992
12984
|
const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
|
|
12993
12985
|
if (reject) {
|
|
12994
12986
|
throw new Error(`theme.css could not be parsed — ${reject.message}`);
|
|
@@ -12997,29 +12989,26 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
12997
12989
|
for (const w of printFindings) {
|
|
12998
12990
|
warnings.push({ code: "print-safety", message: w.message });
|
|
12999
12991
|
}
|
|
13000
|
-
const metaExists =
|
|
12992
|
+
const metaExists = existsSync15(path22.join(sourceDir, EXTENSION_MANIFEST_FILENAME));
|
|
13001
12993
|
const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
|
|
13002
12994
|
if (!metaExists) {
|
|
13003
12995
|
warnings.push({
|
|
13004
12996
|
code: "no-theme-json",
|
|
13005
|
-
message: "No
|
|
12997
|
+
message: "No package.json found — one was written for you, and the theme's name was taken from the file/folder name."
|
|
13006
12998
|
});
|
|
13007
12999
|
} else if (!meta.name || !meta.name.trim()) {
|
|
13008
13000
|
warnings.push({
|
|
13009
13001
|
code: "unnamed-theme",
|
|
13010
|
-
message: `The
|
|
13002
|
+
message: `The package.json has no "name" — the theme's name was taken from the file/folder name.`
|
|
13011
13003
|
});
|
|
13012
13004
|
}
|
|
13013
13005
|
assertExtensionContained(meta);
|
|
13014
|
-
const
|
|
13015
|
-
const
|
|
13016
|
-
...new Set(extensionStyleListWithDefault(meta, sourceDir))
|
|
13017
|
-
].filter((rel) => rel !== "theme.css");
|
|
13018
|
-
const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, metaFilename) ?? [];
|
|
13006
|
+
const extraSheets = [...new Set(extensionStyleList(meta))].filter((rel) => rel !== "theme.css");
|
|
13007
|
+
const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, EXTENSION_MANIFEST_FILENAME) ?? [];
|
|
13019
13008
|
for (let i = 0;i < extraSheets.length; i++) {
|
|
13020
13009
|
const rel = extraSheets[i];
|
|
13021
13010
|
const sheetPath = extraSheetPaths[i];
|
|
13022
|
-
const sheetCss = await
|
|
13011
|
+
const sheetCss = await readFile32(sheetPath, "utf8");
|
|
13023
13012
|
const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
|
|
13024
13013
|
if (sheetReject) {
|
|
13025
13014
|
throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
|
|
@@ -13041,9 +13030,30 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
|
|
|
13041
13030
|
const dest = path22.join(projectDir, EXTENSIONS_DIR, id);
|
|
13042
13031
|
await mkdir13(path22.dirname(dest), { recursive: true });
|
|
13043
13032
|
await cp5(sourceDir, dest, { recursive: true });
|
|
13033
|
+
await completeLandedManifest(dest, id, meta, metaExists);
|
|
13044
13034
|
const entry = await addExtension(projectDir, `./${EXTENSIONS_DIR}/${id}`);
|
|
13045
13035
|
return { entry, warnings };
|
|
13046
13036
|
}
|
|
13037
|
+
async function completeLandedManifest(dest, id, meta, metaExists) {
|
|
13038
|
+
if (metaExists && extensionStyleList(meta).length > 0)
|
|
13039
|
+
return;
|
|
13040
|
+
const file = path22.join(dest, EXTENSION_MANIFEST_FILENAME);
|
|
13041
|
+
let pkg = {};
|
|
13042
|
+
if (metaExists) {
|
|
13043
|
+
try {
|
|
13044
|
+
const parsed = JSON.parse(await readFile32(file, "utf8"));
|
|
13045
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13046
|
+
pkg = parsed;
|
|
13047
|
+
}
|
|
13048
|
+
} catch {}
|
|
13049
|
+
}
|
|
13050
|
+
const gutterpress = pkg.gutterpress && typeof pkg.gutterpress === "object" && !Array.isArray(pkg.gutterpress) ? pkg.gutterpress : {};
|
|
13051
|
+
await writeFile11(file, JSON.stringify({
|
|
13052
|
+
...pkg,
|
|
13053
|
+
name: typeof pkg.name === "string" && pkg.name.trim() ? pkg.name : id,
|
|
13054
|
+
gutterpress: { ...gutterpress, styles: ["theme.css"] }
|
|
13055
|
+
}, null, 2), "utf8");
|
|
13056
|
+
}
|
|
13047
13057
|
async function importExtensionFromZip(projectDir, archive) {
|
|
13048
13058
|
if (archive.length > MAX_THEME_ARCHIVE_BYTES) {
|
|
13049
13059
|
throw new Error(`Extension package is too large (max ${mb2(MAX_THEME_ARCHIVE_BYTES)}).`);
|
|
@@ -13119,8 +13129,8 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
13119
13129
|
const tmp = await mkdtemp3(path22.join(tmpdir3(), "gutterpress-theme-"));
|
|
13120
13130
|
try {
|
|
13121
13131
|
await writeFile11(path22.join(tmp, "theme.css"), css, "utf8");
|
|
13122
|
-
await writeFile11(path22.join(tmp,
|
|
13123
|
-
const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css",
|
|
13132
|
+
await writeFile11(path22.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify({ name: slugify(displayName, "look"), gutterpress: { styles: ["theme.css"] } }, null, 2), "utf8");
|
|
13133
|
+
const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], displayName);
|
|
13124
13134
|
result.warnings.unshift({
|
|
13125
13135
|
code: "no-theme-json",
|
|
13126
13136
|
message: "A single CSS file was imported — its name came from the file name."
|
|
@@ -13133,11 +13143,11 @@ async function importExtensionFromCssText(projectDir, css, name) {
|
|
|
13133
13143
|
async function importExtensionFromFile(projectDir, filePath) {
|
|
13134
13144
|
const ext = path22.extname(filePath).toLowerCase();
|
|
13135
13145
|
if (ext === ".zip") {
|
|
13136
|
-
const buf = await
|
|
13146
|
+
const buf = await readFile32(filePath);
|
|
13137
13147
|
return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
13138
13148
|
}
|
|
13139
13149
|
if (ext === ".css") {
|
|
13140
|
-
const css = await
|
|
13150
|
+
const css = await readFile32(filePath, "utf8");
|
|
13141
13151
|
return importExtensionFromCssText(projectDir, css, prettify(path22.basename(filePath, ".css")));
|
|
13142
13152
|
}
|
|
13143
13153
|
throw new Error("Choose a .zip extension package or a .css stylesheet.");
|
|
@@ -13191,7 +13201,7 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
13191
13201
|
const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
13192
13202
|
css = await fetchText(`${base}theme.css`);
|
|
13193
13203
|
try {
|
|
13194
|
-
const parsed = JSON.parse(await fetchText(`${base}
|
|
13204
|
+
const parsed = JSON.parse(await fetchText(`${base}${EXTENSION_MANIFEST_FILENAME}`));
|
|
13195
13205
|
if (parsed && typeof parsed === "object")
|
|
13196
13206
|
meta = parsed;
|
|
13197
13207
|
} catch {}
|
|
@@ -13203,18 +13213,137 @@ async function importExtensionFromUrl(projectDir, url) {
|
|
|
13203
13213
|
const tmp = await mkdtemp3(path22.join(tmpdir3(), "gutterpress-ext-"));
|
|
13204
13214
|
try {
|
|
13205
13215
|
await writeFile11(path22.join(tmp, "theme.css"), css, "utf8");
|
|
13216
|
+
const gp = meta.gutterpress && typeof meta.gutterpress === "object" && !Array.isArray(meta.gutterpress) ? meta.gutterpress : {};
|
|
13206
13217
|
const finalMeta = {
|
|
13207
|
-
name: typeof meta.name === "string" && meta.name ||
|
|
13218
|
+
name: typeof meta.name === "string" && meta.name || slugify(baseName, "look"),
|
|
13208
13219
|
...typeof meta.author === "string" ? { author: meta.author } : {},
|
|
13209
13220
|
...typeof meta.description === "string" ? { description: meta.description } : {},
|
|
13210
|
-
|
|
13221
|
+
gutterpress: {
|
|
13222
|
+
...gp,
|
|
13223
|
+
styles: ["theme.css"],
|
|
13224
|
+
preview: typeof gp.preview === "string" ? gp.preview : null
|
|
13225
|
+
}
|
|
13211
13226
|
};
|
|
13212
|
-
await writeFile11(path22.join(tmp,
|
|
13213
|
-
return await finalizeExtensionImport(projectDir, tmp, ["theme.css",
|
|
13227
|
+
await writeFile11(path22.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify(finalMeta, null, 2), "utf8");
|
|
13228
|
+
return await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], baseName);
|
|
13214
13229
|
} finally {
|
|
13215
13230
|
await rm6(tmp, { recursive: true, force: true });
|
|
13216
13231
|
}
|
|
13217
13232
|
}
|
|
13233
|
+
// src/lib/extension-search.ts
|
|
13234
|
+
var FETCH_TIMEOUT_MS2 = 15000;
|
|
13235
|
+
var MAX_SEARCH_BYTES = 2 * 1024 * 1024;
|
|
13236
|
+
var SEARCH_KEYWORDS = ["gutterpress", "markdown-it-plugin"];
|
|
13237
|
+
var KIND_FOR_KEYWORD = {
|
|
13238
|
+
gutterpress: "gutterpress",
|
|
13239
|
+
"markdown-it-plugin": "markdown-it"
|
|
13240
|
+
};
|
|
13241
|
+
function invalid(reason) {
|
|
13242
|
+
throw new Error(`Malformed npm search response: ${reason}.`);
|
|
13243
|
+
}
|
|
13244
|
+
function object4(value) {
|
|
13245
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
13246
|
+
}
|
|
13247
|
+
function str2(value) {
|
|
13248
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
13249
|
+
}
|
|
13250
|
+
function parseSearchPage(text, keyword) {
|
|
13251
|
+
let parsed;
|
|
13252
|
+
try {
|
|
13253
|
+
parsed = JSON.parse(text);
|
|
13254
|
+
} catch {
|
|
13255
|
+
invalid("not valid JSON");
|
|
13256
|
+
}
|
|
13257
|
+
const root = object4(parsed);
|
|
13258
|
+
if (!root)
|
|
13259
|
+
invalid("root is not an object");
|
|
13260
|
+
if (!Array.isArray(root.objects))
|
|
13261
|
+
invalid('"objects" must be an array');
|
|
13262
|
+
if (typeof root.total !== "number")
|
|
13263
|
+
invalid('"total" must be a number');
|
|
13264
|
+
const packages = [];
|
|
13265
|
+
const scores = [];
|
|
13266
|
+
root.objects.forEach((raw, i) => {
|
|
13267
|
+
const entry = object4(raw);
|
|
13268
|
+
if (!entry)
|
|
13269
|
+
invalid(`objects[${i}] is not an object`);
|
|
13270
|
+
const pkg = object4(entry.package);
|
|
13271
|
+
if (!pkg)
|
|
13272
|
+
invalid(`objects[${i}].package is not an object`);
|
|
13273
|
+
const name = str2(pkg.name);
|
|
13274
|
+
const version = str2(pkg.version);
|
|
13275
|
+
if (!name)
|
|
13276
|
+
invalid(`objects[${i}].package.name must be a non-empty string`);
|
|
13277
|
+
if (!version)
|
|
13278
|
+
invalid(`objects[${i}].package.version must be a non-empty string`);
|
|
13279
|
+
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords.filter((k) => typeof k === "string") : [];
|
|
13280
|
+
if (!keywords.includes(keyword))
|
|
13281
|
+
return;
|
|
13282
|
+
if (name === "markdown-it" || name.startsWith("@types/"))
|
|
13283
|
+
return;
|
|
13284
|
+
const links = object4(pkg.links) ?? {};
|
|
13285
|
+
packages.push({
|
|
13286
|
+
name,
|
|
13287
|
+
version,
|
|
13288
|
+
...str2(pkg.description) ? { description: str2(pkg.description) } : {},
|
|
13289
|
+
kind: KIND_FOR_KEYWORD[keyword],
|
|
13290
|
+
keywords,
|
|
13291
|
+
...str2(links.homepage) ? { homepage: str2(links.homepage) } : {},
|
|
13292
|
+
...str2(links.npm) ? { npmUrl: str2(links.npm) } : {},
|
|
13293
|
+
...str2(pkg.date) ? { date: str2(pkg.date) } : {}
|
|
13294
|
+
});
|
|
13295
|
+
scores.push(typeof entry.searchScore === "number" ? entry.searchScore : 0);
|
|
13296
|
+
});
|
|
13297
|
+
return { packages, scores, total: root.total };
|
|
13298
|
+
}
|
|
13299
|
+
async function searchNpmExtensions(query, options = {}) {
|
|
13300
|
+
const limit = options.limit ?? 20;
|
|
13301
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
13302
|
+
const registry = npmRegistryUrl();
|
|
13303
|
+
const trimmed = query.trim();
|
|
13304
|
+
const bodies = await withFetchTimeout({
|
|
13305
|
+
timeoutMs: FETCH_TIMEOUT_MS2,
|
|
13306
|
+
signal: options.signal,
|
|
13307
|
+
timeoutMessage: "Searching npm timed out. Check your connection and try again.",
|
|
13308
|
+
offlineMessage: "Couldn't reach the npm registry. Check your connection and try again."
|
|
13309
|
+
}, async (signal) => {
|
|
13310
|
+
const out = [];
|
|
13311
|
+
for (const keyword of SEARCH_KEYWORDS) {
|
|
13312
|
+
const text = [trimmed, `keywords:${keyword}`].filter(Boolean).join(" ");
|
|
13313
|
+
const url = `${registry}/-/v1/search?text=${encodeURIComponent(text)}&size=${limit}`;
|
|
13314
|
+
const res = await fetchImpl(url, { signal });
|
|
13315
|
+
if (!res.ok) {
|
|
13316
|
+
throw new FriendlyHttpError(`Failed to search npm (HTTP ${res.status}).`);
|
|
13317
|
+
}
|
|
13318
|
+
const body = await res.text();
|
|
13319
|
+
if (Buffer.byteLength(body, "utf8") > MAX_SEARCH_BYTES) {
|
|
13320
|
+
throw new FriendlyHttpError(`The npm search response is too large (limit ${MAX_SEARCH_BYTES} bytes).`);
|
|
13321
|
+
}
|
|
13322
|
+
out.push(body);
|
|
13323
|
+
}
|
|
13324
|
+
return out;
|
|
13325
|
+
});
|
|
13326
|
+
const pages = bodies.map((body, i) => parseSearchPage(body, SEARCH_KEYWORDS[i]));
|
|
13327
|
+
const byName = new Map;
|
|
13328
|
+
for (const page of pages) {
|
|
13329
|
+
page.packages.forEach((match, i) => {
|
|
13330
|
+
const existing = byName.get(match.name);
|
|
13331
|
+
if (!existing) {
|
|
13332
|
+
byName.set(match.name, { match, score: page.scores[i] ?? 0 });
|
|
13333
|
+
return;
|
|
13334
|
+
}
|
|
13335
|
+
if (match.kind === "gutterpress")
|
|
13336
|
+
existing.match = match;
|
|
13337
|
+
existing.score = Math.max(existing.score, page.scores[i] ?? 0);
|
|
13338
|
+
});
|
|
13339
|
+
}
|
|
13340
|
+
const matches = [...byName.values()].sort((a, b) => {
|
|
13341
|
+
if (a.match.kind !== b.match.kind)
|
|
13342
|
+
return a.match.kind === "gutterpress" ? -1 : 1;
|
|
13343
|
+
return b.score - a.score;
|
|
13344
|
+
}).slice(0, limit).map((entry) => entry.match);
|
|
13345
|
+
return { matches, total: pages.reduce((sum, page) => sum + page.total, 0) };
|
|
13346
|
+
}
|
|
13218
13347
|
// src/lib/manifest-config.ts
|
|
13219
13348
|
import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar2 } from "yaml";
|
|
13220
13349
|
function hasKey(obj, key) {
|
|
@@ -13306,8 +13435,6 @@ async function setActiveStyles(projectDir, paths) {
|
|
|
13306
13435
|
return paths;
|
|
13307
13436
|
}
|
|
13308
13437
|
// src/lib/host-policy.ts
|
|
13309
|
-
var AUTO_SNAPSHOT_MIN_MINUTES = 5;
|
|
13310
|
-
var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
|
|
13311
13438
|
var AUTO_SNAPSHOT_DEFAULT_MINUTES = 10;
|
|
13312
13439
|
var AUTO_SYNC_MIN_MINUTES = 1;
|
|
13313
13440
|
var AUTO_SYNC_MAX_MINUTES = 24 * 60;
|
|
@@ -13321,12 +13448,6 @@ function clampedDelayMs(enabled, minutes, bounds) {
|
|
|
13321
13448
|
const clamped = Math.min(bounds.max, Math.max(bounds.min, resolved));
|
|
13322
13449
|
return clamped * 60000;
|
|
13323
13450
|
}
|
|
13324
|
-
var AUTO_SNAPSHOT_BOUNDS = {
|
|
13325
|
-
min: AUTO_SNAPSHOT_MIN_MINUTES,
|
|
13326
|
-
max: AUTO_SNAPSHOT_MAX_MINUTES,
|
|
13327
|
-
default: AUTO_SNAPSHOT_DEFAULT_MINUTES,
|
|
13328
|
-
enabledDefault: true
|
|
13329
|
-
};
|
|
13330
13451
|
var AUTO_SYNC_BOUNDS = {
|
|
13331
13452
|
min: AUTO_SYNC_MIN_MINUTES,
|
|
13332
13453
|
max: AUTO_SYNC_MAX_MINUTES,
|
|
@@ -13334,7 +13455,8 @@ var AUTO_SYNC_BOUNDS = {
|
|
|
13334
13455
|
enabledDefault: true
|
|
13335
13456
|
};
|
|
13336
13457
|
function autoSnapshotDelayMs(policy) {
|
|
13337
|
-
|
|
13458
|
+
const isEnabled = policy?.autoSnapshot ?? true;
|
|
13459
|
+
return isEnabled ? AUTO_SNAPSHOT_DEFAULT_MINUTES * 60000 : null;
|
|
13338
13460
|
}
|
|
13339
13461
|
function autoSyncDelayMs(policy) {
|
|
13340
13462
|
return clampedDelayMs(policy?.autoSync, policy?.autoSyncMinutes, AUTO_SYNC_BOUNDS);
|
|
@@ -13355,7 +13477,7 @@ var API_BASE = "https://api.github.com";
|
|
|
13355
13477
|
var REQUEST_TIMEOUT_MS = 15000;
|
|
13356
13478
|
var OFFLINE_MESSAGE = "Couldn't reach GitHub. Check your connection and try again.";
|
|
13357
13479
|
function defaultSleep(ms) {
|
|
13358
|
-
return new Promise((
|
|
13480
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
13359
13481
|
}
|
|
13360
13482
|
async function safeFetch(fetchImpl, url, init) {
|
|
13361
13483
|
return withFetchTimeout({
|
|
@@ -13582,7 +13704,7 @@ function friendlyTokenError(status, body) {
|
|
|
13582
13704
|
var SUCCESS_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>You're connected</h2><p>Return to Gutterpress — you can close this tab.</p></body></html>`;
|
|
13583
13705
|
var FAILURE_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>Sign-in didn't complete</h2><p>Return to Gutterpress and try again.</p></body></html>`;
|
|
13584
13706
|
function waitForCallback(server, expectedState, redirectUri, timeoutMs, signal) {
|
|
13585
|
-
return new Promise((
|
|
13707
|
+
return new Promise((resolve9, reject) => {
|
|
13586
13708
|
let settled = false;
|
|
13587
13709
|
const cleanup = () => {
|
|
13588
13710
|
clearTimeout(timer);
|
|
@@ -13596,7 +13718,7 @@ function waitForCallback(server, expectedState, redirectUri, timeoutMs, signal)
|
|
|
13596
13718
|
return;
|
|
13597
13719
|
settled = true;
|
|
13598
13720
|
cleanup();
|
|
13599
|
-
|
|
13721
|
+
resolve9(value);
|
|
13600
13722
|
};
|
|
13601
13723
|
const finishReject = (err) => {
|
|
13602
13724
|
if (settled)
|
|
@@ -13668,14 +13790,14 @@ class GoogleAuthProvider {
|
|
|
13668
13790
|
const state = b64url(crypto.randomBytes(16));
|
|
13669
13791
|
const challenge = pkceChallengeFromVerifier(verifier);
|
|
13670
13792
|
const server = http2.createServer();
|
|
13671
|
-
await new Promise((
|
|
13793
|
+
await new Promise((resolve9, reject) => {
|
|
13672
13794
|
const onListenError = (err) => {
|
|
13673
13795
|
reject(new Error(`Couldn't start the local sign-in listener: ${err.message}`));
|
|
13674
13796
|
};
|
|
13675
13797
|
server.once("error", onListenError);
|
|
13676
13798
|
server.listen(this.bindPort, "127.0.0.1", () => {
|
|
13677
13799
|
server.removeListener("error", onListenError);
|
|
13678
|
-
|
|
13800
|
+
resolve9();
|
|
13679
13801
|
});
|
|
13680
13802
|
});
|
|
13681
13803
|
const address = server.address();
|
|
@@ -14357,8 +14479,8 @@ async function testRemoteAccess(options) {
|
|
|
14357
14479
|
const credential = options.credential ?? urlCredential;
|
|
14358
14480
|
const timeoutMs = options.timeoutMs ?? 15000;
|
|
14359
14481
|
let timer;
|
|
14360
|
-
const timeout = new Promise((
|
|
14361
|
-
timer = setTimeout(() =>
|
|
14482
|
+
const timeout = new Promise((resolve9) => {
|
|
14483
|
+
timer = setTimeout(() => resolve9(failure("unreachable")), timeoutMs);
|
|
14362
14484
|
});
|
|
14363
14485
|
const probe = (async () => {
|
|
14364
14486
|
try {
|
|
@@ -14793,7 +14915,7 @@ var DEFAULT_SYNC_RETRY = {
|
|
|
14793
14915
|
backoffMs: 150
|
|
14794
14916
|
};
|
|
14795
14917
|
function defaultSleep2(ms) {
|
|
14796
|
-
return new Promise((
|
|
14918
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
14797
14919
|
}
|
|
14798
14920
|
async function syncProject(options) {
|
|
14799
14921
|
const http3 = options.httpClient ?? defaultGitHttp;
|
|
@@ -15176,7 +15298,7 @@ var drivethrurpgProvider = {
|
|
|
15176
15298
|
};
|
|
15177
15299
|
|
|
15178
15300
|
// src/lib/publish/providers/gdrive.ts
|
|
15179
|
-
import { mkdtemp as mkdtemp4, readFile as
|
|
15301
|
+
import { mkdtemp as mkdtemp4, readFile as readFile33, readdir as readdir10, rm as rm8, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
|
|
15180
15302
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
15181
15303
|
import path26 from "node:path";
|
|
15182
15304
|
import { Zip, ZipDeflate } from "fflate";
|
|
@@ -15325,7 +15447,7 @@ var RESUMABLE_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
|
15325
15447
|
var MAX_CHUNK_RETRIES = 3;
|
|
15326
15448
|
var RETRY_BASE_DELAY_MS = 500;
|
|
15327
15449
|
function defaultSleep3(ms) {
|
|
15328
|
-
return new Promise((
|
|
15450
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
15329
15451
|
}
|
|
15330
15452
|
async function startResumableSession(fetchImpl, accessToken, opts) {
|
|
15331
15453
|
const mimeType = opts.mimeType ?? "application/pdf";
|
|
@@ -15507,10 +15629,10 @@ function toDestination(folder) {
|
|
|
15507
15629
|
}
|
|
15508
15630
|
var ZIP_CHUNK_BYTES = 64 * 1024;
|
|
15509
15631
|
function yieldToEventLoop() {
|
|
15510
|
-
return new Promise((
|
|
15632
|
+
return new Promise((resolve9) => setImmediate(resolve9));
|
|
15511
15633
|
}
|
|
15512
15634
|
function zipEntriesNonBlocking(entries, opts) {
|
|
15513
|
-
return new Promise((
|
|
15635
|
+
return new Promise((resolve9, reject) => {
|
|
15514
15636
|
const chunks = [];
|
|
15515
15637
|
let total = 0;
|
|
15516
15638
|
let settled = false;
|
|
@@ -15534,7 +15656,7 @@ function zipEntriesNonBlocking(entries, opts) {
|
|
|
15534
15656
|
out.set(c, offset);
|
|
15535
15657
|
offset += c.length;
|
|
15536
15658
|
}
|
|
15537
|
-
|
|
15659
|
+
resolve9(out);
|
|
15538
15660
|
}
|
|
15539
15661
|
});
|
|
15540
15662
|
(async () => {
|
|
@@ -15568,12 +15690,12 @@ async function collectZipEntries(root, dir, out, onWarn) {
|
|
|
15568
15690
|
await collectZipEntries(root, full, out, onWarn);
|
|
15569
15691
|
} else if (entry.isFile()) {
|
|
15570
15692
|
const rel = path26.relative(root, full).split(path26.sep).join("/");
|
|
15571
|
-
out[rel] = await
|
|
15693
|
+
out[rel] = await readFile33(full);
|
|
15572
15694
|
} else if (entry.isSymbolicLink()) {
|
|
15573
15695
|
const resolved = await stat9(full).catch(() => null);
|
|
15574
15696
|
if (resolved?.isFile()) {
|
|
15575
15697
|
const rel = path26.relative(root, full).split(path26.sep).join("/");
|
|
15576
|
-
out[rel] = await
|
|
15698
|
+
out[rel] = await readFile33(full);
|
|
15577
15699
|
} else if (resolved?.isDirectory()) {
|
|
15578
15700
|
await collectZipEntries(root, full, out, onWarn);
|
|
15579
15701
|
} else {
|
|
@@ -16390,7 +16512,7 @@ async function disconnectPublishCredential(key, deps, options = {}) {
|
|
|
16390
16512
|
}
|
|
16391
16513
|
}
|
|
16392
16514
|
// src/lib/publish/selections.ts
|
|
16393
|
-
import { chmod as chmod3, mkdir as mkdir18, readFile as
|
|
16515
|
+
import { chmod as chmod3, mkdir as mkdir18, readFile as readFile34, writeFile as writeFile16 } from "node:fs/promises";
|
|
16394
16516
|
import path29 from "node:path";
|
|
16395
16517
|
class PublishSelectionsStore {
|
|
16396
16518
|
filePath;
|
|
@@ -16400,7 +16522,7 @@ class PublishSelectionsStore {
|
|
|
16400
16522
|
}
|
|
16401
16523
|
async read() {
|
|
16402
16524
|
try {
|
|
16403
|
-
const raw = await
|
|
16525
|
+
const raw = await readFile34(this.filePath, "utf8");
|
|
16404
16526
|
const parsed = JSON.parse(raw);
|
|
16405
16527
|
if (parsed && typeof parsed === "object") {
|
|
16406
16528
|
return {
|
|
@@ -16516,4 +16638,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
|
|
|
16516
16638
|
await writeManifestDoc(file, doc);
|
|
16517
16639
|
return settingsFromDoc(doc);
|
|
16518
16640
|
}
|
|
16519
|
-
export { PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME,
|
|
16641
|
+
export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, formatReport, getChecks, getCheckById, resolveCheckSelectors, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, MAX_SEARCH_BYTES, SEARCH_KEYWORDS, searchNpmExtensions, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
|