@heroiclands/package-build 22.15.1 → 22.16.0
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/CHANGELOG.md +95 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Render and inspect a new content package without requiring a project config. */
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
import prettier from "prettier";
|
|
14
|
+
import prettierConfig from "../prettier-config.mjs";
|
|
15
|
+
import { CONFIG_FILENAMES, configFromData } from "./pack-config.mjs";
|
|
16
|
+
import { checkLabelRegistry } from "../labels.mjs";
|
|
17
|
+
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
18
|
+
|
|
19
|
+
/** Package kinds the initializer can create. */
|
|
20
|
+
export const INIT_KINDS = Object.freeze(["systems", "modules", "documentation"]);
|
|
21
|
+
/** License templates the initializer can create. */
|
|
22
|
+
export const INIT_LICENSES = Object.freeze(["original", "fan"]);
|
|
23
|
+
|
|
24
|
+
const REQUIRED_SCRIPTS = Object.freeze([
|
|
25
|
+
"prepare",
|
|
26
|
+
"clean",
|
|
27
|
+
"build",
|
|
28
|
+
"build:noci",
|
|
29
|
+
"build:content-index",
|
|
30
|
+
"build:site",
|
|
31
|
+
"build:book",
|
|
32
|
+
"serve:site",
|
|
33
|
+
"lint",
|
|
34
|
+
"format",
|
|
35
|
+
"changeset",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** @param {string} value */
|
|
39
|
+
function nonempty(value) {
|
|
40
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate values before creating a target directory.
|
|
45
|
+
* @param {object} answers
|
|
46
|
+
*/
|
|
47
|
+
export function validateInitAnswers(answers) {
|
|
48
|
+
for (const key of ["name", "title", "description", "author"]) {
|
|
49
|
+
if (!nonempty(answers[key])) throw new Error(`init: --${key} is required`);
|
|
50
|
+
}
|
|
51
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(answers.name)) {
|
|
52
|
+
throw new Error("init: --name must be lowercase letters, digits, and internal hyphens");
|
|
53
|
+
}
|
|
54
|
+
const contentPackage = answers.name.replaceAll("-", "");
|
|
55
|
+
if (!contentPackage) throw new Error("init: --name must yield a content package id");
|
|
56
|
+
if (!INIT_KINDS.includes(answers.kind)) {
|
|
57
|
+
throw new Error(`init: --kind must be one of ${INIT_KINDS.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
if (!INIT_LICENSES.includes(answers.license)) {
|
|
60
|
+
throw new Error(`init: --license must be one of ${INIT_LICENSES.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
if (!nonempty(answers.version)) throw new Error("init: toolchain version is unavailable");
|
|
63
|
+
if (answers.kind !== "documentation") {
|
|
64
|
+
for (const key of ["coreMinimum", "coreVerified"]) {
|
|
65
|
+
if (!nonempty(answers[key])) throw new Error(`init: --${key} is required`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ...answers, contentPackage };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {object} answers */
|
|
72
|
+
function projectScripts(answers) {
|
|
73
|
+
const scripts = {
|
|
74
|
+
prepare:
|
|
75
|
+
"if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git config core.hooksPath node_modules/@heroiclands/package-build/githooks; fi",
|
|
76
|
+
clean: "package-build clean",
|
|
77
|
+
distclean: "package-build clean --distclean",
|
|
78
|
+
build: "npm ci && npm run build:noci",
|
|
79
|
+
"build:local": "npm install && npm run build:noci",
|
|
80
|
+
"build:content-index": "package-build content-index",
|
|
81
|
+
"build:deps": "package-build deps fetch",
|
|
82
|
+
"build:site-content": "package-build site",
|
|
83
|
+
"build:site-html": "hugo --source build/hugo --minify --gc --cleanDestinationDir",
|
|
84
|
+
"build:site-root": "package-build site-root",
|
|
85
|
+
"build:site":
|
|
86
|
+
"run-s build:deps build:content-index build:site-content build:site-html build:site-root",
|
|
87
|
+
"serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
|
|
88
|
+
"serve:site-html": "hugo server --source build/hugo",
|
|
89
|
+
"build:book": "package-build pdf",
|
|
90
|
+
lint: "run-s lint:format lint:markdown lint:addresses lint:content-links lint:labels",
|
|
91
|
+
"lint:format": "package-build format",
|
|
92
|
+
"lint:markdown": "package-build markdown",
|
|
93
|
+
"lint:markdown:fix": "package-build markdown --fix",
|
|
94
|
+
"lint:addresses": "package-build lint",
|
|
95
|
+
"lint:content-links": "package-build links",
|
|
96
|
+
"lint:labels": "package-build labels check",
|
|
97
|
+
format: "package-build format --write",
|
|
98
|
+
"format:check": "package-build format",
|
|
99
|
+
changeset: "changeset",
|
|
100
|
+
"changeset:version":
|
|
101
|
+
"changeset version && package-build changelog group && npm install --package-lock-only",
|
|
102
|
+
};
|
|
103
|
+
if (answers.kind === "documentation") {
|
|
104
|
+
scripts["build:noci"] = "run-s lint build:content-index";
|
|
105
|
+
return scripts;
|
|
106
|
+
}
|
|
107
|
+
Object.assign(scripts, {
|
|
108
|
+
"build:assets": "package-build assets",
|
|
109
|
+
"build:stage-reset": "package-build stage reset",
|
|
110
|
+
"build:compiledb": "package-build package compile",
|
|
111
|
+
"build:unpackdb": "package-build package unpack",
|
|
112
|
+
"build:module": "package-build manifest",
|
|
113
|
+
"build:db": "run-s build:content-index build:assets build:compiledb",
|
|
114
|
+
"build:noci": "run-s lint build:stage-reset build:db build:module",
|
|
115
|
+
"build:pack-release": "package-build release",
|
|
116
|
+
"lint:lang": "package-build lang check",
|
|
117
|
+
"push:dev": "package-build deploy dev",
|
|
118
|
+
"push:qa": "package-build deploy qa",
|
|
119
|
+
"push:prod": "package-build deploy prod",
|
|
120
|
+
});
|
|
121
|
+
scripts.lint =
|
|
122
|
+
"run-s lint:format lint:markdown lint:lang lint:addresses lint:content-links lint:labels";
|
|
123
|
+
return scripts;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @param {object} answers */
|
|
127
|
+
function projectConfig(answers) {
|
|
128
|
+
const config = {
|
|
129
|
+
contentPackage: answers.contentPackage,
|
|
130
|
+
packageKind: answers.kind,
|
|
131
|
+
};
|
|
132
|
+
if (answers.kind !== "documentation") {
|
|
133
|
+
config.compatibility = {
|
|
134
|
+
minimum: answers.coreMinimum,
|
|
135
|
+
verified: answers.coreVerified,
|
|
136
|
+
};
|
|
137
|
+
config.stats = { lastModifiedBy: "builder000000000" };
|
|
138
|
+
config.packs = [{ name: "journals", label: "Journals", type: "JournalEntry" }];
|
|
139
|
+
}
|
|
140
|
+
config.publish = { address: { prefix: "" } };
|
|
141
|
+
config.pdf = { title: answers.title, document: "book.yaml", out: "build/pdf" };
|
|
142
|
+
config.site = {
|
|
143
|
+
...(answers.kind === "documentation" ? { title: answers.title } : {}),
|
|
144
|
+
assets: "https://cdn.heroiclands.org",
|
|
145
|
+
description: answers.description,
|
|
146
|
+
};
|
|
147
|
+
if (answers.kind !== "documentation") {
|
|
148
|
+
config.packageBuild = {
|
|
149
|
+
assets: [
|
|
150
|
+
{ from: "README.md", to: "README.md" },
|
|
151
|
+
{ from: "LICENSE.md", to: "LICENSE.md" },
|
|
152
|
+
{ from: "lang", to: "lang" },
|
|
153
|
+
],
|
|
154
|
+
manifest: {
|
|
155
|
+
title: answers.title,
|
|
156
|
+
descriptionHtml: answers.description,
|
|
157
|
+
authors: [{ name: answers.author }],
|
|
158
|
+
license: "LICENSE.md",
|
|
159
|
+
readme: "README.md",
|
|
160
|
+
languages: [{ lang: "en", name: "English", path: "lang/en.json" }],
|
|
161
|
+
packFolders: [{ name: answers.title, sorting: "m", packs: ["journals"] }],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {object} answers */
|
|
169
|
+
function projectManifest(answers) {
|
|
170
|
+
return {
|
|
171
|
+
name: answers.name,
|
|
172
|
+
version: "0.1.0",
|
|
173
|
+
private: true,
|
|
174
|
+
author: answers.author,
|
|
175
|
+
license:
|
|
176
|
+
answers.license === "original" ?
|
|
177
|
+
"GPL-3.0-or-later AND CC-BY-SA-4.0"
|
|
178
|
+
: "SEE LICENSE IN LICENSE.md",
|
|
179
|
+
type: "module",
|
|
180
|
+
repository: { type: "git", url: `https://github.com/HeroicLands/${answers.name}` },
|
|
181
|
+
homepage: `https://www.heroiclands.org/${answers.contentPackage}/`,
|
|
182
|
+
engines: { node: ">=24.0.0" },
|
|
183
|
+
scripts: projectScripts(answers),
|
|
184
|
+
devDependencies: {
|
|
185
|
+
"@changesets/cli": "^3.0.0",
|
|
186
|
+
"@heroiclands/hugo-theme": "^0.9.0",
|
|
187
|
+
"@heroiclands/package-build": `^${answers.version}`,
|
|
188
|
+
"npm-run-all": "^4.1.5",
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {object} answers */
|
|
194
|
+
function projectText(answers) {
|
|
195
|
+
const license =
|
|
196
|
+
answers.license === "original" ?
|
|
197
|
+
`# License\n\nCopyright © ${new Date().getFullYear()} ${answers.author}.\n\nCode is licensed under GPL-3.0-or-later. Original content is licensed under CC-BY-SA-4.0.\n`
|
|
198
|
+
: `# License\n\nCopyright © ${new Date().getFullYear()} ${answers.author} for original contributions.\n\nOriginal content is licensed under CC-BY-NC-SA-4.0. Third-party setting material retains its owners' rights. This is unofficial fan material.\n`;
|
|
199
|
+
const config = projectConfig(answers);
|
|
200
|
+
const homepage = [
|
|
201
|
+
"---",
|
|
202
|
+
YAML.stringify(
|
|
203
|
+
{
|
|
204
|
+
shortcode: "root",
|
|
205
|
+
name: { full: answers.title },
|
|
206
|
+
type: "homepage",
|
|
207
|
+
description: answers.description,
|
|
208
|
+
},
|
|
209
|
+
{ indent: 4 },
|
|
210
|
+
).trim(),
|
|
211
|
+
"---",
|
|
212
|
+
"",
|
|
213
|
+
`# ${answers.title}`,
|
|
214
|
+
"",
|
|
215
|
+
answers.description,
|
|
216
|
+
"",
|
|
217
|
+
"Start with [Introduction](doc-introduction/).",
|
|
218
|
+
"",
|
|
219
|
+
].join("\n");
|
|
220
|
+
const introduction = [
|
|
221
|
+
"---",
|
|
222
|
+
YAML.stringify(
|
|
223
|
+
{
|
|
224
|
+
shortcode: "introduction",
|
|
225
|
+
name: { full: "Introduction" },
|
|
226
|
+
type: "doc",
|
|
227
|
+
subType: "concept",
|
|
228
|
+
description: `An introduction to ${answers.title}.`,
|
|
229
|
+
},
|
|
230
|
+
{ indent: 4 },
|
|
231
|
+
).trim(),
|
|
232
|
+
"---",
|
|
233
|
+
"",
|
|
234
|
+
"# Introduction",
|
|
235
|
+
"",
|
|
236
|
+
`${answers.title} is ready for its first notes. This page introduces the package to readers.`,
|
|
237
|
+
"",
|
|
238
|
+
].join("\n");
|
|
239
|
+
const files = {
|
|
240
|
+
"package.json": JSON.stringify(projectManifest(answers)),
|
|
241
|
+
"package-build.config.yaml": YAML.stringify(config, { indent: 4 }),
|
|
242
|
+
".gitignore": [
|
|
243
|
+
"node_modules",
|
|
244
|
+
"/build/",
|
|
245
|
+
"/nogit/",
|
|
246
|
+
"/.env*",
|
|
247
|
+
"/.DS_Store",
|
|
248
|
+
"/.claude/",
|
|
249
|
+
"*.tgz",
|
|
250
|
+
"",
|
|
251
|
+
].join("\n"),
|
|
252
|
+
"prettier.config.js":
|
|
253
|
+
'/* SPDX-License-Identifier: GPL-3.0-or-later */\nexport { default } from "@heroiclands/package-build/prettier";\n',
|
|
254
|
+
".prettierignore": "CHANGELOG.md\n",
|
|
255
|
+
"README.md": `# ${answers.title}\n\n${answers.description}\n\nRun \`npm install\`, then \`npm run lint\`. Build the website with \`npm run build:site\`, serve it with \`npm run serve:site\`, and build the book with \`npm run build:book\`. The site requires Hugo Extended and the book requires Typst on your PATH. Site deployment needs its own hosting configuration.\n`,
|
|
256
|
+
"LICENSE.md": license,
|
|
257
|
+
"lang/en.json": JSON.stringify({
|
|
258
|
+
[answers.contentPackage.toUpperCase()]: { Title: answers.title },
|
|
259
|
+
}),
|
|
260
|
+
"assets/content/homepage.md": homepage,
|
|
261
|
+
"assets/content/Introduction.md": introduction,
|
|
262
|
+
"book.yaml": YAML.stringify({
|
|
263
|
+
contents: [
|
|
264
|
+
{
|
|
265
|
+
sectionName: "Introduction",
|
|
266
|
+
contents: [{ filter: "type = 'doc' AND shortcode = 'introduction'" }],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}),
|
|
270
|
+
".changeset/config.json": JSON.stringify({
|
|
271
|
+
$schema: "https://unpkg.com/@changesets/config@4.0.0/schema.json",
|
|
272
|
+
changelog: "@heroiclands/package-build/changelog",
|
|
273
|
+
commit: true,
|
|
274
|
+
baseBranch: "main",
|
|
275
|
+
privatePackages: { version: true, tag: false },
|
|
276
|
+
updateInternalDependencies: "patch",
|
|
277
|
+
ignore: [],
|
|
278
|
+
}),
|
|
279
|
+
".github/labels.yml": YAML.stringify([
|
|
280
|
+
{
|
|
281
|
+
name: "documentation",
|
|
282
|
+
color: "0075ca",
|
|
283
|
+
description: "Documentation and author guides.",
|
|
284
|
+
},
|
|
285
|
+
{ name: "devops", color: "e07b31", description: "Build, CI, and release work." },
|
|
286
|
+
]),
|
|
287
|
+
".github/ISSUE_REPORTING.md": [
|
|
288
|
+
"# Issue reporting",
|
|
289
|
+
"",
|
|
290
|
+
"## 1. Scope",
|
|
291
|
+
"",
|
|
292
|
+
"File a reproducible issue in the repository whose change can resolve it.",
|
|
293
|
+
"",
|
|
294
|
+
"## 2. Work shape",
|
|
295
|
+
"",
|
|
296
|
+
"Use the organization issue type for a bug, feature, task, spike, or epic.",
|
|
297
|
+
"",
|
|
298
|
+
"## 3. Labels — the closed registry",
|
|
299
|
+
"",
|
|
300
|
+
"| Label | Use it for |",
|
|
301
|
+
"| ----- | ---------- |",
|
|
302
|
+
"| `documentation` | Documentation and author guides. |",
|
|
303
|
+
"| `devops` | Build, CI, and release work. |",
|
|
304
|
+
"",
|
|
305
|
+
].join("\n"),
|
|
306
|
+
".github/workflows/build.yml": [
|
|
307
|
+
"# SPDX-License-Identifier: GPL-3.0-or-later",
|
|
308
|
+
"name: Build and Test",
|
|
309
|
+
"on:",
|
|
310
|
+
" pull_request:",
|
|
311
|
+
" branches: [main]",
|
|
312
|
+
" push:",
|
|
313
|
+
" branches: [main]",
|
|
314
|
+
"jobs:",
|
|
315
|
+
" test-build:",
|
|
316
|
+
" runs-on: ubuntu-latest",
|
|
317
|
+
" steps:",
|
|
318
|
+
" - uses: actions/checkout@v7",
|
|
319
|
+
" - uses: actions/setup-node@v7",
|
|
320
|
+
" with:",
|
|
321
|
+
" node-version: 24",
|
|
322
|
+
" cache: npm",
|
|
323
|
+
" - run: npm ci",
|
|
324
|
+
" - run: npm run build:noci",
|
|
325
|
+
"",
|
|
326
|
+
].join("\n"),
|
|
327
|
+
};
|
|
328
|
+
return files;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Format every generated file with the toolchain's own conventions. */
|
|
332
|
+
export async function renderProject(answers) {
|
|
333
|
+
const valid = validateInitAnswers(answers);
|
|
334
|
+
const files = projectText(valid);
|
|
335
|
+
const result = new Map();
|
|
336
|
+
for (const [file, source] of Object.entries(files)) {
|
|
337
|
+
if (file === ".gitignore" || file === ".prettierignore") {
|
|
338
|
+
result.set(file, source);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const formatted = await prettier.format(source, {
|
|
342
|
+
...prettierConfig,
|
|
343
|
+
filepath: file,
|
|
344
|
+
});
|
|
345
|
+
result.set(
|
|
346
|
+
file,
|
|
347
|
+
file.startsWith("assets/content/") ? formatNoteFrontmatter(formatted) : formatted,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Write a complete scaffold after collision checks; undo this run's files on failure. */
|
|
354
|
+
export async function initializeProject(rootDir, answers) {
|
|
355
|
+
const root = path.resolve(rootDir);
|
|
356
|
+
const files = await renderProject(answers);
|
|
357
|
+
const parent = path.dirname(root);
|
|
358
|
+
if (!fs.existsSync(parent)) throw new Error(`init: parent directory does not exist: ${parent}`);
|
|
359
|
+
const existed = fs.existsSync(root);
|
|
360
|
+
if (existed && !fs.statSync(root).isDirectory()) {
|
|
361
|
+
throw new Error(`init: target is not a directory: ${root}`);
|
|
362
|
+
}
|
|
363
|
+
if (existed && fs.lstatSync(root).isSymbolicLink()) {
|
|
364
|
+
throw new Error(`init: target must not be a symbolic link: ${root}`);
|
|
365
|
+
}
|
|
366
|
+
for (const name of CONFIG_FILENAMES) {
|
|
367
|
+
if (fs.existsSync(path.join(root, name))) {
|
|
368
|
+
throw new Error(`init: configuration already exists: ${path.join(root, name)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const file of files.keys()) {
|
|
372
|
+
const dest = path.join(root, file);
|
|
373
|
+
if (fs.existsSync(dest)) throw new Error(`init: refusing to overwrite ${dest}`);
|
|
374
|
+
}
|
|
375
|
+
const written = [];
|
|
376
|
+
const madeDirs = [];
|
|
377
|
+
try {
|
|
378
|
+
if (!existed) {
|
|
379
|
+
fs.mkdirSync(root);
|
|
380
|
+
madeDirs.push(root);
|
|
381
|
+
}
|
|
382
|
+
for (const [file, source] of files) {
|
|
383
|
+
const dest = path.join(root, file);
|
|
384
|
+
const dir = path.dirname(dest);
|
|
385
|
+
const missing = [];
|
|
386
|
+
for (let p = dir; p !== root && !fs.existsSync(p); p = path.dirname(p)) missing.push(p);
|
|
387
|
+
for (const p of missing.reverse()) {
|
|
388
|
+
fs.mkdirSync(p);
|
|
389
|
+
madeDirs.push(p);
|
|
390
|
+
}
|
|
391
|
+
fs.writeFileSync(dest, source, { flag: "wx" });
|
|
392
|
+
written.push(dest);
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
for (const file of written.reverse()) fs.rmSync(file, { force: true });
|
|
396
|
+
for (const dir of madeDirs.reverse()) {
|
|
397
|
+
try {
|
|
398
|
+
fs.rmdirSync(dir);
|
|
399
|
+
} catch {
|
|
400
|
+
// An existing file in the directory remains untouched.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
return [...files.keys()];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Report required structure and meaning in an existing package without writes. */
|
|
409
|
+
export function checkProject(rootDir) {
|
|
410
|
+
const root = path.resolve(rootDir);
|
|
411
|
+
const findings = [];
|
|
412
|
+
const add = (file, message) => findings.push({ file, message });
|
|
413
|
+
const required = [
|
|
414
|
+
"package.json",
|
|
415
|
+
".gitignore",
|
|
416
|
+
"prettier.config.js",
|
|
417
|
+
".prettierignore",
|
|
418
|
+
"README.md",
|
|
419
|
+
"LICENSE.md",
|
|
420
|
+
".changeset/config.json",
|
|
421
|
+
".github/labels.yml",
|
|
422
|
+
".github/ISSUE_REPORTING.md",
|
|
423
|
+
".github/workflows/build.yml",
|
|
424
|
+
];
|
|
425
|
+
for (const file of required) {
|
|
426
|
+
if (!fs.existsSync(path.join(root, file))) add(file, "required project file is missing");
|
|
427
|
+
}
|
|
428
|
+
const names = CONFIG_FILENAMES.filter((name) => fs.existsSync(path.join(root, name)));
|
|
429
|
+
if (names.length !== 1) {
|
|
430
|
+
add("package-build.config.yaml", "exactly one package-build configuration is required");
|
|
431
|
+
}
|
|
432
|
+
let manifest;
|
|
433
|
+
try {
|
|
434
|
+
manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
435
|
+
for (const name of REQUIRED_SCRIPTS) {
|
|
436
|
+
if (!nonempty(manifest.scripts?.[name])) {
|
|
437
|
+
add("package.json", `scripts.${name} is required`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!manifest.devDependencies?.["@heroiclands/package-build"]) {
|
|
441
|
+
add("package.json", "the package-build development dependency is required");
|
|
442
|
+
}
|
|
443
|
+
if (!manifest.devDependencies?.["@heroiclands/hugo-theme"]) {
|
|
444
|
+
add("package.json", "the Hugo theme development dependency is required");
|
|
445
|
+
}
|
|
446
|
+
if (!nonempty(manifest.homepage))
|
|
447
|
+
add("package.json", "homepage is required for the website");
|
|
448
|
+
} catch (err) {
|
|
449
|
+
if (fs.existsSync(path.join(root, "package.json"))) {
|
|
450
|
+
add("package.json", `cannot read package metadata: ${err.message}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
let config;
|
|
454
|
+
if (names.length === 1 && names[0] !== "package-build.config.mjs") {
|
|
455
|
+
const file = names[0];
|
|
456
|
+
try {
|
|
457
|
+
const source = fs.readFileSync(path.join(root, file), "utf8");
|
|
458
|
+
config = YAML.parse(source);
|
|
459
|
+
configFromData(config, path.join(root, file));
|
|
460
|
+
} catch (err) {
|
|
461
|
+
add(file, `invalid package configuration: ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (config) {
|
|
465
|
+
if (!nonempty(config.site?.assets) || !nonempty(config.site?.description)) {
|
|
466
|
+
add(names[0], "site.assets and site.description are required for the website");
|
|
467
|
+
}
|
|
468
|
+
if (!nonempty(config.pdf?.title) || !nonempty(config.pdf?.document)) {
|
|
469
|
+
add(names[0], "pdf.title and pdf.document are required for the book");
|
|
470
|
+
} else if (!fs.existsSync(path.join(root, config.pdf.document))) {
|
|
471
|
+
add(config.pdf.document, "the book document tree is missing");
|
|
472
|
+
}
|
|
473
|
+
if (config.packageKind !== "documentation") {
|
|
474
|
+
if (!nonempty(config.packageBuild?.manifest?.title)) {
|
|
475
|
+
add(names[0], "packageBuild.manifest.title is required");
|
|
476
|
+
}
|
|
477
|
+
if (!fs.existsSync(path.join(root, "lang/en.json"))) {
|
|
478
|
+
add("lang/en.json", "the localization file is missing");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const contentRoot = path.join(root, "assets/content");
|
|
483
|
+
if (!fs.existsSync(contentRoot)) {
|
|
484
|
+
add("assets/content", "the content tree is missing");
|
|
485
|
+
} else if (!fs.readdirSync(contentRoot).some((file) => file.endsWith(".md"))) {
|
|
486
|
+
add("assets/content", "the content tree has no notes");
|
|
487
|
+
}
|
|
488
|
+
const labelsPath = path.join(root, ".github/labels.yml");
|
|
489
|
+
const reportingPath = path.join(root, ".github/ISSUE_REPORTING.md");
|
|
490
|
+
if (fs.existsSync(labelsPath) && fs.existsSync(reportingPath)) {
|
|
491
|
+
const result = checkLabelRegistry({
|
|
492
|
+
registryText: fs.readFileSync(labelsPath, "utf8"),
|
|
493
|
+
docText: fs.readFileSync(reportingPath, "utf8"),
|
|
494
|
+
});
|
|
495
|
+
for (const finding of result.registry) add(".github/labels.yml", finding.message);
|
|
496
|
+
for (const finding of result.doc) add(".github/ISSUE_REPORTING.md", finding.message);
|
|
497
|
+
}
|
|
498
|
+
return findings;
|
|
499
|
+
}
|
package/engine/prose-config.mjs
CHANGED
|
@@ -269,7 +269,7 @@ export const MARKDOWNLINT_CONFIG = Object.freeze({
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* The globs `
|
|
272
|
+
* The globs `package-build markdown` checks when a consumer names no paths.
|
|
273
273
|
*
|
|
274
274
|
* Every markdown file the repository tracks, which is what a consumer means by
|
|
275
275
|
* "lint my markdown". What to *skip* is the repository's own business and comes
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* it formats and what an ignore file excludes, through `getFileInfo`;
|
|
26
26
|
* markdownlint expands its own globs and honours `.gitignore` itself. A second
|
|
27
27
|
* implementation of either would drift from the tool it stands in for, and the
|
|
28
|
-
* whole point of the command is that `
|
|
28
|
+
* whole point of the command is that `package-build format --check` and a bare
|
|
29
29
|
* `prettier --check .` report the same thing.
|
|
30
30
|
*
|
|
31
31
|
* @module
|
|
@@ -244,7 +244,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
244
244
|
// No line or column: Prettier's answer is about the whole
|
|
245
245
|
// file, and the rule is to drop a field rather than
|
|
246
246
|
// invent one.
|
|
247
|
-
message: "is not formatted; run `
|
|
247
|
+
message: "is not formatted; run `package-build format --write` to fix it",
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
} catch (err) {
|
|
@@ -300,7 +300,7 @@ function divergenceMessage({ key, shared, local }, scope = "") {
|
|
|
300
300
|
*
|
|
301
301
|
* The no-configuration case is the sharper one and is reported even though the
|
|
302
302
|
* command itself behaves correctly there: with no config file the shared
|
|
303
|
-
* conventions reach `
|
|
303
|
+
* conventions reach `package-build format` and reach *nothing else*, so an
|
|
304
304
|
* editor's format-on-save and a bare `npx prettier --check .` apply Prettier's
|
|
305
305
|
* own defaults to the same tree, and the two take turns rewriting the same
|
|
306
306
|
* lines. That is not hypothetical — it is what the config files in
|
|
@@ -327,7 +327,7 @@ export async function checkPrettierConventions(root, opts = {}) {
|
|
|
327
327
|
{
|
|
328
328
|
severity: "warning",
|
|
329
329
|
message:
|
|
330
|
-
"this repository declares no Prettier configuration, so `
|
|
330
|
+
"this repository declares no Prettier configuration, so `package-build " +
|
|
331
331
|
"format` applies the shared conventions while an editor and a bare `npx " +
|
|
332
332
|
"prettier` apply Prettier's own to the same tree; declare them in a " +
|
|
333
333
|
`prettier.config.mjs — ${SHARED_CONFIG_RE_EXPORT}`,
|
|
@@ -419,7 +419,8 @@ function toDiagnostic(directory, result) {
|
|
|
419
419
|
* and every per-rule option; but its `ignores` *replaces*
|
|
420
420
|
* {@link MARKDOWN_IGNORES} rather than extending it, so such a file must restate
|
|
421
421
|
* every shared entry it still wants. Omitting `CHANGELOG.md` there silently
|
|
422
|
-
* starts linting a generated file.
|
|
422
|
+
* starts linting a generated file. `node_modules` stays excluded by the
|
|
423
|
+
* command's glob even when a consumer replaces `ignores`.
|
|
423
424
|
*
|
|
424
425
|
* @param {string} root - Repository to lint.
|
|
425
426
|
* @param {object} [opts]
|
|
@@ -445,7 +446,7 @@ export async function lintMarkdown(root, opts = {}) {
|
|
|
445
446
|
collected.push(...results);
|
|
446
447
|
};
|
|
447
448
|
|
|
448
|
-
const argv = [...(paths?.length ? paths : MARKDOWN_GLOBS)];
|
|
449
|
+
const argv = [...(paths?.length ? paths : MARKDOWN_GLOBS), "!**/node_modules/**"];
|
|
449
450
|
if (fix) argv.push("--fix");
|
|
450
451
|
|
|
451
452
|
const exitCode = await run({
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Optional Markdown prose analysis with source positions preserved. */
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { unified } from "unified";
|
|
9
|
+
import remarkParse from "remark-parse";
|
|
10
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
11
|
+
import remarkRetext from "remark-retext";
|
|
12
|
+
import retextEnglish from "retext-english";
|
|
13
|
+
import retextReadability from "retext-readability";
|
|
14
|
+
import retextSimplify from "retext-simplify";
|
|
15
|
+
import retextStringify from "retext-stringify";
|
|
16
|
+
|
|
17
|
+
/** Files in a content tree, excluding configured directory names. */
|
|
18
|
+
export function proseFiles(root, skipDirectories = []) {
|
|
19
|
+
if (fs.statSync(root).isFile()) return [root];
|
|
20
|
+
const files = [];
|
|
21
|
+
const walk = (dir) => {
|
|
22
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const file = path.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory() && !skipDirectories.includes(entry.name)) walk(file);
|
|
25
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) files.push(file);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(root);
|
|
29
|
+
return files.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keep source offsets while excluding HeroicLands inline addresses and expressions. */
|
|
33
|
+
function maskMarkup(source) {
|
|
34
|
+
return source.replace(/!?\[\[[^\]\n]+\]\]|\{\{[^}\n]+\}\}/g, (match) =>
|
|
35
|
+
match.replace(/[^\n]/g, " "),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Source-aligned findings for one Markdown note. */
|
|
40
|
+
export async function analyzeProse(file, source, { age = 21, threshold = 5, minWords = 8 } = {}) {
|
|
41
|
+
const text = maskMarkup(source);
|
|
42
|
+
const virtualFile = { value: text, path: file, messages: [] };
|
|
43
|
+
const processor = unified()
|
|
44
|
+
.use(remarkParse)
|
|
45
|
+
.use(remarkFrontmatter)
|
|
46
|
+
.use(
|
|
47
|
+
remarkRetext,
|
|
48
|
+
unified()
|
|
49
|
+
.use(retextEnglish)
|
|
50
|
+
.use(retextReadability, { age, threshold: threshold / 7, minWords })
|
|
51
|
+
.use(retextSimplify)
|
|
52
|
+
.use(retextStringify),
|
|
53
|
+
);
|
|
54
|
+
await processor.run(processor.parse(virtualFile), virtualFile);
|
|
55
|
+
return virtualFile.messages.map((message) => {
|
|
56
|
+
const position = message.place?.start;
|
|
57
|
+
const sentence = message.ancestors?.find((node) => node.type === "SentenceNode");
|
|
58
|
+
const span = sentence?.position;
|
|
59
|
+
const offending =
|
|
60
|
+
span?.start?.offset !== undefined && span?.end?.offset !== undefined ?
|
|
61
|
+
source.slice(span.start.offset, span.end.offset)
|
|
62
|
+
: (message.actual ?? "");
|
|
63
|
+
const match = /according to (\d+) out of 7 algorithms/.exec(message.reason);
|
|
64
|
+
const confidence =
|
|
65
|
+
match ? `${match[1]}/7`
|
|
66
|
+
: message.source === "retext-readability" && /all 7 algorithms/.test(message.reason) ?
|
|
67
|
+
"7/7"
|
|
68
|
+
: "unscored";
|
|
69
|
+
return {
|
|
70
|
+
file,
|
|
71
|
+
line: position?.line,
|
|
72
|
+
column: position?.column,
|
|
73
|
+
severity: "warning",
|
|
74
|
+
message: `${message.source}/${message.ruleId}: confidence=${confidence}; sentence=${JSON.stringify(offending.trim())}; expected=${JSON.stringify(message.expected ?? [])}; ${message.reason}`,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Analyze one note or the configured corpus without changing source files. */
|
|
80
|
+
export async function lintProse(root, options, skipDirectories = []) {
|
|
81
|
+
const findings = [];
|
|
82
|
+
for (const file of proseFiles(root, skipDirectories)) {
|
|
83
|
+
findings.push(...(await analyzeProse(file, fs.readFileSync(file, "utf8"), options)));
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
}
|