@zeropress/build-pages 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -26
- package/action.yml +3 -0
- package/dist/action.js +465 -132
- package/dist/prebuild.js +110 -5
- package/package.json +2 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +50 -3
- package/src/action.js +1 -0
- package/src/index.js +154 -3
- package/src/prebuild.js +134 -5
- package/themes/docs/404.html +13 -2
- package/themes/docs/assets/style.css +975 -154
- package/themes/docs/assets/theme.js +333 -0
- package/themes/docs/layout.html +73 -14
- package/themes/docs/page.html +9 -14
- package/themes/docs/partials/theme-bootstrap.html +10 -0
- package/themes/docs/partials/theme-scripts.html +1 -0
- package/themes/docs/post.html +5 -2
- package/themes/docs/theme.json +10 -7
package/dist/prebuild.js
CHANGED
|
@@ -3524,6 +3524,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3524
3524
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3525
3525
|
var rootDir = process.cwd();
|
|
3526
3526
|
var sourceDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_SOURCE"], "docs");
|
|
3527
|
+
var publicDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_PUBLIC_DIR"], sourceDir);
|
|
3527
3528
|
var defaultConfigPath = path.join(sourceDir, ".zeropress", "config.json");
|
|
3528
3529
|
var configPath = resolveOptionalEnvPath(["ZEROPRESS_BUILD_PAGES_CONFIG"], defaultConfigPath);
|
|
3529
3530
|
var outDir = path.join(rootDir, ".zeropress");
|
|
@@ -3533,13 +3534,14 @@ var buildReportPath = path.join(outDir, "build-report.json");
|
|
|
3533
3534
|
var skipUntitledMarkdown = readBooleanEnv("ZEROPRESS_SKIP_UNTITLED_MARKDOWN");
|
|
3534
3535
|
var copyMarkdownSource = readBooleanEnv("ZEROPRESS_COPY_MARKDOWN_SOURCE", true);
|
|
3535
3536
|
var FRONT_PAGE_TYPES = /* @__PURE__ */ new Set(["theme_index", "markdown", "html"]);
|
|
3536
|
-
var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://zeropress.dev/
|
|
3537
|
-
var PREVIEW_DATA_SCHEMA_URL = "https://zeropress.dev/
|
|
3537
|
+
var BUILD_PAGES_CONFIG_SCHEMA_URL = "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json";
|
|
3538
|
+
var PREVIEW_DATA_SCHEMA_URL = "https://schemas.zeropress.dev/preview-data/v0.6/schema.json";
|
|
3538
3539
|
var FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
|
|
3539
3540
|
var FRONT_MATTER_DATA_MAX_DEPTH = 4;
|
|
3540
3541
|
var FRONT_MATTER_DATA_MAX_KEYS = 64;
|
|
3541
3542
|
var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
|
|
3542
3543
|
var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
|
|
3544
|
+
var markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
|
|
3543
3545
|
var PrebuildMarkdownError = class extends Error {
|
|
3544
3546
|
constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
|
|
3545
3547
|
super(reason);
|
|
@@ -3727,7 +3729,7 @@ function buildSiteData(config, frontPage) {
|
|
|
3727
3729
|
description: configuredSite.description,
|
|
3728
3730
|
url: configuredSite.url,
|
|
3729
3731
|
media_base_url: "",
|
|
3730
|
-
locale:
|
|
3732
|
+
locale: configuredSite.locale,
|
|
3731
3733
|
posts_per_page: 10,
|
|
3732
3734
|
datetime_display: "static",
|
|
3733
3735
|
date_style: "medium",
|
|
@@ -3740,8 +3742,15 @@ function buildSiteData(config, frontPage) {
|
|
|
3740
3742
|
},
|
|
3741
3743
|
disallow_comments: true,
|
|
3742
3744
|
expose_generator: configuredSite.expose_generator !== false,
|
|
3745
|
+
search: configuredSite.search !== false,
|
|
3743
3746
|
indexing: configuredSite.indexing !== false
|
|
3744
3747
|
};
|
|
3748
|
+
if (configuredSite.logo) {
|
|
3749
|
+
site.logo = configuredSite.logo;
|
|
3750
|
+
}
|
|
3751
|
+
if (configuredSite.meta !== void 0) {
|
|
3752
|
+
site.meta = configuredSite.meta;
|
|
3753
|
+
}
|
|
3745
3754
|
if (configuredSite.footer) {
|
|
3746
3755
|
site.footer = configuredSite.footer;
|
|
3747
3756
|
}
|
|
@@ -3768,20 +3777,83 @@ function normalizeSiteConfig(value) {
|
|
|
3768
3777
|
);
|
|
3769
3778
|
}
|
|
3770
3779
|
const configuredSite = isPlainObject(value) ? value : {};
|
|
3771
|
-
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "indexing", "footer"], "site");
|
|
3780
|
+
assertKnownConfigKeys(configuredSite, ["title", "description", "url", "logo", "locale", "expose_generator", "search", "indexing", "footer", "meta"], "site");
|
|
3772
3781
|
const site = {
|
|
3773
3782
|
title: readConfigString(configuredSite.title, "Documentation"),
|
|
3774
3783
|
description: readConfigString(configuredSite.description, "A documentation site."),
|
|
3775
3784
|
url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
|
|
3785
|
+
locale: normalizeSiteLocale(configuredSite.locale),
|
|
3776
3786
|
expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
|
|
3787
|
+
search: readConfigBoolean(configuredSite.search, true, "site.search"),
|
|
3777
3788
|
indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
|
|
3778
3789
|
};
|
|
3790
|
+
const logo = normalizeSiteLogo(configuredSite.logo);
|
|
3791
|
+
if (logo) {
|
|
3792
|
+
site.logo = logo;
|
|
3793
|
+
}
|
|
3779
3794
|
const footer = normalizeFooter(configuredSite.footer);
|
|
3780
3795
|
if (footer) {
|
|
3781
3796
|
site.footer = footer;
|
|
3782
3797
|
}
|
|
3798
|
+
if (configuredSite.meta !== void 0) {
|
|
3799
|
+
site.meta = normalizeSiteMeta(configuredSite.meta, "site.meta");
|
|
3800
|
+
}
|
|
3783
3801
|
return site;
|
|
3784
3802
|
}
|
|
3803
|
+
function normalizeSiteLocale(value) {
|
|
3804
|
+
if (value === void 0) {
|
|
3805
|
+
return "en-US";
|
|
3806
|
+
}
|
|
3807
|
+
if (typeof value !== "string") {
|
|
3808
|
+
throw new PrebuildConfigError("site.locale must be a string when provided.");
|
|
3809
|
+
}
|
|
3810
|
+
const locale = value.trim();
|
|
3811
|
+
if (locale.length < 2) {
|
|
3812
|
+
throw new PrebuildConfigError('site.locale must be a non-empty locale string such as "en-US" or "ko-KR".');
|
|
3813
|
+
}
|
|
3814
|
+
return locale;
|
|
3815
|
+
}
|
|
3816
|
+
function normalizeSiteLogo(value) {
|
|
3817
|
+
if (value === void 0) {
|
|
3818
|
+
return void 0;
|
|
3819
|
+
}
|
|
3820
|
+
if (!isPlainObject(value)) {
|
|
3821
|
+
throw new PrebuildConfigError("site.logo must be an object when provided.");
|
|
3822
|
+
}
|
|
3823
|
+
assertKnownConfigKeys(value, ["src", "alt"], "site.logo");
|
|
3824
|
+
const src = readConfigString(value.src, "");
|
|
3825
|
+
if (!src) {
|
|
3826
|
+
throw new PrebuildConfigError(
|
|
3827
|
+
"site.logo.src must be a non-empty URL-like string.",
|
|
3828
|
+
' "logo": { "src": "/logo.svg", "alt": "My Site" }'
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
validateUrlLikeString(src, "site.logo.src");
|
|
3832
|
+
const logo = { src };
|
|
3833
|
+
if (value.alt !== void 0) {
|
|
3834
|
+
if (typeof value.alt !== "string") {
|
|
3835
|
+
throw new PrebuildConfigError("site.logo.alt must be a string when provided.");
|
|
3836
|
+
}
|
|
3837
|
+
const alt = value.alt.trim();
|
|
3838
|
+
if (alt) {
|
|
3839
|
+
logo.alt = alt;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return logo;
|
|
3843
|
+
}
|
|
3844
|
+
function normalizeSiteMeta(value, pathLabel) {
|
|
3845
|
+
if (!isPlainObject(value)) {
|
|
3846
|
+
throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
|
|
3847
|
+
}
|
|
3848
|
+
const meta = {};
|
|
3849
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
3850
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
3851
|
+
throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
|
|
3852
|
+
}
|
|
3853
|
+
meta[key] = metaValue;
|
|
3854
|
+
}
|
|
3855
|
+
return meta;
|
|
3856
|
+
}
|
|
3785
3857
|
function normalizeFooter(value) {
|
|
3786
3858
|
if (value === void 0) {
|
|
3787
3859
|
return void 0;
|
|
@@ -3803,6 +3875,22 @@ function normalizeFooter(value) {
|
|
|
3803
3875
|
}
|
|
3804
3876
|
return Object.keys(footer).length ? footer : void 0;
|
|
3805
3877
|
}
|
|
3878
|
+
function validateUrlLikeString(value, pathLabel) {
|
|
3879
|
+
if (value.startsWith("//")) {
|
|
3880
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
3881
|
+
}
|
|
3882
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
try {
|
|
3886
|
+
const url = new URL(value);
|
|
3887
|
+
if (!url.protocol || !url.hostname) {
|
|
3888
|
+
throw new Error("missing host");
|
|
3889
|
+
}
|
|
3890
|
+
} catch {
|
|
3891
|
+
throw new PrebuildConfigError(`${pathLabel} must be an absolute URL or a safe relative path.`);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3806
3894
|
function readConfigBoolean(value, fallback, pathName) {
|
|
3807
3895
|
if (value === void 0) {
|
|
3808
3896
|
return fallback;
|
|
@@ -4184,6 +4272,7 @@ function buildPrebuildReport({
|
|
|
4184
4272
|
return {
|
|
4185
4273
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4186
4274
|
source_dir: formatSourcePath(sourceDir),
|
|
4275
|
+
public_dir: formatSourcePath(publicDir),
|
|
4187
4276
|
config_path: formatSourcePath(configPath),
|
|
4188
4277
|
build_pages_config_path: formatSourcePath(buildPagesConfigPath),
|
|
4189
4278
|
preview_data_path: formatSourcePath(previewDataPath),
|
|
@@ -4209,7 +4298,8 @@ function buildPrebuildReport({
|
|
|
4209
4298
|
function printPrebuildSummary(report) {
|
|
4210
4299
|
const lines = [
|
|
4211
4300
|
"ZeroPress build report",
|
|
4212
|
-
`-
|
|
4301
|
+
`- Source root: ${report.source_dir}`,
|
|
4302
|
+
`- Public root: ${report.public_dir}`,
|
|
4213
4303
|
`- Markdown discovered: ${report.markdown.discovered}`,
|
|
4214
4304
|
`- Markdown pages generated: ${report.markdown.generated_pages}`,
|
|
4215
4305
|
`- Markdown skipped: ${report.markdown.skipped}`,
|
|
@@ -4497,6 +4587,9 @@ async function listMarkdownFiles(dir) {
|
|
|
4497
4587
|
continue;
|
|
4498
4588
|
}
|
|
4499
4589
|
const entryPath = path.join(dir, entry.name);
|
|
4590
|
+
if (isMarkdownDiscoverExcluded(entryPath)) {
|
|
4591
|
+
continue;
|
|
4592
|
+
}
|
|
4500
4593
|
if (entry.isDirectory()) {
|
|
4501
4594
|
files.push(...await listMarkdownFiles(entryPath));
|
|
4502
4595
|
continue;
|
|
@@ -4507,6 +4600,18 @@ async function listMarkdownFiles(dir) {
|
|
|
4507
4600
|
}
|
|
4508
4601
|
return files.sort((left, right) => left.localeCompare(right));
|
|
4509
4602
|
}
|
|
4603
|
+
function buildMarkdownDiscoverExcludeRoots() {
|
|
4604
|
+
if (samePath(sourceDir, publicDir) || !isPathInside(sourceDir, publicDir)) {
|
|
4605
|
+
return [];
|
|
4606
|
+
}
|
|
4607
|
+
return [publicDir];
|
|
4608
|
+
}
|
|
4609
|
+
function isMarkdownDiscoverExcluded(entryPath) {
|
|
4610
|
+
return markdownDiscoverExcludeRoots.some((excludeRoot) => samePath(entryPath, excludeRoot) || isPathInside(excludeRoot, entryPath));
|
|
4611
|
+
}
|
|
4612
|
+
function samePath(firstPath, secondPath) {
|
|
4613
|
+
return path.resolve(firstPath) === path.resolve(secondPath);
|
|
4614
|
+
}
|
|
4510
4615
|
function shouldIgnoreMarkdownDiscoverEntry(name) {
|
|
4511
4616
|
const basename = String(name || "");
|
|
4512
4617
|
const lowerName = basename.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-pages",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "ZeroPress Markdown build action and CLI for static hosting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18.18.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@zeropress/build": "0.6.
|
|
43
|
+
"@zeropress/build": "0.6.3",
|
|
44
44
|
"gray-matter": "4.0.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://zeropress.dev/
|
|
3
|
+
"$id": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
|
|
4
4
|
"title": "ZeroPress Build Pages Config v0.1",
|
|
5
5
|
"description": "Optional source-directory configuration for @zeropress/build-pages workflows.",
|
|
6
6
|
"markdownDescription": "Optional source-directory configuration for `@zeropress/build-pages` workflows.",
|
|
@@ -50,12 +50,27 @@
|
|
|
50
50
|
"description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
|
|
51
51
|
"markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
|
|
52
52
|
},
|
|
53
|
+
"logo": {
|
|
54
|
+
"$ref": "#/$defs/siteLogo"
|
|
55
|
+
},
|
|
56
|
+
"locale": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"minLength": 2,
|
|
59
|
+
"description": "Site locale used for generated preview-data language metadata, such as html lang and feed language.",
|
|
60
|
+
"markdownDescription": "Site locale used for generated preview-data language metadata, such as HTML `lang` and feed language."
|
|
61
|
+
},
|
|
53
62
|
"expose_generator": {
|
|
54
63
|
"type": "boolean",
|
|
55
64
|
"default": true,
|
|
56
65
|
"description": "Whether generated HTML should expose the ZeroPress generator meta tag. Set false for white-label sites.",
|
|
57
66
|
"markdownDescription": "Whether generated HTML should expose `<meta name=\"generator\" content=\"ZeroPress\">`. Set `false` for white-label sites."
|
|
58
67
|
},
|
|
68
|
+
"search": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"default": true,
|
|
71
|
+
"description": "Whether native ZeroPress search should be enabled when the selected theme supports search UI. Set false to omit native search artifacts and hide theme search UI.",
|
|
72
|
+
"markdownDescription": "Whether native ZeroPress search should be enabled when the selected theme supports search UI. Set `false` to omit native search artifacts and hide theme search UI."
|
|
73
|
+
},
|
|
59
74
|
"indexing": {
|
|
60
75
|
"type": "boolean",
|
|
61
76
|
"default": true,
|
|
@@ -64,6 +79,29 @@
|
|
|
64
79
|
},
|
|
65
80
|
"footer": {
|
|
66
81
|
"$ref": "#/$defs/siteFooter"
|
|
82
|
+
},
|
|
83
|
+
"meta": {
|
|
84
|
+
"$ref": "#/$defs/previewMeta"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"siteLogo": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"required": ["src"],
|
|
92
|
+
"description": "Optional site logo data copied into generated preview-data.",
|
|
93
|
+
"markdownDescription": "Optional site logo data copied into generated preview-data.",
|
|
94
|
+
"properties": {
|
|
95
|
+
"src": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"minLength": 1,
|
|
98
|
+
"description": "Logo URL or safe relative path.",
|
|
99
|
+
"markdownDescription": "Logo URL or safe relative path."
|
|
100
|
+
},
|
|
101
|
+
"alt": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Optional logo alternative text. Themes may fall back to site.title when omitted.",
|
|
104
|
+
"markdownDescription": "Optional logo alternative text. Themes may fall back to `site.title` when omitted."
|
|
67
105
|
}
|
|
68
106
|
}
|
|
69
107
|
},
|
|
@@ -285,14 +323,23 @@
|
|
|
285
323
|
},
|
|
286
324
|
"examples": [
|
|
287
325
|
{
|
|
288
|
-
"$schema": "https://zeropress.dev/
|
|
326
|
+
"$schema": "https://schemas.zeropress.dev/build-pages-config/v0.1/schema.json",
|
|
289
327
|
"version": "0.1",
|
|
290
328
|
"site": {
|
|
291
329
|
"title": "ZeroPress Public Docs",
|
|
292
330
|
"description": "Public documentation.",
|
|
293
331
|
"url": "https://zeropress.dev",
|
|
332
|
+
"logo": {
|
|
333
|
+
"src": "/logo.svg",
|
|
334
|
+
"alt": "ZeroPress Public Docs"
|
|
335
|
+
},
|
|
336
|
+
"locale": "en-US",
|
|
294
337
|
"expose_generator": true,
|
|
295
|
-
"
|
|
338
|
+
"search": true,
|
|
339
|
+
"indexing": true,
|
|
340
|
+
"meta": {
|
|
341
|
+
"issue": "Spring 2026"
|
|
342
|
+
}
|
|
296
343
|
},
|
|
297
344
|
"front_page": {
|
|
298
345
|
"type": "markdown"
|
package/src/action.js
CHANGED
package/src/index.js
CHANGED
|
@@ -39,6 +39,8 @@ export async function runBuildPages(options) {
|
|
|
39
39
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
40
40
|
const copyMarkdownSource = options.copyMarkdownSource !== false;
|
|
41
41
|
const sourceDir = path.resolve(cwd, options.source);
|
|
42
|
+
const publicDirExplicit = hasExplicitPublicDir(options);
|
|
43
|
+
const publicDir = publicDirExplicit ? path.resolve(cwd, options.publicDir) : sourceDir;
|
|
42
44
|
const destinationDir = path.resolve(cwd, options.destination);
|
|
43
45
|
const generatedDir = path.join(cwd, '.zeropress');
|
|
44
46
|
const stagingDir = path.join(cwd, STAGING_DIR);
|
|
@@ -48,18 +50,23 @@ export async function runBuildPages(options) {
|
|
|
48
50
|
assertBuildPagesPathLayout({
|
|
49
51
|
cwd,
|
|
50
52
|
sourceDir,
|
|
53
|
+
publicDir,
|
|
54
|
+
publicDirExplicit,
|
|
51
55
|
destinationDir,
|
|
52
56
|
themeDir,
|
|
53
57
|
generatedDir,
|
|
54
58
|
});
|
|
55
59
|
await assertDirectory(sourceDir, 'Source directory');
|
|
60
|
+
await assertPublicDirectory(publicDir, publicDirExplicit);
|
|
61
|
+
await assertDestinationPath(destinationDir);
|
|
56
62
|
await fs.rm(generatedDir, { recursive: true, force: true });
|
|
57
63
|
await fs.mkdir(generatedDir, { recursive: true });
|
|
58
64
|
|
|
59
65
|
const env = {
|
|
60
66
|
...process.env,
|
|
61
67
|
ZEROPRESS_BUILD_PAGES_SOURCE: sourceDir,
|
|
62
|
-
|
|
68
|
+
ZEROPRESS_BUILD_PAGES_PUBLIC_DIR: publicDir,
|
|
69
|
+
ZEROPRESS_PUBLIC_DIR: publicDir,
|
|
63
70
|
ZEROPRESS_SKIP_UNTITLED_MARKDOWN: String(Boolean(options.skipUntitledMarkdown)),
|
|
64
71
|
ZEROPRESS_COPY_MARKDOWN_SOURCE: String(copyMarkdownSource),
|
|
65
72
|
};
|
|
@@ -85,10 +92,13 @@ export async function runBuildPages(options) {
|
|
|
85
92
|
await fs.rm(destinationDir, { recursive: true, force: true });
|
|
86
93
|
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
87
94
|
await fs.mkdir(stagingDir, { recursive: true });
|
|
88
|
-
await copyPublicStaging(
|
|
95
|
+
await copyPublicStaging(publicDir, stagingDir, {
|
|
89
96
|
excludePaths: [destinationDir, themeDir, generatedDir],
|
|
90
97
|
copyMarkdownSource,
|
|
91
98
|
});
|
|
99
|
+
if (copyMarkdownSource) {
|
|
100
|
+
await copySourceMarkdownFiles(sourceDir, stagingDir, previewData);
|
|
101
|
+
}
|
|
92
102
|
|
|
93
103
|
const previousPublicDir = process.env.ZEROPRESS_PUBLIC_DIR;
|
|
94
104
|
process.env.ZEROPRESS_PUBLIC_DIR = stagingDir;
|
|
@@ -137,6 +147,7 @@ export function parseArgs(argv) {
|
|
|
137
147
|
|
|
138
148
|
const valueOptions = new Set([
|
|
139
149
|
'--source',
|
|
150
|
+
'--public-dir',
|
|
140
151
|
'--destination',
|
|
141
152
|
'--theme',
|
|
142
153
|
'--theme-path',
|
|
@@ -167,6 +178,7 @@ export function parseArgs(argv) {
|
|
|
167
178
|
|
|
168
179
|
return {
|
|
169
180
|
source,
|
|
181
|
+
publicDir: flags['public-dir'] || '',
|
|
170
182
|
destination,
|
|
171
183
|
theme: flags.theme || DEFAULT_THEME,
|
|
172
184
|
themePath: flags['theme-path'] || '',
|
|
@@ -186,6 +198,7 @@ Usage:
|
|
|
186
198
|
|
|
187
199
|
Options:
|
|
188
200
|
--source <dir> Dedicated source directory (required)
|
|
201
|
+
--public-dir <dir> Public passthrough directory (default: source)
|
|
189
202
|
--destination <dir> Output directory (required)
|
|
190
203
|
--theme docs Bundled theme name (default: docs)
|
|
191
204
|
--theme-path <dir> Custom ZeroPress theme directory
|
|
@@ -208,6 +221,10 @@ function resolveThemeDir(cwd, options) {
|
|
|
208
221
|
throw new Error(`Unknown bundled theme: ${options.theme}`);
|
|
209
222
|
}
|
|
210
223
|
|
|
224
|
+
function hasExplicitPublicDir(options) {
|
|
225
|
+
return typeof options.publicDir === 'string' && Boolean(options.publicDir.trim());
|
|
226
|
+
}
|
|
227
|
+
|
|
211
228
|
async function assertDirectory(dir, label) {
|
|
212
229
|
let stat;
|
|
213
230
|
try {
|
|
@@ -223,7 +240,55 @@ async function assertDirectory(dir, label) {
|
|
|
223
240
|
}
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
function
|
|
243
|
+
async function assertPublicDirectory(publicDir, explicit) {
|
|
244
|
+
if (!explicit) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let stat;
|
|
249
|
+
try {
|
|
250
|
+
stat = await fs.lstat(publicDir);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error?.code === 'ENOENT') {
|
|
253
|
+
throw new Error(`Public directory not found: ${publicDir}`);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (stat.isSymbolicLink()) {
|
|
259
|
+
throw new Error(`Public directory must not be a symbolic link: ${publicDir}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!stat.isDirectory()) {
|
|
263
|
+
throw new Error(`Public path is not a directory: ${publicDir}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function assertDestinationPath(destinationDir) {
|
|
268
|
+
let stat;
|
|
269
|
+
try {
|
|
270
|
+
stat = await fs.lstat(destinationDir);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error?.code === 'ENOENT') {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!stat.isDirectory()) {
|
|
279
|
+
throw new Error(`Destination path is not a directory: ${destinationDir}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function assertBuildPagesPathLayout({
|
|
284
|
+
cwd,
|
|
285
|
+
sourceDir,
|
|
286
|
+
publicDir,
|
|
287
|
+
publicDirExplicit,
|
|
288
|
+
destinationDir,
|
|
289
|
+
themeDir,
|
|
290
|
+
generatedDir,
|
|
291
|
+
}) {
|
|
227
292
|
if (samePath(sourceDir, cwd)) {
|
|
228
293
|
throw new Error(
|
|
229
294
|
'Source directory must be a dedicated content directory, not the current working directory. '
|
|
@@ -231,11 +296,36 @@ function assertBuildPagesPathLayout({ cwd, sourceDir, destinationDir, themeDir,
|
|
|
231
296
|
);
|
|
232
297
|
}
|
|
233
298
|
|
|
299
|
+
if (publicDirExplicit && samePath(publicDir, cwd)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
'Public directory must be a dedicated asset directory, not the current working directory. '
|
|
302
|
+
+ `Received: ${formatPath(cwd, publicDir)}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
234
306
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'internal .zeropress working directory', generatedDir);
|
|
235
307
|
assertNoPathOverlap(cwd, 'Destination directory', destinationDir, 'internal .zeropress working directory', generatedDir);
|
|
236
308
|
assertNoPathOverlap(cwd, 'Theme directory', themeDir, 'internal .zeropress working directory', generatedDir);
|
|
309
|
+
if (!samePath(publicDir, sourceDir)) {
|
|
310
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'internal .zeropress working directory', generatedDir);
|
|
311
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'destination directory', destinationDir);
|
|
312
|
+
assertNoPathOverlap(cwd, 'Public directory', publicDir, 'theme directory', themeDir);
|
|
313
|
+
}
|
|
237
314
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'destination directory', destinationDir);
|
|
238
315
|
assertNoPathOverlap(cwd, 'Source directory', sourceDir, 'theme directory', themeDir);
|
|
316
|
+
assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir) {
|
|
320
|
+
if (samePath(sourceDir, publicDir) || !isPathInside(publicDir, sourceDir)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error(
|
|
325
|
+
'Source directory must not be inside the public directory. '
|
|
326
|
+
+ `Source directory: ${formatPath(cwd, sourceDir)}; `
|
|
327
|
+
+ `Public directory: ${formatPath(cwd, publicDir)}`,
|
|
328
|
+
);
|
|
239
329
|
}
|
|
240
330
|
|
|
241
331
|
function assertNoPathOverlap(cwd, firstLabel, firstPath, secondLabel, secondPath) {
|
|
@@ -282,6 +372,67 @@ async function copyPublicStaging(sourceDir, targetDir, options) {
|
|
|
282
372
|
}
|
|
283
373
|
}
|
|
284
374
|
|
|
375
|
+
async function copySourceMarkdownFiles(sourceDir, targetDir, previewData) {
|
|
376
|
+
const markdownUrls = new Set();
|
|
377
|
+
|
|
378
|
+
for (const page of previewData?.content?.pages || []) {
|
|
379
|
+
const sourceMarkdownUrl = page?.meta?.source_markdown_url;
|
|
380
|
+
if (typeof sourceMarkdownUrl === 'string' && sourceMarkdownUrl) {
|
|
381
|
+
markdownUrls.add(sourceMarkdownUrl);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const sourceMarkdownUrl of markdownUrls) {
|
|
386
|
+
const relativePath = sourceMarkdownUrlToRelativePath(sourceMarkdownUrl);
|
|
387
|
+
if (!relativePath) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
392
|
+
if (!isPathInside(sourceDir, sourcePath)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
397
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
398
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sourceMarkdownUrlToRelativePath(sourceMarkdownUrl) {
|
|
403
|
+
if (
|
|
404
|
+
!sourceMarkdownUrl.startsWith('/')
|
|
405
|
+
|| sourceMarkdownUrl.includes('?')
|
|
406
|
+
|| sourceMarkdownUrl.includes('#')
|
|
407
|
+
) {
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const rawSegments = sourceMarkdownUrl.slice(1).split('/');
|
|
412
|
+
const segments = [];
|
|
413
|
+
for (const rawSegment of rawSegments) {
|
|
414
|
+
if (!rawSegment) {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let segment;
|
|
419
|
+
try {
|
|
420
|
+
segment = decodeURIComponent(rawSegment);
|
|
421
|
+
} catch {
|
|
422
|
+
return '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\')) {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
segments.push(segment);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const relativePath = segments.join('/');
|
|
433
|
+
return relativePath.toLowerCase().endsWith('.md') ? relativePath : '';
|
|
434
|
+
}
|
|
435
|
+
|
|
285
436
|
function shouldIgnorePublicEntry(name) {
|
|
286
437
|
const basename = String(name || '');
|
|
287
438
|
const lowerName = basename.toLowerCase();
|