@janga/norna 0.7.28 → 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 +8 -5
- package/astro.config.mjs +14 -2
- package/package.json +10 -8
- package/schemas/config.schema.json +80 -65
- package/schemas/content-frontmatter.schema.json +15 -8
- package/schemas/manifest.json +1 -1
- package/schemas/page-theme.schema.json +237 -237
- package/schemas/root-theme.schema.json +237 -237
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +237 -237
- package/scripts/add-site-node.mjs +29 -100
- package/scripts/dev-local.mjs +34 -7
- package/scripts/generate-images.mjs +83 -65
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-routes.json +63 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +9 -9
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +2 -0
- package/scripts/lib/editor-language-service.mjs +21 -18
- package/scripts/lib/editor-page-addresses.mjs +5 -3
- package/scripts/lib/editor-page-placement.mjs +28 -98
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +44 -9
- package/scripts/lib/editor-site-files.mjs +3 -2
- package/scripts/lib/editor-site-links.mjs +6 -4
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +10 -3
- package/scripts/lib/editor-source-files.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/norna-markdown-blocks.mjs +0 -57
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +5 -0
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +8 -22
- package/scripts/lib/page-move-apply.mjs +6 -4
- package/scripts/lib/page-move-plan.mjs +37 -19
- 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/project-config.mjs +3 -0
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +4 -0
- package/scripts/lib/schema-editor-metadata.mjs +4 -1
- package/scripts/lib/site-content.mjs +1 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-node-create.mjs +47 -34
- package/scripts/lib/site-paths.mjs +2 -2
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +14 -13
- package/scripts/lib/site-upgrade.mjs +2 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +1 -1
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/move-site-page.mjs +46 -133
- package/scripts/sync-content-sections.mjs +8 -10
- package/scripts/sync-site-public.mjs +44 -66
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/SearchPage.astro +2 -1
- package/src/components/SitePage.astro +1 -0
- package/src/components/SiteSection.astro +1 -0
- package/src/content.config.ts +1 -4
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/sectionContent.ts +64 -19
- package/src/lib/sitePages.ts +4 -30
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/[...slug].astro +2 -1
- package/src/styles/area-navigation.css +14 -5
- package/src/styles/content.css +6 -0
- package/src/styles/media.css +24 -0
- package/starters/basic/site/site-config/settings.yaml +1 -0
|
@@ -1,103 +1,32 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { invocationRoot, siteDir, siteProjectRoot } from './lib/site-paths.mjs';
|
|
3
3
|
import { createSiteNode, planSiteNodeCreation } from './lib/site-node-create.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
norna page:add <title> [--parent <path>] [--
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
help: false,
|
|
34
|
-
order: null,
|
|
35
|
-
parent: null,
|
|
36
|
-
slug: null,
|
|
37
|
-
titleParts: [],
|
|
38
|
-
};
|
|
39
|
-
const seen = new Set();
|
|
40
|
-
|
|
41
|
-
for (let index = 0; index < rawArgs.length;) {
|
|
42
|
-
const arg = rawArgs[index];
|
|
43
|
-
if (arg === '-h' || arg === '--help') {
|
|
44
|
-
options.help = true;
|
|
45
|
-
index += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (arg === '--dry-run') {
|
|
49
|
-
if (seen.has('dryRun')) throw new Error(`--dry-run may be specified only once.\n${usage}`);
|
|
50
|
-
seen.add('dryRun');
|
|
51
|
-
options.dryRun = true;
|
|
52
|
-
index += 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let matched = false;
|
|
57
|
-
for (const [name, key] of [['--order', 'order'], ['--parent', 'parent'], ['--slug', 'slug']]) {
|
|
58
|
-
const parsed = parseValueOption(rawArgs, index, name);
|
|
59
|
-
if (!parsed) continue;
|
|
60
|
-
if (seen.has(key)) throw new Error(`${name} may be specified only once.\n${usage}`);
|
|
61
|
-
seen.add(key);
|
|
62
|
-
options[key] = parsed.value;
|
|
63
|
-
index += parsed.consumed;
|
|
64
|
-
matched = true;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
if (matched) continue;
|
|
68
|
-
|
|
69
|
-
if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
70
|
-
options.titleParts.push(arg);
|
|
71
|
-
index += 1;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return options;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const toDisplayPath = (filePath) => {
|
|
78
|
-
const relativePath = path.relative(siteProjectRoot, filePath);
|
|
79
|
-
return (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
|
|
80
|
-
? relativePath
|
|
81
|
-
: filePath).split(path.sep).join('/');
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const [kind, ...rawArgs] = process.argv.slice(2);
|
|
85
|
-
if (kind !== 'page') throw new Error(usage);
|
|
86
|
-
const options = parseArgs(rawArgs);
|
|
87
|
-
if (options.help) {
|
|
88
|
-
console.log(usage);
|
|
89
|
-
process.exit(0);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const title = options.titleParts.join(' ').trim();
|
|
93
|
-
if (!title || /[\r\n]/.test(title)) throw new Error(`A one-line page title is required.\n${usage}`);
|
|
94
|
-
const plan = await planSiteNodeCreation({
|
|
95
|
-
siteRoot: siteDir, kind, title, slug: options.slug, order: options.order,
|
|
96
|
-
parentPath: options.parent, invocationDirectory: invocationRoot,
|
|
97
|
-
});
|
|
98
|
-
const { destination, pagePath } = plan;
|
|
99
|
-
const action = options.dryRun ? 'Would create' : 'Created';
|
|
100
|
-
if (!options.dryRun) await createSiteNode(plan);
|
|
101
|
-
|
|
102
|
-
console.log(`${action} page "${title}" at ${toDisplayPath(destination)}.`);
|
|
103
|
-
console.log(`URL: /${pagePath}/`);
|
|
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/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,11 +12,14 @@ 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
|
}
|
|
@@ -107,6 +112,8 @@ const writeState = async (host) => {
|
|
|
107
112
|
await writeFile(statePath, `${JSON.stringify({
|
|
108
113
|
port,
|
|
109
114
|
pid,
|
|
115
|
+
token: serverToken,
|
|
116
|
+
siteRoot: siteDir,
|
|
110
117
|
host,
|
|
111
118
|
mode: host === '0.0.0.0' ? 'lan' : 'local',
|
|
112
119
|
url: localUrl,
|
|
@@ -367,6 +374,12 @@ const openBrowser = async () => {
|
|
|
367
374
|
|
|
368
375
|
const stopServer = async ({ quiet = false } = {}) => {
|
|
369
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
|
+
}
|
|
370
383
|
let astroError;
|
|
371
384
|
let astroOutput = '';
|
|
372
385
|
try {
|
|
@@ -397,7 +410,7 @@ const stopServer = async ({ quiet = false } = {}) => {
|
|
|
397
410
|
};
|
|
398
411
|
|
|
399
412
|
const startServer = async ({ host = localHost, open = true, killBlockingPort = false } = {}) => {
|
|
400
|
-
await stopServer({ quiet: true });
|
|
413
|
+
if (process.env.NORNA_DEV_NO_RESTART !== '1') await stopServer({ quiet: true });
|
|
401
414
|
|
|
402
415
|
if (!(await isPortFree(host))) {
|
|
403
416
|
if (!killBlockingPort) {
|
|
@@ -432,8 +445,24 @@ const startServer = async ({ host = localHost, open = true, killBlockingPort = f
|
|
|
432
445
|
}
|
|
433
446
|
const previousLog = await readDevLog();
|
|
434
447
|
try {
|
|
435
|
-
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();
|
|
436
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
|
+
}
|
|
437
466
|
const currentLog = await readDevLog();
|
|
438
467
|
const newLog = currentLog.startsWith(previousLog)
|
|
439
468
|
? currentLog.slice(previousLog.length)
|
|
@@ -443,11 +472,9 @@ const startServer = async ({ host = localHost, open = true, killBlockingPort = f
|
|
|
443
472
|
`Could not start the dev server at ${localUrl}.`,
|
|
444
473
|
excerpt ? `Astro reported:\n${excerpt}` : 'Astro did not write a detailed startup error.',
|
|
445
474
|
`Full log: ${logPath}`,
|
|
446
|
-
|
|
475
|
+
cleanupHint,
|
|
476
|
+
].filter(Boolean).join('\n\n'), { cause: error });
|
|
447
477
|
}
|
|
448
|
-
await waitForServer();
|
|
449
|
-
|
|
450
|
-
await writeState(host);
|
|
451
478
|
if (open) {
|
|
452
479
|
await openBrowser();
|
|
453
480
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getPageImageSourceKey } from './lib/site-conventions.mjs';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { copyFile, mkdir, readdir, readFile, rm, stat, lstat, writeFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
|
|
27
|
-
const imageOutputVersion =
|
|
27
|
+
const imageOutputVersion = 3;
|
|
28
28
|
const sourceHashSlugLength = 8;
|
|
29
29
|
const widths = [480, 768, 1080, 1440, 1920];
|
|
30
30
|
|
|
@@ -86,7 +86,7 @@ const getSourceHashSlug = (sourceHash) => sourceHash.slice(0, sourceHashSlugLeng
|
|
|
86
86
|
|
|
87
87
|
const getGeneratedPath = (sourceKey, sourceHash, width) => {
|
|
88
88
|
const parsed = path.parse(sourceKey.replace(/^root\//, ''));
|
|
89
|
-
return path.join(generatedImagesDir, parsed.dir, `${parsed.
|
|
89
|
+
return path.join(generatedImagesDir, parsed.dir, `${parsed.base}-${getSourceHashSlug(sourceHash)}-${width}.webp`);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
const getOriginalPath = (sourceKey, sourceHash) => {
|
|
@@ -103,6 +103,8 @@ const getImageSourceKey = (contentFile, image) => getPageImageSourceKey(contentF
|
|
|
103
103
|
const getReferencedImages = async (contentFile) => {
|
|
104
104
|
const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
105
105
|
const page = await parsePageMarkdown(body, { label: contentFile.contentLabel });
|
|
106
|
+
const imageError = page.diagnostics.find((issue) => issue.code === 'invalid-markdown-image');
|
|
107
|
+
if (imageError) throw new Error(imageError.message + ' ' + imageError.fix);
|
|
106
108
|
const references = [];
|
|
107
109
|
|
|
108
110
|
for (const region of page.regions) {
|
|
@@ -138,6 +140,7 @@ const getContentSourcePath = ({ image, sourceKey }) => {
|
|
|
138
140
|
const getReferencedSources = async () => {
|
|
139
141
|
const contentFiles = await getContentFiles();
|
|
140
142
|
const seen = new Set();
|
|
143
|
+
const normalizedSources = new Map();
|
|
141
144
|
const sources = [];
|
|
142
145
|
|
|
143
146
|
for (const contentFile of contentFiles) {
|
|
@@ -152,7 +155,9 @@ const getReferencedSources = async () => {
|
|
|
152
155
|
continue;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
const fileStat = await
|
|
158
|
+
const fileStat = await lstat(sourcePath).catch(() => null);
|
|
159
|
+
const directoryStat = await lstat(contentFile.imagesDir).catch(() => null);
|
|
160
|
+
if (fileStat?.isSymbolicLink() || directoryStat?.isSymbolicLink()) fail(`Image ${sourcePath} must belong to a regular images/ directory; symbolic links are not supported.`);
|
|
156
161
|
if (!fileStat?.isFile()) {
|
|
157
162
|
fail(`Image file does not exist: ${contentFile.imagesLabel}/${siteImagePath}`);
|
|
158
163
|
}
|
|
@@ -161,6 +166,10 @@ const getReferencedSources = async () => {
|
|
|
161
166
|
fail(`Image "${imageName}" is used in ${reference.sectionLabel} but is not stored directly in ${contentFile.imagesLabel}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
const normalizedKey = reference.sourceKey.normalize('NFC').toLowerCase();
|
|
170
|
+
const previous = normalizedSources.get(normalizedKey);
|
|
171
|
+
if (previous) fail(`Image sources ${previous} and ${sourcePath} conflict after case/Unicode normalization. Rename one image.`);
|
|
172
|
+
normalizedSources.set(normalizedKey, sourcePath);
|
|
164
173
|
seen.add(reference.sourceKey);
|
|
165
174
|
sources.push({ sourcePath, sourceKey: reference.sourceKey, reference });
|
|
166
175
|
}
|
|
@@ -203,13 +212,13 @@ const validateCarouselOrientations = async (manifest) => {
|
|
|
203
212
|
}
|
|
204
213
|
};
|
|
205
214
|
|
|
206
|
-
const identify = async (sourcePath) => {
|
|
215
|
+
const identify = async (sourcePath, imageMagick) => {
|
|
207
216
|
const output = await imageMagick.identify(sourcePath);
|
|
208
217
|
const [width, height] = output.split(' ').map(Number);
|
|
209
218
|
return { width, height };
|
|
210
219
|
};
|
|
211
220
|
|
|
212
|
-
const convert = async (sourcePath, outputPath, width) => {
|
|
221
|
+
const convert = async (sourcePath, outputPath, width, imageMagick) => {
|
|
213
222
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
214
223
|
await imageMagick.convert(sourcePath, outputPath, width);
|
|
215
224
|
};
|
|
@@ -288,12 +297,12 @@ const getReusableEntry = async (sourceKey, previousEntry, sourceHash) => {
|
|
|
288
297
|
};
|
|
289
298
|
};
|
|
290
299
|
|
|
291
|
-
const getReusableStaticEntry = async (previousEntry, sourceHash) => {
|
|
300
|
+
const getReusableStaticEntry = async (previousEntry, sourceHash, sourceKey) => {
|
|
292
301
|
if (
|
|
293
302
|
previousEntry?.kind !== 'static'
|
|
294
303
|
|| previousEntry?.sourceHash !== sourceHash
|
|
295
304
|
|| previousEntry?.outputVersion !== imageOutputVersion
|
|
296
|
-
||
|
|
305
|
+
|| previousEntry?.src !== getPublicPath(getOriginalPath(sourceKey, sourceHash))
|
|
297
306
|
|| !(await fileExists(getFilePathFromPublicPath(previousEntry.src)))
|
|
298
307
|
) {
|
|
299
308
|
return null;
|
|
@@ -349,80 +358,89 @@ const removeUnreferencedGeneratedFiles = async (manifest) => {
|
|
|
349
358
|
}
|
|
350
359
|
};
|
|
351
360
|
|
|
352
|
-
|
|
361
|
+
const main = async () => {
|
|
362
|
+
await mkdir(generatedImagesDir, { recursive: true });
|
|
353
363
|
|
|
354
|
-
const previousManifest = await readManifest();
|
|
355
|
-
const sources = await getReferencedSources();
|
|
356
|
-
const manifest = {};
|
|
357
|
-
const isStaticSource = (sourcePath) => staticImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
358
|
-
const isRasterSource = (sourcePath) => rasterImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
364
|
+
const previousManifest = await readManifest();
|
|
365
|
+
const sources = await getReferencedSources();
|
|
366
|
+
const manifest = {};
|
|
367
|
+
const isStaticSource = (sourcePath) => staticImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
368
|
+
const isRasterSource = (sourcePath) => rasterImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
359
369
|
|
|
360
|
-
if (sources.length === 0) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
370
|
+
if (sources.length === 0) {
|
|
371
|
+
await removeUnreferencedGeneratedFiles(manifest);
|
|
372
|
+
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
373
|
+
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const imageMagick = sources.some(({ sourcePath }) => isRasterSource(sourcePath))
|
|
378
|
+
? await getImageMagick()
|
|
379
|
+
: null;
|
|
366
380
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
: null;
|
|
381
|
+
for (const { sourcePath, sourceKey } of sources) {
|
|
382
|
+
const sourceHash = await getSourceHash(sourcePath);
|
|
370
383
|
|
|
371
|
-
|
|
372
|
-
|
|
384
|
+
if (isStaticSource(sourcePath)) {
|
|
385
|
+
const reusableEntry = await getReusableStaticEntry(previousManifest[sourceKey], sourceHash, sourceKey);
|
|
373
386
|
|
|
374
|
-
|
|
375
|
-
|
|
387
|
+
if (reusableEntry) {
|
|
388
|
+
manifest[sourceKey] = reusableEntry;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const outputPath = getOriginalPath(sourceKey, sourceHash);
|
|
393
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
394
|
+
await copyFile(sourcePath, outputPath);
|
|
395
|
+
|
|
396
|
+
const dimensions = await readImageDimensions(sourcePath).catch(() => null);
|
|
397
|
+
manifest[sourceKey] = {
|
|
398
|
+
kind: 'static',
|
|
399
|
+
outputVersion: imageOutputVersion,
|
|
400
|
+
sourceHash,
|
|
401
|
+
src: getPublicPath(outputPath),
|
|
402
|
+
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
403
|
+
};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const reusableEntry = await getReusableEntry(sourceKey, previousManifest[sourceKey], sourceHash);
|
|
376
408
|
|
|
377
409
|
if (reusableEntry) {
|
|
378
410
|
manifest[sourceKey] = reusableEntry;
|
|
379
411
|
continue;
|
|
380
412
|
}
|
|
381
413
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
414
|
+
if (!imageMagick) {
|
|
415
|
+
throw new Error(`ImageMagick is missing for raster image: ${sourcePath}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const dimensions = await identify(sourcePath, imageMagick);
|
|
419
|
+
const variantWidths = getVariantWidths(dimensions);
|
|
420
|
+
const variants = getVariants(sourceKey, sourceHash, variantWidths);
|
|
421
|
+
for (const width of variantWidths) {
|
|
422
|
+
const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
|
|
423
|
+
await convert(sourcePath, outputPath, width, imageMagick);
|
|
424
|
+
}
|
|
385
425
|
|
|
386
|
-
const dimensions = await readImageDimensions(sourcePath).catch(() => null);
|
|
387
426
|
manifest[sourceKey] = {
|
|
388
|
-
kind: 'static',
|
|
389
427
|
outputVersion: imageOutputVersion,
|
|
390
428
|
sourceHash,
|
|
391
|
-
|
|
392
|
-
|
|
429
|
+
width: dimensions.width,
|
|
430
|
+
height: dimensions.height,
|
|
431
|
+
variants,
|
|
393
432
|
};
|
|
394
|
-
continue;
|
|
395
433
|
}
|
|
396
434
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!imageMagick) {
|
|
405
|
-
throw new Error(`ImageMagick is missing for raster image: ${sourcePath}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const dimensions = await identify(sourcePath);
|
|
409
|
-
const variantWidths = getVariantWidths(dimensions);
|
|
410
|
-
const variants = getVariants(sourceKey, sourceHash, variantWidths);
|
|
411
|
-
for (const width of variantWidths) {
|
|
412
|
-
const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
|
|
413
|
-
await convert(sourcePath, outputPath, width);
|
|
414
|
-
}
|
|
435
|
+
await validateCarouselOrientations(manifest);
|
|
436
|
+
await removeUnreferencedGeneratedFiles(manifest);
|
|
437
|
+
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
438
|
+
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
439
|
+
};
|
|
415
440
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
variants,
|
|
422
|
-
};
|
|
441
|
+
try {
|
|
442
|
+
await main();
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error(error.message);
|
|
445
|
+
process.exitCode = 1;
|
|
423
446
|
}
|
|
424
|
-
|
|
425
|
-
await validateCarouselOrientations(manifest);
|
|
426
|
-
await removeUnreferencedGeneratedFiles(manifest);
|
|
427
|
-
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
428
|
-
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { searchOutputDirectory } from './lib/publication-paths.mjs';
|
|
1
2
|
import { cp, mkdir, readdir, rm } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import * as pagefind from 'pagefind';
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
astroPublicDir,
|
|
8
9
|
} from './lib/site-paths.mjs';
|
|
9
10
|
|
|
10
|
-
const searchDirectoryName =
|
|
11
|
+
const searchDirectoryName = searchOutputDirectory;
|
|
11
12
|
const distSearchDirectory = path.join(astroDistDir, searchDirectoryName);
|
|
12
13
|
const localSearchDirectory = path.join(astroPublicDir, searchDirectoryName);
|
|
13
14
|
|
|
@@ -44,7 +45,7 @@ if (!projectConfig.search.enabled) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
await rm(localSearchDirectory, { force: true, recursive: true });
|
|
47
|
-
await mkdir(
|
|
48
|
+
await mkdir(path.dirname(localSearchDirectory), { recursive: true });
|
|
48
49
|
await cp(distSearchDirectory, localSearchDirectory, { recursive: true });
|
|
49
50
|
|
|
50
51
|
console.log(`Generated static search index for ${indexedPageCount} page${indexedPageCount === 1 ? '' : 's'}.`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { getSourcePageLocation } from './source-files.mjs';
|
|
4
|
+
import { parsePageMarkdownSource } from './page-markdown.mjs';
|
|
5
|
+
import { readPageAttachments, resolveAttachment, attachmentHref } from './page-attachments.mjs';
|
|
6
|
+
|
|
7
|
+
export const rewriteAttachmentLinks = async (source, fileURL, siteRoot) => {
|
|
8
|
+
if (!fileURL) return source;
|
|
9
|
+
const contentPath = fileURLToPath(fileURL);
|
|
10
|
+
const location = getSourcePageLocation(siteRoot, path.dirname(contentPath));
|
|
11
|
+
if (!location || path.basename(contentPath) !== 'content.md') return source;
|
|
12
|
+
const { files } = await readPageAttachments({ ...location, contentPath });
|
|
13
|
+
if (!files.length) return source;
|
|
14
|
+
const document = await parsePageMarkdownSource(source);
|
|
15
|
+
const edits = new Map();
|
|
16
|
+
for (const link of document.links) {
|
|
17
|
+
const file = resolveAttachment(link.target, location.isHome ? '/' : `/${location.pagePath}/`, files);
|
|
18
|
+
if (!file || !link.targetRange) continue;
|
|
19
|
+
const href = attachmentHref(file, link.target);
|
|
20
|
+
edits.set(link.targetRange.start, { ...link.targetRange, text: link.yamlScalar ? JSON.stringify(href) : href });
|
|
21
|
+
}
|
|
22
|
+
return [...edits.values()].sort((a,b) => b.start-a.start).reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source);
|
|
23
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export const identityPath = '/.well-known/norna-dev';
|
|
4
|
+
export const readDevServerIdentity = async (state, siteRoot) => {
|
|
5
|
+
if (!state?.token || !Number.isInteger(state.port) || state.port < 1 || state.port > 65535) return null;
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(`http://127.0.0.1:${state.port}${identityPath}`, { signal: AbortSignal.timeout(1500), redirect: 'error' });
|
|
8
|
+
if (!response.ok) return null;
|
|
9
|
+
const identity = await response.json();
|
|
10
|
+
return identity.token === state.token && Number.isInteger(identity.pid) && identity.pid > 0 && identity.siteRoot === await realpath(siteRoot) ? identity : null;
|
|
11
|
+
} catch { return null; }
|
|
12
|
+
};
|
|
13
|
+
export const verifyDevServer = async (state, siteRoot) => Number.isInteger(state?.pid) && state.pid > 0
|
|
14
|
+
&& (await readDevServerIdentity(state, siteRoot))?.pid === state.pid;
|
|
15
|
+
|
|
16
|
+
export const devServerIdentityPlugin = siteRoot => ({
|
|
17
|
+
name: 'norna-dev-server-identity',
|
|
18
|
+
async configureServer(server) {
|
|
19
|
+
const canonicalRoot = await realpath(siteRoot);
|
|
20
|
+
return () => {
|
|
21
|
+
server.middlewares.stack.unshift({ route: '', handle(request, response, next) {
|
|
22
|
+
if (request.url !== identityPath || request.method !== 'GET' || !process.env.NORNA_DEV_TOKEN) return next();
|
|
23
|
+
if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(request.socket.remoteAddress)) return next();
|
|
24
|
+
response.setHeader('Content-Type', 'application/json');
|
|
25
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
26
|
+
response.end(JSON.stringify({ siteRoot: canonicalRoot, token: process.env.NORNA_DEV_TOKEN, pid: process.pid }));
|
|
27
|
+
} });
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|