@janga/norna 0.7.27 → 0.7.29
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 +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -7
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/SearchPage.astro +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +9 -8
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,251 +1,32 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
3
1
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!value) throw new Error(`${name} requires a value.\n${usage}`);
|
|
36
|
-
return { consumed: 1, value };
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const parseArgs = (rawArgs) => {
|
|
42
|
-
const options = {
|
|
43
|
-
dryRun: false,
|
|
44
|
-
help: false,
|
|
45
|
-
order: null,
|
|
46
|
-
parent: null,
|
|
47
|
-
slug: null,
|
|
48
|
-
titleParts: [],
|
|
49
|
-
};
|
|
50
|
-
const seen = new Set();
|
|
51
|
-
|
|
52
|
-
for (let index = 0; index < rawArgs.length;) {
|
|
53
|
-
const arg = rawArgs[index];
|
|
54
|
-
if (arg === '-h' || arg === '--help') {
|
|
55
|
-
options.help = true;
|
|
56
|
-
index += 1;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (arg === '--dry-run') {
|
|
60
|
-
if (seen.has('dryRun')) throw new Error(`--dry-run may be specified only once.\n${usage}`);
|
|
61
|
-
seen.add('dryRun');
|
|
62
|
-
options.dryRun = true;
|
|
63
|
-
index += 1;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let matched = false;
|
|
68
|
-
for (const [name, key] of [['--order', 'order'], ['--parent', 'parent'], ['--slug', 'slug']]) {
|
|
69
|
-
const parsed = parseValueOption(rawArgs, index, name);
|
|
70
|
-
if (!parsed) continue;
|
|
71
|
-
if (seen.has(key)) throw new Error(`${name} may be specified only once.\n${usage}`);
|
|
72
|
-
seen.add(key);
|
|
73
|
-
options[key] = parsed.value;
|
|
74
|
-
index += parsed.consumed;
|
|
75
|
-
matched = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
if (matched) continue;
|
|
79
|
-
|
|
80
|
-
if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
81
|
-
options.titleParts.push(arg);
|
|
82
|
-
index += 1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return options;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const parseOrder = (value) => {
|
|
89
|
-
if (!/^\d{1,3}$/.test(value)) {
|
|
90
|
-
throw new Error(`Invalid --order "${value}". Use an integer from 1 to 999.`);
|
|
91
|
-
}
|
|
92
|
-
const order = Number.parseInt(value, 10);
|
|
93
|
-
if (order < 1 || order > 999) {
|
|
94
|
-
throw new Error(`Invalid --order "${value}". Use an integer from 1 to 999.`);
|
|
95
|
-
}
|
|
96
|
-
return order;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const normalizeParentPath = (value) => {
|
|
100
|
-
if (value === '/') return '';
|
|
101
|
-
if (value.includes('\\') || value.includes('?') || value.includes('#')) {
|
|
102
|
-
throw new Error(`Invalid --parent "${value}". Use a logical path such as /guides/installation/.`);
|
|
103
|
-
}
|
|
104
|
-
const normalized = value.replace(/^\/+|\/+$/g, '');
|
|
105
|
-
if (!normalized || normalized.split('/').some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) {
|
|
106
|
-
throw new Error(`Invalid --parent "${value}". Use / for the top level or a logical path such as /guides/installation/.`);
|
|
107
|
-
}
|
|
108
|
-
return normalized;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const getNextOrder = (siblings) => {
|
|
112
|
-
const highestOrder = siblings.reduce((highest, node) => Math.max(highest, node.pageOrder), 0);
|
|
113
|
-
const nextOrder = Math.floor(highestOrder / 10) * 10 + 10;
|
|
114
|
-
if (nextOrder > 999) {
|
|
115
|
-
throw new Error('No automatic page order remains below 1000. Reorder the sibling nodes before adding another one.');
|
|
116
|
-
}
|
|
117
|
-
return nextOrder;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const toDisplayPath = (filePath) => {
|
|
121
|
-
const relativePath = path.relative(siteProjectRoot, filePath);
|
|
122
|
-
return (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
|
|
123
|
-
? relativePath
|
|
124
|
-
: filePath).split(path.sep).join('/');
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const escapeMarkdownHeading = (value) => value.replace(/([\\`*_[\]{}<>#])/g, '\\$1');
|
|
128
|
-
|
|
129
|
-
const canonicalDirectory = async (directory) => realpath(directory).catch((error) => {
|
|
130
|
-
if (error?.code === 'ENOENT') return path.resolve(directory);
|
|
131
|
-
throw error;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const resolveParent = async ({ nodes, parent }) => {
|
|
135
|
-
if (parent !== null) {
|
|
136
|
-
const parentPath = normalizeParentPath(parent);
|
|
137
|
-
if (!parentPath) {
|
|
138
|
-
return { collectionDir: sitePagesDir, node: null, pagePath: '' };
|
|
139
|
-
}
|
|
140
|
-
const node = nodes.find((candidate) => candidate.pagePath === parentPath);
|
|
141
|
-
if (!node) {
|
|
142
|
-
throw new Error(`Cannot find parent "${parent}". Use an existing logical page/category path, or / for the top level.`);
|
|
143
|
-
}
|
|
144
|
-
if (node.isHome) throw new Error('The homepage cannot contain child pages or categories.');
|
|
145
|
-
return { collectionDir: path.join(node.nodeDir, 'pages'), node, pagePath: node.pagePath };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const currentDirectory = await canonicalDirectory(invocationRoot);
|
|
149
|
-
if (currentDirectory === await canonicalDirectory(sitePagesDir)) {
|
|
150
|
-
return { collectionDir: sitePagesDir, node: null, pagePath: '' };
|
|
151
|
-
}
|
|
152
|
-
let node = null;
|
|
153
|
-
for (const candidate of nodes) {
|
|
154
|
-
if (await canonicalDirectory(candidate.nodeDir) === currentDirectory) {
|
|
155
|
-
node = candidate;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (!node) {
|
|
160
|
-
throw new Error([
|
|
161
|
-
'Cannot infer where to add the node from the current directory.',
|
|
162
|
-
`Run the command from ${toDisplayPath(sitePagesDir)} for a top-level node, from an existing page/category directory for a child, or pass --parent.`,
|
|
163
|
-
].join('\n'));
|
|
164
|
-
}
|
|
165
|
-
if (node.isHome) throw new Error('The homepage cannot contain child pages or categories.');
|
|
166
|
-
return { collectionDir: path.join(node.nodeDir, 'pages'), node, pagePath: node.pagePath };
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const assertCollectionDirectory = async (collectionDir) => {
|
|
170
|
-
try {
|
|
171
|
-
const info = await stat(collectionDir);
|
|
172
|
-
if (!info.isDirectory()) throw new Error(`${toDisplayPath(collectionDir)} exists but is not a directory.`);
|
|
173
|
-
return false;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
176
|
-
await mkdir(collectionDir);
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const writeNode = async ({ destination, kind, title }) => {
|
|
182
|
-
const stagingRoot = path.join(siteDir, '.norna', 'create');
|
|
183
|
-
await mkdir(stagingRoot, { recursive: true });
|
|
184
|
-
const stagingDirectory = path.join(stagingRoot, `node-${process.pid}-${randomUUID()}`);
|
|
185
|
-
await mkdir(stagingDirectory);
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
if (kind === 'page') {
|
|
189
|
-
await writeFile(path.join(stagingDirectory, 'content.md'), `# ${escapeMarkdownHeading(title)}\n\n## Introduction\n\nStart writing here.\n`);
|
|
190
|
-
await mkdir(path.join(stagingDirectory, 'images'));
|
|
191
|
-
} else {
|
|
192
|
-
await writeFile(path.join(stagingDirectory, 'category.yaml'), dumpYaml({ label: title }, {
|
|
193
|
-
lineWidth: -1,
|
|
194
|
-
noRefs: true,
|
|
195
|
-
}));
|
|
196
|
-
await mkdir(path.join(stagingDirectory, 'pages'));
|
|
197
|
-
}
|
|
198
|
-
await rename(stagingDirectory, destination);
|
|
199
|
-
} finally {
|
|
200
|
-
await rm(stagingDirectory, { force: true, recursive: true });
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const [kind, ...rawArgs] = process.argv.slice(2);
|
|
205
|
-
if (!['category', 'page'].includes(kind)) throw new Error(usage);
|
|
206
|
-
const options = parseArgs(rawArgs);
|
|
207
|
-
if (options.help) {
|
|
208
|
-
console.log(usage);
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const title = options.titleParts.join(' ').trim();
|
|
213
|
-
if (!title || /[\r\n]/.test(title)) throw new Error(`A one-line ${kind === 'page' ? 'page title' : 'category label'} is required.\n${usage}`);
|
|
214
|
-
const slug = options.slug ?? slugifyAsciiIdentifier(title);
|
|
215
|
-
if (!slug || !pageDirectoryPattern.test(`010-${slug}`)) {
|
|
216
|
-
throw new Error(`Invalid ${options.slug === null ? 'generated ' : ''}slug "${slug}". Use lowercase ASCII letters, numbers, and single hyphens; pass --slug when automatic transliteration is unsuitable.`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const structure = await getSiteStructure();
|
|
220
|
-
const parent = await resolveParent({ nodes: structure.nodes, parent: options.parent });
|
|
221
|
-
const siblings = structure.nodes.filter((node) => node.parentPagePath === (parent.pagePath || null) && !node.isHome);
|
|
222
|
-
if (siblings.some((node) => node.pageId === slug)) {
|
|
223
|
-
throw new Error(`Cannot create "${slug}" below ${parent.pagePath ? `/${parent.pagePath}/` : '/'}. A sibling with that slug already exists.`);
|
|
224
|
-
}
|
|
225
|
-
const order = options.order === null ? getNextOrder(siblings) : parseOrder(options.order);
|
|
226
|
-
if (siblings.some((node) => node.pageOrder === order)) {
|
|
227
|
-
throw new Error(`Cannot use order ${String(order).padStart(3, '0')} below ${parent.pagePath ? `/${parent.pagePath}/` : '/'}. A sibling already uses it.`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const directoryName = `${String(order).padStart(3, '0')}-${slug}`;
|
|
231
|
-
const destination = path.join(parent.collectionDir, directoryName);
|
|
232
|
-
const pagePath = [parent.pagePath, slug].filter(Boolean).join('/');
|
|
233
|
-
const action = options.dryRun ? 'Would create' : 'Created';
|
|
234
|
-
|
|
235
|
-
if (!options.dryRun) {
|
|
236
|
-
const createdCollection = await assertCollectionDirectory(parent.collectionDir);
|
|
237
|
-
try {
|
|
238
|
-
await writeNode({ destination, kind, title });
|
|
239
|
-
} catch (error) {
|
|
240
|
-
if (createdCollection) await rmdir(parent.collectionDir).catch(() => {});
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
console.log(`${action} ${kind} "${title}" at ${toDisplayPath(destination)}.`);
|
|
246
|
-
if (kind === 'page') {
|
|
247
|
-
console.log(`URL: /${pagePath}/`);
|
|
248
|
-
} else {
|
|
249
|
-
console.log('This navigation category has no authored content page. Norna generates its destination from the child pages.');
|
|
250
|
-
console.log(`Child URL prefix: /${pagePath}/`);
|
|
251
|
-
}
|
|
2
|
+
import { invocationRoot, siteDir, siteProjectRoot } from './lib/site-paths.mjs';
|
|
3
|
+
import { createSiteNode, planSiteNodeCreation } from './lib/site-node-create.mjs';
|
|
4
|
+
import { parsePlacementArguments, confirmCliRenumbering, describeRenumbering } from './lib/page-placement-cli.mjs';
|
|
5
|
+
|
|
6
|
+
const usage = `Usage:
|
|
7
|
+
norna page:add <title> [--parent <path>] [--first | --last] [--slug <slug>]
|
|
8
|
+
norna page:add <title> --before <page-url> | --after <page-url>
|
|
9
|
+
|
|
10
|
+
Options: --order <integer>, --dry-run, --allow-renumber
|
|
11
|
+
Without a placement, append under --parent or the current page/site directory.
|
|
12
|
+
Before/after infer the parent. --order cannot accompany a relative placement.`;
|
|
13
|
+
const display = filename => path.relative(siteProjectRoot, filename) || filename;
|
|
14
|
+
try {
|
|
15
|
+
const [kind, ...args] = process.argv.slice(2);
|
|
16
|
+
if (kind !== 'page') throw new Error(usage);
|
|
17
|
+
const options = parsePlacementArguments(args);
|
|
18
|
+
if (options.help) { console.log(usage); process.exit(0); }
|
|
19
|
+
if (options.write || options.noAliases) throw new Error('page:add uses --dry-run; --write and --no-aliases are page:move options.');
|
|
20
|
+
const title = options.positionals.join(' ').trim();
|
|
21
|
+
if (!title) throw new Error(`A one-line page title is required.\n${usage}`);
|
|
22
|
+
const plan = await planSiteNodeCreation({ siteRoot: siteDir, kind, title, slug: options.slug, order: options.order,
|
|
23
|
+
parentPath: options.parent, invocationDirectory: invocationRoot,
|
|
24
|
+
placement: options.order !== null ? null : options.placement, targetPath: options.target });
|
|
25
|
+
console.log(`Page: ${title}\nSource: ${display(plan.destination)}\nURL: ${plan.url}\nOrder: ${plan.order}`);
|
|
26
|
+
const renumbering = describeRenumbering(plan.orderChanges, display);
|
|
27
|
+
if (renumbering) console.log(renumbering);
|
|
28
|
+
if (options.dryRun) { console.log(`Would create page "${title}". Dry run only. No files changed.`); process.exit(0); }
|
|
29
|
+
if (!await confirmCliRenumbering(plan.orderChanges, options)) { console.log('Cancelled. No files changed.'); process.exit(0); }
|
|
30
|
+
const result = await createSiteNode(plan, { allowRenumber: true });
|
|
31
|
+
console.log(`Created page "${title}" at ${display(plan.destination)}. ${result.renumberedPages} existing sibling page(s) renumbered.`);
|
|
32
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
package/scripts/build-site.mjs
CHANGED
|
@@ -9,9 +9,14 @@ const runScript = (relativePath, args = []) => runInherit(
|
|
|
9
9
|
{ cwd: siteProjectRoot },
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
await runScript('scripts/
|
|
14
|
-
await runScript('scripts/sync-
|
|
15
|
-
await runScript('scripts/
|
|
16
|
-
await
|
|
17
|
-
await
|
|
12
|
+
try {
|
|
13
|
+
await runScript('scripts/check-config.mjs');
|
|
14
|
+
await runScript('scripts/sync-content-sections.mjs', ['--check']);
|
|
15
|
+
await runScript('scripts/sync-site-public.mjs');
|
|
16
|
+
await runScript('scripts/generate-images.mjs');
|
|
17
|
+
await runAstroInherit(['build']);
|
|
18
|
+
await runScript('scripts/generate-search-index.mjs');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error.message);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
package/scripts/check-config.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { siteConfigLabel, sitePagesDir, sitePublicLabel, siteThemeLabel } from './lib/site-paths.mjs';
|
|
1
|
+
import { siteConfigLabel, siteDir, sitePublicLabel } from './lib/site-paths.mjs';
|
|
3
2
|
import { getLogoAssets, getPublicAssetInspection } from './lib/logo-assets.mjs';
|
|
4
3
|
import { logoAssetFilenames, socialImageAssetFilenames } from './lib/public-asset-conventions.mjs';
|
|
5
4
|
import { getSocialImageAssets } from './lib/social-image-assets.mjs';
|
|
6
5
|
import { readSitewideContent } from './lib/sitewide-content.mjs';
|
|
7
6
|
import { assertSectionBackgroundPatternCompatibility } from './lib/presentation.mjs';
|
|
8
7
|
import { readThemeConfig, validatePageThemeFiles } from './lib/theme-config.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { selectPageTheme } from './lib/theme-packages.mjs';
|
|
10
9
|
|
|
11
10
|
const formatErrorMessage = (error) => {
|
|
12
11
|
if (error instanceof Error) {
|
|
@@ -21,26 +20,11 @@ try {
|
|
|
21
20
|
const themeConfig = await readThemeConfig();
|
|
22
21
|
const sitewideContent = await readSitewideContent();
|
|
23
22
|
const pageThemeFiles = await validatePageThemeFiles();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const ancestorTheme = pageThemesByDirectory.get(directory);
|
|
30
|
-
if (ancestorTheme) ancestorThemes.unshift(ancestorTheme);
|
|
31
|
-
directory = path.dirname(directory);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const resolvedPageTheme = ancestorThemes.reduce(
|
|
35
|
-
(current, ancestor) => mergePageThemeConfig(current, ancestor.config),
|
|
36
|
-
themeConfig,
|
|
37
|
-
);
|
|
38
|
-
resolveThemeVisualConfig(resolvedPageTheme, pageThemeFile.label);
|
|
39
|
-
}
|
|
40
|
-
if (projectConfig.navigation.mode === 'tree') {
|
|
41
|
-
assertSectionBackgroundPatternCompatibility(themeConfig, siteThemeLabel, 'tree');
|
|
42
|
-
for (const pageThemeFile of pageThemeFiles) {
|
|
43
|
-
assertSectionBackgroundPatternCompatibility(pageThemeFile.config, pageThemeFile.label, 'tree');
|
|
23
|
+
for (const pageDirectory of new Set(['.', ...pageThemeFiles.map((file) => file.pageDirectory)])) {
|
|
24
|
+
const { treeTheme, selected } = await selectPageTheme({ siteRoot: siteDir, pageDirectory });
|
|
25
|
+
for (const theme of [treeTheme, selected]) {
|
|
26
|
+
resolveThemeVisualConfig(theme.config, theme.label);
|
|
27
|
+
if (projectConfig.navigation.mode === 'tree') assertSectionBackgroundPatternCompatibility(theme.config, theme.label, 'tree');
|
|
44
28
|
}
|
|
45
29
|
}
|
|
46
30
|
const logoAssets = getLogoAssets();
|
package/scripts/dev-local.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readDevServerIdentity, verifyDevServer } from './lib/dev-server-identity.mjs';
|
|
1
3
|
import { execFile } from 'node:child_process';
|
|
2
4
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
5
|
import net from 'node:net';
|
|
@@ -10,20 +12,35 @@ import {
|
|
|
10
12
|
engineRoot,
|
|
11
13
|
siteProjectRoot,
|
|
12
14
|
siteStateDir,
|
|
15
|
+
siteDir,
|
|
13
16
|
} from './lib/site-paths.mjs';
|
|
14
17
|
|
|
15
18
|
const execFileAsync = promisify(execFile);
|
|
16
19
|
|
|
17
|
-
const
|
|
20
|
+
const portValue = process.env.NORNA_DEV_PORT ?? '4321';
|
|
21
|
+
const port = /^\d+$/.test(portValue) ? Number(portValue) : NaN;
|
|
22
|
+
const serverToken = process.env.NORNA_DEV_START_TOKEN || randomUUID();
|
|
18
23
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
19
24
|
throw new Error('NORNA_DEV_PORT must be an integer from 1 through 65535.');
|
|
20
25
|
}
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const knownCommands = new Set(['start', 'lan', 'status', 'logs', 'restart', 'stop']);
|
|
28
|
+
const command = args.find((arg) => knownCommands.has(arg)) ?? args.find((arg) => !arg.startsWith('-')) ?? 'start';
|
|
29
|
+
const shouldKillBlockingPort = args.includes('--kill');
|
|
30
|
+
const shouldFollowLogs = args.includes('--follow');
|
|
31
|
+
|
|
21
32
|
let projectConfig;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
if (['stop', 'status', 'logs'].includes(command)) {
|
|
34
|
+
// Recovery commands use the recorded server even while sources need conversion.
|
|
35
|
+
const recorded = await readFile(path.join(siteStateDir, 'dev', 'state.json'), 'utf8').then(JSON.parse).catch(() => null);
|
|
36
|
+
projectConfig = { site: { basePath: recorded?.url ? new URL(recorded.url).pathname : '/' } };
|
|
37
|
+
} else {
|
|
38
|
+
try {
|
|
39
|
+
({ default: projectConfig } = await import('./lib/project-config.mjs'));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
const localHost = '127.0.0.1';
|
|
29
46
|
const localUrl = `http://${localHost}:${port}${projectConfig.site.basePath}`;
|
|
@@ -38,11 +55,6 @@ const legacyStatePath = path.join(astroCacheDir, 'dev-local.json');
|
|
|
38
55
|
const astroStatePath = path.join(astroCacheDir, 'dev.json');
|
|
39
56
|
const logPath = path.join(astroCacheDir, 'dev.log');
|
|
40
57
|
const preparationLogPath = path.join(stateDirectory, 'preparation.log');
|
|
41
|
-
const args = process.argv.slice(2);
|
|
42
|
-
const knownCommands = new Set(['start', 'lan', 'status', 'logs', 'restart', 'stop']);
|
|
43
|
-
const command = args.find((arg) => knownCommands.has(arg)) ?? args.find((arg) => !arg.startsWith('-')) ?? 'start';
|
|
44
|
-
const shouldKillBlockingPort = args.includes('--kill');
|
|
45
|
-
const shouldFollowLogs = args.includes('--follow');
|
|
46
58
|
|
|
47
59
|
const runAstro = async (args, options = {}) => execFileAsync(process.execPath, getAstroArgs(args), {
|
|
48
60
|
cwd: siteProjectRoot,
|
|
@@ -100,6 +112,8 @@ const writeState = async (host) => {
|
|
|
100
112
|
await writeFile(statePath, `${JSON.stringify({
|
|
101
113
|
port,
|
|
102
114
|
pid,
|
|
115
|
+
token: serverToken,
|
|
116
|
+
siteRoot: siteDir,
|
|
103
117
|
host,
|
|
104
118
|
mode: host === '0.0.0.0' ? 'lan' : 'local',
|
|
105
119
|
url: localUrl,
|
|
@@ -360,6 +374,12 @@ const openBrowser = async () => {
|
|
|
360
374
|
|
|
361
375
|
const stopServer = async ({ quiet = false } = {}) => {
|
|
362
376
|
const state = await readState();
|
|
377
|
+
if (process.env.NORNA_DEV_EXPECT_TOKEN) {
|
|
378
|
+
if (state?.token !== process.env.NORNA_DEV_EXPECT_TOKEN || state.port !== port || !(await verifyDevServer(state, siteDir))) throw new Error('Cannot verify this site’s server. No process was stopped.');
|
|
379
|
+
if (!(await terminateTrackedProcess(state, state.host ?? localHost))) throw new Error('The verified preview server did not stop. Try Stop Preview Server again.');
|
|
380
|
+
await removeState();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
363
383
|
let astroError;
|
|
364
384
|
let astroOutput = '';
|
|
365
385
|
try {
|
|
@@ -390,7 +410,7 @@ const stopServer = async ({ quiet = false } = {}) => {
|
|
|
390
410
|
};
|
|
391
411
|
|
|
392
412
|
const startServer = async ({ host = localHost, open = true, killBlockingPort = false } = {}) => {
|
|
393
|
-
await stopServer({ quiet: true });
|
|
413
|
+
if (process.env.NORNA_DEV_NO_RESTART !== '1') await stopServer({ quiet: true });
|
|
394
414
|
|
|
395
415
|
if (!(await isPortFree(host))) {
|
|
396
416
|
if (!killBlockingPort) {
|
|
@@ -425,8 +445,24 @@ const startServer = async ({ host = localHost, open = true, killBlockingPort = f
|
|
|
425
445
|
}
|
|
426
446
|
const previousLog = await readDevLog();
|
|
427
447
|
try {
|
|
428
|
-
await runAstroInherit(['dev', '--background', '--host', host, '--port', String(port)]);
|
|
448
|
+
await runAstroInherit(['dev', '--background', '--host', host, '--port', String(port)], { env: { ...process.env, NORNA_DEV_TOKEN: serverToken } });
|
|
449
|
+
// Record the process before probing rendered pages, so failed readiness
|
|
450
|
+
// checks can clean up only the server started by this request.
|
|
451
|
+
await writeState(host);
|
|
452
|
+
await waitForServer();
|
|
429
453
|
} catch (error) {
|
|
454
|
+
let cleanupHint = '';
|
|
455
|
+
const identity = await readDevServerIdentity({ port, token: serverToken }, siteDir);
|
|
456
|
+
if (identity) {
|
|
457
|
+
const candidate = { port, ...identity };
|
|
458
|
+
try {
|
|
459
|
+
if (!(await terminateTrackedProcess(candidate, host))) throw new Error('The process did not stop.');
|
|
460
|
+
const recorded = await readState();
|
|
461
|
+
if (recorded?.token === serverToken) await removeState();
|
|
462
|
+
} catch (cleanup) {
|
|
463
|
+
cleanupHint = `Cleanup failed: ${cleanup.message} Use the site's dev:stop command after inspecting the log.`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
430
466
|
const currentLog = await readDevLog();
|
|
431
467
|
const newLog = currentLog.startsWith(previousLog)
|
|
432
468
|
? currentLog.slice(previousLog.length)
|
|
@@ -436,11 +472,9 @@ const startServer = async ({ host = localHost, open = true, killBlockingPort = f
|
|
|
436
472
|
`Could not start the dev server at ${localUrl}.`,
|
|
437
473
|
excerpt ? `Astro reported:\n${excerpt}` : 'Astro did not write a detailed startup error.',
|
|
438
474
|
`Full log: ${logPath}`,
|
|
439
|
-
|
|
475
|
+
cleanupHint,
|
|
476
|
+
].filter(Boolean).join('\n\n'), { cause: error });
|
|
440
477
|
}
|
|
441
|
-
await waitForServer();
|
|
442
|
-
|
|
443
|
-
await writeState(host);
|
|
444
478
|
if (open) {
|
|
445
479
|
await openBrowser();
|
|
446
480
|
} else {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
renderThemePresetReference,
|
|
5
5
|
themePresetNames,
|
|
6
6
|
} from './lib/theme-presets.mjs';
|
|
7
|
-
import { siteDir, siteThemeLabel } from './lib/site-paths.mjs';
|
|
7
|
+
import { siteDir, siteDirLabel, siteThemeLabel } from './lib/site-paths.mjs';
|
|
8
8
|
|
|
9
9
|
const [presetName, ...extraArgs] = process.argv.slice(2);
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ if (!presetName || extraArgs.length > 0) {
|
|
|
14
14
|
|
|
15
15
|
const filename = `orig-${presetName}-theme.yaml`;
|
|
16
16
|
const outputPath = path.join(siteDir, filename);
|
|
17
|
-
const outputLabel = path.posix.join(
|
|
17
|
+
const outputLabel = path.posix.join(siteDirLabel, filename);
|
|
18
18
|
const source = renderThemePresetReference(presetName, siteThemeLabel);
|
|
19
19
|
|
|
20
20
|
try {
|