@heroiclands/package-build 23.5.0 → 23.6.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 +33 -0
- package/bin/content-commands.mjs +23 -5
- package/bin/package-build.mjs +1 -74
- package/docs/api.md +9 -0
- package/docs/authoring/links-and-markup.md +77 -12
- package/docs/commands.md +4 -48
- package/docs/content-format.md +16 -0
- package/docs/diagnostics.md +1 -2
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/reference-pages.md +89 -0
- package/docs/guides/release-and-deploy.md +3 -3
- package/docs/project-setup.md +16 -59
- package/docs/reference/format-details.md +533 -84
- package/docs/reference/note-types.md +191 -149
- package/engine/base-compiler.mjs +21 -0
- package/engine/cdn-publish.mjs +30 -5
- package/engine/content-format.yaml +9 -0
- package/engine/content-index.mjs +19 -5
- package/engine/culture-reach.mjs +330 -0
- package/engine/dictionary-layout.mjs +340 -0
- package/engine/dictionary-rows.mjs +608 -0
- package/engine/glossary-entries.mjs +182 -0
- package/engine/glossary-language.mjs +99 -0
- package/engine/index.mjs +1 -0
- package/engine/infobox.mjs +8 -0
- package/engine/language-words.mjs +360 -0
- package/engine/literature-notes.mjs +10 -8
- package/engine/note-addresses.mjs +25 -0
- package/engine/note-vocabulary.mjs +161 -1
- package/engine/path-charset.mjs +120 -0
- package/engine/project-init.mjs +4 -29
- package/engine/sql-tables.mjs +311 -21
- package/engine/word-classes.mjs +167 -0
- package/engine/word-rows.mjs +233 -0
- package/engine/word-terms.mjs +306 -0
- package/package.json +2 -4
- package/types/engine/cdn-publish.d.mts +26 -25
- package/types/engine/content-index.d.mts +15 -5
- package/types/engine/culture-reach.d.mts +66 -0
- package/types/engine/dictionary-layout.d.mts +69 -0
- package/types/engine/dictionary-rows.d.mts +39 -0
- package/types/engine/glossary-entries.d.mts +12 -0
- package/types/engine/glossary-language.d.mts +42 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/language-words.d.mts +33 -0
- package/types/engine/literature-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +17 -0
- package/types/engine/path-charset.d.mts +38 -0
- package/types/engine/sql-tables.d.mts +7 -2
- package/types/engine/word-classes.d.mts +87 -0
- package/types/engine/word-rows.d.mts +64 -0
- package/types/engine/word-terms.d.mts +68 -0
- package/labels.mjs +0 -191
- package/types/labels.d.mts +0 -40
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* `data.culture`, is a field every lore note but a culture note may state,
|
|
18
18
|
* declared in `culture-choice.mjs`.
|
|
19
19
|
*
|
|
20
|
-
* `data.language` is shared
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* `data.language` is shared beyond literature: a work names the tongue it is
|
|
21
|
+
* composed in, a culture the tongue its people speak, whose name lists
|
|
22
|
+
* `package-build npc new` draws from, and an affiliation the trade, temple or
|
|
23
|
+
* order tongue it keeps.
|
|
23
24
|
*
|
|
24
25
|
* `DataFieldSpec` declares the keys a type accepts, not the subType that may
|
|
25
26
|
* write them, so {@link checkLiteratureNote} scopes these keys to the subTypes
|
|
@@ -74,8 +75,8 @@ export function checkLanguageChoice(note, { index } = {}) {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* `data.language`: the tongue a work is composed in,
|
|
78
|
-
* people speak. Optional on
|
|
78
|
+
* `data.language`: the tongue a work is composed in, the tongue a culture's
|
|
79
|
+
* people speak, or the tongue an affiliation keeps. Optional on all three.
|
|
79
80
|
*
|
|
80
81
|
* @type {object}
|
|
81
82
|
*/
|
|
@@ -87,9 +88,10 @@ export const LANGUAGE_FIELD = Object.freeze({
|
|
|
87
88
|
accepts: Object.freeze(["skill"]),
|
|
88
89
|
check: checkLanguageChoice,
|
|
89
90
|
describe:
|
|
90
|
-
"The tongue the
|
|
91
|
-
"
|
|
92
|
-
"
|
|
91
|
+
"The tongue the note is in, as a language skill note: the tongue a work is composed " +
|
|
92
|
+
"in, a culture's people speak, or an affiliation keeps as its trade, temple or order " +
|
|
93
|
+
"tongue. `package-build npc new` draws a culture's names from its language's " +
|
|
94
|
+
"`data.names`. A `literature` or `culture` lore note, or an affiliation, only.",
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
/**
|
|
@@ -69,6 +69,31 @@ export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
|
69
69
|
type: "place",
|
|
70
70
|
accepts: ["place"],
|
|
71
71
|
},
|
|
72
|
+
// A language's word Addresses and a glossary entry's language, read
|
|
73
|
+
// leniently: a value that is no Address stays as written, for the words or
|
|
74
|
+
// glossary check to refuse at its own key.
|
|
75
|
+
{
|
|
76
|
+
noteType: "skill",
|
|
77
|
+
path: ["data", "words", "*", "attested"],
|
|
78
|
+
shape: "list",
|
|
79
|
+
lenient: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
noteType: "skill",
|
|
83
|
+
path: ["data", "words", "*", "source", "language"],
|
|
84
|
+
shape: "value",
|
|
85
|
+
type: "skill",
|
|
86
|
+
accepts: ["skill"],
|
|
87
|
+
lenient: true,
|
|
88
|
+
},
|
|
89
|
+
...["lore", "affiliation"].map((noteType) => ({
|
|
90
|
+
noteType,
|
|
91
|
+
path: ["data", "glossary", "*", "language"],
|
|
92
|
+
shape: "value",
|
|
93
|
+
type: "skill",
|
|
94
|
+
accepts: ["skill"],
|
|
95
|
+
lenient: true,
|
|
96
|
+
})),
|
|
72
97
|
]);
|
|
73
98
|
|
|
74
99
|
/**
|
|
@@ -119,6 +119,20 @@ import {
|
|
|
119
119
|
} from "./appearance-profile.mjs";
|
|
120
120
|
import { checkExtraFeatures } from "./appearance-features.mjs";
|
|
121
121
|
import { LANGUAGE_FIELD, LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
122
|
+
import {
|
|
123
|
+
AFFIX_FIELDS,
|
|
124
|
+
GLOSSARY_ENTRY_FIELDS,
|
|
125
|
+
WORD_FIELDS,
|
|
126
|
+
WORD_FIELD_FIELDS,
|
|
127
|
+
} from "./word-terms.mjs";
|
|
128
|
+
import { WORD_CLASS_MODIFIERS } from "./word-classes.mjs";
|
|
129
|
+
import {
|
|
130
|
+
checkLanguageAbbreviation,
|
|
131
|
+
checkLanguageFamily,
|
|
132
|
+
checkLanguageWords,
|
|
133
|
+
} from "./language-words.mjs";
|
|
134
|
+
import { checkGlossary } from "./glossary-entries.mjs";
|
|
135
|
+
import { checkAffiliationReach } from "./culture-reach.mjs";
|
|
122
136
|
import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
|
|
123
137
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
124
138
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
@@ -540,6 +554,126 @@ const APPEARANCE_PROFILE_FIELD = Object.freeze({
|
|
|
540
554
|
]),
|
|
541
555
|
});
|
|
542
556
|
|
|
557
|
+
/**
|
|
558
|
+
* A language's words and what they are read against: the class modifiers it
|
|
559
|
+
* uses, the fields its words are grouped under, and the affixes a word's
|
|
560
|
+
* pieces may name. A language skill note only; {@link checkLanguageWords}
|
|
561
|
+
* checks what the values mean.
|
|
562
|
+
*
|
|
563
|
+
* @type {ReadonlyArray<DataFieldSpec>}
|
|
564
|
+
*/
|
|
565
|
+
const WORD_DATA_FIELDS = Object.freeze([
|
|
566
|
+
Object.freeze({
|
|
567
|
+
name: "classModifiers",
|
|
568
|
+
kind: "list",
|
|
569
|
+
shape: "list of word class modifiers",
|
|
570
|
+
entries: Object.freeze({
|
|
571
|
+
kind: "string",
|
|
572
|
+
allowed: Object.freeze(WORD_CLASS_MODIFIERS.map((item) => item.token)),
|
|
573
|
+
shape: "a word class modifier",
|
|
574
|
+
}),
|
|
575
|
+
describe:
|
|
576
|
+
"The class modifiers the language uses, from the master word classes. A word's " +
|
|
577
|
+
"class may carry only these. Empty when unstated. A language skill note only.",
|
|
578
|
+
}),
|
|
579
|
+
Object.freeze({
|
|
580
|
+
name: "fields",
|
|
581
|
+
kind: "list",
|
|
582
|
+
shape: "list of `{ name, holds }`",
|
|
583
|
+
entries: Object.freeze({
|
|
584
|
+
kind: "map",
|
|
585
|
+
shape: "a map — `{ name, holds }`",
|
|
586
|
+
fields: WORD_FIELD_FIELDS,
|
|
587
|
+
}),
|
|
588
|
+
describe: "The fields the language's words are grouped under. A language skill note only.",
|
|
589
|
+
}),
|
|
590
|
+
Object.freeze({
|
|
591
|
+
name: "words",
|
|
592
|
+
kind: "list",
|
|
593
|
+
shape: "list of `{ form, class, field, gloss, … }`",
|
|
594
|
+
entries: Object.freeze({
|
|
595
|
+
kind: "map",
|
|
596
|
+
shape: "a map — `{ form, class, field, gloss, … }`",
|
|
597
|
+
fields: WORD_FIELDS,
|
|
598
|
+
}),
|
|
599
|
+
describe:
|
|
600
|
+
"The language's words, one map each, in the order they print. A language skill " +
|
|
601
|
+
"note only. See [a language's words](format-details.md#a-languages-words).",
|
|
602
|
+
}),
|
|
603
|
+
Object.freeze({
|
|
604
|
+
name: "affixes",
|
|
605
|
+
kind: "list",
|
|
606
|
+
shape: "list of `{ form, gloss, attaches?, examples? }`",
|
|
607
|
+
entries: Object.freeze({
|
|
608
|
+
kind: "map",
|
|
609
|
+
shape: "a map — `{ form, gloss, attaches?, examples? }`",
|
|
610
|
+
fields: AFFIX_FIELDS,
|
|
611
|
+
}),
|
|
612
|
+
describe:
|
|
613
|
+
"The affixes a word's `built` pieces may name, each with its gloss. A language " +
|
|
614
|
+
"skill note only.",
|
|
615
|
+
}),
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* A language's `data.family`: the parent language whose words it reads as its
|
|
620
|
+
* own. {@link checkLanguageFamily} checks it.
|
|
621
|
+
*
|
|
622
|
+
* @type {DataFieldSpec}
|
|
623
|
+
*/
|
|
624
|
+
const FAMILY_FIELD = Object.freeze({
|
|
625
|
+
name: "family",
|
|
626
|
+
kind: "address",
|
|
627
|
+
shape: "an Address",
|
|
628
|
+
ref: "skill",
|
|
629
|
+
accepts: Object.freeze(["skill"]),
|
|
630
|
+
check: checkLanguageFamily,
|
|
631
|
+
describe:
|
|
632
|
+
"The parent language, as a language skill note: this language reads the parent's " +
|
|
633
|
+
"words, and its parents' in turn, as its own. A language skill note only. See " +
|
|
634
|
+
"[a language's words](format-details.md#a-languages-words).",
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* A language's `data.abbreviation`: the tag a dictionary prints beside an
|
|
639
|
+
* entry's class. {@link checkLanguageAbbreviation} checks it.
|
|
640
|
+
*
|
|
641
|
+
* @type {DataFieldSpec}
|
|
642
|
+
*/
|
|
643
|
+
const ABBREVIATION_FIELD = Object.freeze({
|
|
644
|
+
name: "abbreviation",
|
|
645
|
+
kind: "string",
|
|
646
|
+
shape: "a string, such as `Prov.`",
|
|
647
|
+
check: checkLanguageAbbreviation,
|
|
648
|
+
describe:
|
|
649
|
+
"The tag a dictionary prints for the language, beside an entry's class and in an " +
|
|
650
|
+
"etymology, as `Prov.`; the language's full name where unstated. A language skill " +
|
|
651
|
+
"note only. See [a language's words](format-details.md#a-languages-words).",
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* A culture's or an affiliation's `data.glossary`: the terms that belong to
|
|
656
|
+
* its people or members rather than to a language's grammar.
|
|
657
|
+
* {@link checkGlossary} checks what an entry means.
|
|
658
|
+
*
|
|
659
|
+
* @type {DataFieldSpec}
|
|
660
|
+
*/
|
|
661
|
+
const GLOSSARY_FIELD = Object.freeze({
|
|
662
|
+
name: "glossary",
|
|
663
|
+
kind: "list",
|
|
664
|
+
shape: "list of `{ name, name_sort?, common?, definition?, pronunciation?, language? }`",
|
|
665
|
+
entries: Object.freeze({
|
|
666
|
+
kind: "map",
|
|
667
|
+
shape: "a map — `{ name, name_sort?, common?, definition?, pronunciation?, language? }`",
|
|
668
|
+
fields: GLOSSARY_ENTRY_FIELDS,
|
|
669
|
+
}),
|
|
670
|
+
describe:
|
|
671
|
+
"Its own glossary terms: euphemisms, idioms, terms of art and titles that are not " +
|
|
672
|
+
"ranks. An entry with a `definition` creates a term or replaces its senses; one " +
|
|
673
|
+
"without marks a term the note already has. Every entry resolves to a language. " +
|
|
674
|
+
"Empty when unstated. See [glossary terms](format-details.md#glossary-terms).",
|
|
675
|
+
});
|
|
676
|
+
|
|
543
677
|
/**
|
|
544
678
|
* A language's `data.names`: the pattern its speakers' names are built by and
|
|
545
679
|
* the lists each part is drawn from. Its keys beyond `pattern` and `given` are
|
|
@@ -1527,7 +1661,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1527
1661
|
|
|
1528
1662
|
affiliation: Object.freeze({
|
|
1529
1663
|
stubbable: true,
|
|
1530
|
-
|
|
1664
|
+
// A body's rank floor, whether it reaches a culture at all — a body
|
|
1665
|
+
// reaching none puts its ranks, offices and language in no culture's
|
|
1666
|
+
// glossary — and its own glossary.
|
|
1667
|
+
check: (note, context) => [
|
|
1668
|
+
...checkAffiliationRankFloor(note, context),
|
|
1669
|
+
...checkAffiliationReach(note, context),
|
|
1670
|
+
...checkGlossary(note, context),
|
|
1671
|
+
],
|
|
1531
1672
|
subTypes: Object.freeze([
|
|
1532
1673
|
"guild",
|
|
1533
1674
|
"order",
|
|
@@ -1645,6 +1786,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1645
1786
|
...CULTURE_FIELD,
|
|
1646
1787
|
describe: "The people this affiliation belongs to, as a culture lore note.",
|
|
1647
1788
|
},
|
|
1789
|
+
{
|
|
1790
|
+
...LANGUAGE_FIELD,
|
|
1791
|
+
describe:
|
|
1792
|
+
"The trade, temple or order tongue the affiliation keeps, as a language " +
|
|
1793
|
+
"skill note. It reaches the affiliation's culture, and its words enter that " +
|
|
1794
|
+
"culture's glossary. A glossary entry that states no `language` is in it.",
|
|
1795
|
+
},
|
|
1796
|
+
GLOSSARY_FIELD,
|
|
1648
1797
|
{
|
|
1649
1798
|
name: "relations",
|
|
1650
1799
|
kind: "map",
|
|
@@ -1839,6 +1988,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1839
1988
|
|
|
1840
1989
|
skill: Object.freeze({
|
|
1841
1990
|
stubbable: true,
|
|
1991
|
+
// A language note's words, held to the master word classes and to what
|
|
1992
|
+
// the note declares; no other skill states them.
|
|
1993
|
+
check: checkLanguageWords,
|
|
1842
1994
|
subTypes: Object.freeze([
|
|
1843
1995
|
"social",
|
|
1844
1996
|
"nature",
|
|
@@ -1862,6 +2014,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1862
2014
|
describe: "The skill this one specialises.",
|
|
1863
2015
|
},
|
|
1864
2016
|
NAMES_FIELD,
|
|
2017
|
+
FAMILY_FIELD,
|
|
2018
|
+
ABBREVIATION_FIELD,
|
|
2019
|
+
...WORD_DATA_FIELDS,
|
|
1865
2020
|
]),
|
|
1866
2021
|
}),
|
|
1867
2022
|
|
|
@@ -2045,6 +2200,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2045
2200
|
...checkCalendarNote(note, context),
|
|
2046
2201
|
...checkLiteratureNote(note),
|
|
2047
2202
|
...checkCultureSubType(note, context),
|
|
2203
|
+
...checkGlossary(note, context),
|
|
2048
2204
|
],
|
|
2049
2205
|
data: Object.freeze([
|
|
2050
2206
|
...CALENDAR_FIELDS,
|
|
@@ -2068,6 +2224,10 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2068
2224
|
LANGUAGE_FIELD,
|
|
2069
2225
|
EVENTS_FIELD,
|
|
2070
2226
|
APPEARANCE_PROFILE_FIELD,
|
|
2227
|
+
{
|
|
2228
|
+
...GLOSSARY_FIELD,
|
|
2229
|
+
describe: `\`culture\` only: ${GLOSSARY_FIELD.describe}`,
|
|
2230
|
+
},
|
|
2071
2231
|
]),
|
|
2072
2232
|
}),
|
|
2073
2233
|
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The characters a content or picture path may hold.
|
|
16
|
+
*
|
|
17
|
+
* Every character of a path is printable 7-bit ASCII, U+0020 to U+007E. A note's
|
|
18
|
+
* identity comes from its `shortcode` and a picture's from its basename, so a
|
|
19
|
+
* path's spelling changes no address; what a non-ASCII path does change is how
|
|
20
|
+
* the file travels. Filesystems disagree on Unicode normalisation, shells and
|
|
21
|
+
* `git ls-files` quote the bytes, and archive tools and CDN keys each encode
|
|
22
|
+
* the name their own way.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
|
|
30
|
+
import { DEPLOYED_ASSET_TYPES } from "./asset-types.mjs";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The distinct characters of a string outside U+0020 to U+007E, in order of
|
|
34
|
+
* first appearance.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text - A path, or one component of one.
|
|
37
|
+
* @returns {string[]} The offending characters; empty when the text is clean.
|
|
38
|
+
*/
|
|
39
|
+
export function offendingPathCharacters(text) {
|
|
40
|
+
const found = [];
|
|
41
|
+
for (const ch of text) {
|
|
42
|
+
const point = ch.codePointAt(0) ?? 0;
|
|
43
|
+
if ((point < 0x20 || point > 0x7e) && !found.includes(ch)) found.push(ch);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The message for a path holding characters outside printable ASCII.
|
|
50
|
+
*
|
|
51
|
+
* @param {string[]} characters - The offending characters.
|
|
52
|
+
* @returns {string} The finding's message.
|
|
53
|
+
*/
|
|
54
|
+
function pathMessage(characters) {
|
|
55
|
+
const named = characters.map((ch) => {
|
|
56
|
+
const point = ch.codePointAt(0) ?? 0;
|
|
57
|
+
const code = `U+${point.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
58
|
+
return point < 0x20 || point === 0x7f ? code : `${code} (${ch})`;
|
|
59
|
+
});
|
|
60
|
+
return (
|
|
61
|
+
`the path holds ${named.join(", ")}, outside printable ASCII; ` +
|
|
62
|
+
"rename the file using ASCII only"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Walk the content tree and the package's picture roots, reporting each entry
|
|
68
|
+
* whose name holds a character outside printable ASCII.
|
|
69
|
+
*
|
|
70
|
+
* An entry is checked by its own name as the walk reaches it, so a directory
|
|
71
|
+
* with a bad name is reported once and not again for every file beneath it.
|
|
72
|
+
* Dot-entries and `skipDirectories` are skipped, as in the other content walks.
|
|
73
|
+
* A finding names its file relative to the working directory, with no line or
|
|
74
|
+
* column.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} opts - Options.
|
|
77
|
+
* @param {string} opts.contentBase - Root of the content tree.
|
|
78
|
+
* @param {string} opts.assetsBase - The package's asset directory, whose
|
|
79
|
+
* picture roots are walked.
|
|
80
|
+
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
81
|
+
* @returns {{findings: Array<{file: string, severity: "error", message: string}>,
|
|
82
|
+
* files: number}} The findings, and how many paths were checked.
|
|
83
|
+
*/
|
|
84
|
+
export function lintPathCharset({ contentBase, assetsBase, skipDirectories = [] }) {
|
|
85
|
+
const skip = new Set(skipDirectories);
|
|
86
|
+
const findings = [];
|
|
87
|
+
let files = 0;
|
|
88
|
+
|
|
89
|
+
/** @param {string} dir - Directory to descend into. */
|
|
90
|
+
const walk = (dir) => {
|
|
91
|
+
/** @type {import("node:fs").Dirent[]} */
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.name.startsWith(".") || skip.has(entry.name)) continue;
|
|
100
|
+
const full = path.join(dir, entry.name);
|
|
101
|
+
files += 1;
|
|
102
|
+
const bad = offendingPathCharacters(entry.name);
|
|
103
|
+
if (bad.length) {
|
|
104
|
+
findings.push({
|
|
105
|
+
file: path.relative(process.cwd(), full),
|
|
106
|
+
severity: /** @type {const} */ ("error"),
|
|
107
|
+
message: pathMessage(bad),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (entry.isDirectory()) walk(full);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const roots = new Set([
|
|
115
|
+
path.resolve(contentBase),
|
|
116
|
+
...DEPLOYED_ASSET_TYPES.map((entry) => path.resolve(assetsBase, entry.root)),
|
|
117
|
+
]);
|
|
118
|
+
for (const root of roots) walk(root);
|
|
119
|
+
return { findings, files };
|
|
120
|
+
}
|
package/engine/project-init.mjs
CHANGED
|
@@ -20,7 +20,6 @@ import prettier from "prettier";
|
|
|
20
20
|
import prettierConfig from "../prettier-config.mjs";
|
|
21
21
|
import { CONFIG_FILENAMES, configFromData } from "./pack-config.mjs";
|
|
22
22
|
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
23
|
-
import { checkLabelRegistry } from "../labels.mjs";
|
|
24
23
|
import { formatNoteFrontmatter } from "./note-format.mjs";
|
|
25
24
|
|
|
26
25
|
/** Package kinds the initializer can create. */
|
|
@@ -94,13 +93,12 @@ function projectScripts(answers) {
|
|
|
94
93
|
"serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
|
|
95
94
|
"serve:site-html": "hugo server --source build/hugo",
|
|
96
95
|
"build:book": "package-build pdf",
|
|
97
|
-
lint: "run-s lint:format lint:markdown lint:addresses lint:content-links
|
|
96
|
+
lint: "run-s lint:format lint:markdown lint:addresses lint:content-links",
|
|
98
97
|
"lint:format": "package-build format",
|
|
99
98
|
"lint:markdown": "package-build markdown",
|
|
100
99
|
"lint:markdown:fix": "package-build markdown --fix",
|
|
101
100
|
"lint:addresses": "package-build lint",
|
|
102
101
|
"lint:content-links": "package-build links",
|
|
103
|
-
"lint:labels": "package-build labels check",
|
|
104
102
|
format: "package-build format --write",
|
|
105
103
|
"format:check": "package-build format",
|
|
106
104
|
changeset: "changeset",
|
|
@@ -125,8 +123,7 @@ function projectScripts(answers) {
|
|
|
125
123
|
"push:qa": "package-build deploy qa",
|
|
126
124
|
"push:prod": "package-build deploy prod",
|
|
127
125
|
});
|
|
128
|
-
scripts.lint =
|
|
129
|
-
"run-s lint:format lint:markdown lint:lang lint:addresses lint:content-links lint:labels";
|
|
126
|
+
scripts.lint = "run-s lint:format lint:markdown lint:lang lint:addresses lint:content-links";
|
|
130
127
|
return scripts;
|
|
131
128
|
}
|
|
132
129
|
|
|
@@ -295,14 +292,6 @@ function projectText(answers) {
|
|
|
295
292
|
updateInternalDependencies: "patch",
|
|
296
293
|
ignore: [],
|
|
297
294
|
}),
|
|
298
|
-
".github/labels.yml": YAML.stringify([
|
|
299
|
-
{
|
|
300
|
-
name: "documentation",
|
|
301
|
-
color: "0075ca",
|
|
302
|
-
description: "Documentation and author guides.",
|
|
303
|
-
},
|
|
304
|
-
{ name: "devops", color: "e07b31", description: "Build, CI, and release work." },
|
|
305
|
-
]),
|
|
306
295
|
".github/ISSUE_REPORTING.md": [
|
|
307
296
|
"# Issue reporting",
|
|
308
297
|
"",
|
|
@@ -314,12 +303,9 @@ function projectText(answers) {
|
|
|
314
303
|
"",
|
|
315
304
|
"Use the organization issue type for a bug, feature, task, spike, or epic.",
|
|
316
305
|
"",
|
|
317
|
-
"## 3. Labels
|
|
306
|
+
"## 3. Labels",
|
|
318
307
|
"",
|
|
319
|
-
"
|
|
320
|
-
"| ----- | ---------- |",
|
|
321
|
-
"| `documentation` | Documentation and author guides. |",
|
|
322
|
-
"| `devops` | Build, CI, and release work. |",
|
|
308
|
+
"Labels are organization-level, defined in `HeroicLands/.github`.",
|
|
323
309
|
"",
|
|
324
310
|
].join("\n"),
|
|
325
311
|
".github/workflows/build.yml": [
|
|
@@ -437,7 +423,6 @@ export function checkProject(rootDir) {
|
|
|
437
423
|
"README.md",
|
|
438
424
|
"LICENSE.md",
|
|
439
425
|
".changeset/config.json",
|
|
440
|
-
".github/labels.yml",
|
|
441
426
|
".github/ISSUE_REPORTING.md",
|
|
442
427
|
".github/workflows/build.yml",
|
|
443
428
|
];
|
|
@@ -525,15 +510,5 @@ export function checkProject(rootDir) {
|
|
|
525
510
|
} else if (!fs.readdirSync(contentRoot).some((file) => file.endsWith(".md"))) {
|
|
526
511
|
add("assets/content", "the content tree has no notes");
|
|
527
512
|
}
|
|
528
|
-
const labelsPath = path.join(root, ".github/labels.yml");
|
|
529
|
-
const reportingPath = path.join(root, ".github/ISSUE_REPORTING.md");
|
|
530
|
-
if (fs.existsSync(labelsPath) && fs.existsSync(reportingPath)) {
|
|
531
|
-
const result = checkLabelRegistry({
|
|
532
|
-
registryText: fs.readFileSync(labelsPath, "utf8"),
|
|
533
|
-
docText: fs.readFileSync(reportingPath, "utf8"),
|
|
534
|
-
});
|
|
535
|
-
for (const finding of result.registry) add(".github/labels.yml", finding.message);
|
|
536
|
-
for (const finding of result.doc) add(".github/ISSUE_REPORTING.md", finding.message);
|
|
537
|
-
}
|
|
538
513
|
return findings;
|
|
539
514
|
}
|