@glw907/cairn-cms 0.24.0 → 0.26.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 +87 -0
- package/README.md +50 -37
- package/dist/content/compose.d.ts +15 -4
- package/dist/content/compose.d.ts.map +1 -1
- package/dist/content/compose.js +9 -4
- package/dist/content/manifest.d.ts +20 -3
- package/dist/content/manifest.d.ts.map +1 -1
- package/dist/content/manifest.js +49 -6
- package/dist/content/validate.d.ts +4 -1
- package/dist/content/validate.d.ts.map +1 -1
- package/dist/content/validate.js +4 -1
- package/dist/delivery/content-index.d.ts +4 -0
- package/dist/delivery/content-index.d.ts.map +1 -1
- package/dist/delivery/data.d.ts +24 -0
- package/dist/delivery/data.d.ts.map +1 -0
- package/dist/delivery/data.js +18 -0
- package/dist/delivery/index.d.ts +1 -23
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +5 -20
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/render/pipeline.d.ts +2 -2
- package/dist/render/pipeline.d.ts.map +1 -1
- package/dist/render/pipeline.js +2 -1
- package/dist/vite/bin.d.ts +3 -0
- package/dist/vite/bin.d.ts.map +1 -0
- package/dist/vite/bin.js +9 -0
- package/dist/vite/index.d.ts +33 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +178 -0
- package/package.json +21 -4
- package/src/lib/content/compose.ts +18 -9
- package/src/lib/content/manifest.ts +63 -7
- package/src/lib/content/validate.ts +4 -1
- package/src/lib/delivery/content-index.ts +4 -0
- package/src/lib/delivery/data.ts +26 -0
- package/src/lib/delivery/index.ts +5 -28
- package/src/lib/index.ts +3 -1
- package/src/lib/render/pipeline.ts +5 -2
- package/src/lib/vite/bin.ts +10 -0
- package/src/lib/vite/index.ts +213 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PluggableList } from 'unified';
|
|
2
2
|
import type { Schema } from 'hast-util-sanitize';
|
|
3
|
-
import type
|
|
3
|
+
import { type ComponentRegistry } from './registry.js';
|
|
4
4
|
import type { LinkResolve } from '../content/links.js';
|
|
5
5
|
export interface RendererOptions {
|
|
6
6
|
/** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
@@ -24,7 +24,7 @@ export interface RendererOptions {
|
|
|
24
24
|
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
25
25
|
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
26
26
|
* rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
|
|
27
|
-
export declare function createRenderer(registry
|
|
27
|
+
export declare function createRenderer(registry?: ComponentRegistry, options?: RendererOptions): {
|
|
28
28
|
remarkPlugins: PluggableList;
|
|
29
29
|
rehypePlugins: PluggableList;
|
|
30
30
|
renderMarkdown: (content: string, opts?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAStD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAMjD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAStD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAMjD,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;wCAGoC;IACpC,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C;;+EAE2E;IAC3E,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;+FAE2F;IAC3F,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,iBAAsD,EAChE,OAAO,GAAE,eAAoB;;;8BA2BK,MAAM,SAAQ;QAAE,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,KAAQ,OAAO,CAAC,MAAM,CAAC;EAKjG"}
|
package/dist/render/pipeline.js
CHANGED
|
@@ -12,10 +12,11 @@ import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
|
|
|
12
12
|
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
13
13
|
import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
|
|
14
14
|
import { rehypeDispatch } from './rehype-dispatch.js';
|
|
15
|
+
import { defineRegistry } from './registry.js';
|
|
15
16
|
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
16
17
|
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
17
18
|
* rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
|
|
18
|
-
export function createRenderer(registry, options = {}) {
|
|
19
|
+
export function createRenderer(registry = defineRegistry({ components: [] }), options = {}) {
|
|
19
20
|
const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry], remarkResolveCairnLinks];
|
|
20
21
|
// The sanitize floor runs after rehype-raw (so author raw HTML is parsed, then cleaned) and
|
|
21
22
|
// before the dispatch (so the site's trusted build() output and its inline SVG icons are never
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/lib/vite/bin.ts"],"names":[],"mappings":""}
|
package/dist/vite/bin.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cairn-manifest: the regenerate command. It evaluates the cairnManifest virtual module in write mode
|
|
3
|
+
// through the consumer's own Vite resolution and writes the canonical content manifest. A thin shell
|
|
4
|
+
// over writeManifest so the write logic stays testable apart from the CLI.
|
|
5
|
+
import { writeManifest } from './index.js';
|
|
6
|
+
writeManifest(process.cwd()).catch((err) => {
|
|
7
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Plugin, PluginOption } from 'vite';
|
|
2
|
+
/** Options for {@link cairnManifest}. Paths are app-root-absolute (the form `import.meta.glob` wants),
|
|
3
|
+
* so they match the build's own resolution. */
|
|
4
|
+
export interface CairnManifestOptions {
|
|
5
|
+
/** The module exporting the `cairn` adapter and the parsed `siteConfig`, app-root-absolute. */
|
|
6
|
+
configModule: string;
|
|
7
|
+
/** Per-concept content globs, keyed by concept id, app-root-absolute. */
|
|
8
|
+
content: Record<string, string>;
|
|
9
|
+
/** The committed manifest path, app-root-absolute. Defaults to `/src/content/.cairn/index.json`. */
|
|
10
|
+
manifestPath?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Flatten the consumer's plugins option and drop the cairnManifest plugin at any nesting depth, so
|
|
13
|
+
* the nested verify server can never re-enter its buildStart. Vite supports (and flattens) nested
|
|
14
|
+
* plugin arrays, and findCairnOptions recurses into them, so a flat single-level filter would miss a
|
|
15
|
+
* cairnManifest nested inside a shared preset's sub-array and let it survive into the nested server.
|
|
16
|
+
* This mirrors findCairnOptions's recursion. Falsy slots pass through, which Vite tolerates. */
|
|
17
|
+
export declare function stripCairnManifest(plugins: PluginOption | PluginOption[]): PluginOption[];
|
|
18
|
+
/** Verify the committed manifest against the corpus from a Vite context, throwing on drift. The bin
|
|
19
|
+
* and the plugin share this; the spike proved it runs cleanly inside the consumer's config. */
|
|
20
|
+
export declare function verifyManifestFromVite(opts: CairnManifestOptions, root: string): Promise<void>;
|
|
21
|
+
/** Regenerate the serialized manifest from the corpus in a Vite context, sharing the build's
|
|
22
|
+
* resolution. The cairn-manifest bin (a later task) will call this and write the result. */
|
|
23
|
+
export declare function buildManifestFromVite(opts: CairnManifestOptions, root: string): Promise<string>;
|
|
24
|
+
/** The cairnManifest plugin. It serves the verify virtual module to the app graph and, in
|
|
25
|
+
* buildStart, evaluates it through a nested Vite SSR load so a manifest drift fails the build. */
|
|
26
|
+
export declare function cairnManifest(opts: CairnManifestOptions): Plugin;
|
|
27
|
+
/** Regenerate the committed manifest from the consumer's corpus and write it to the configured
|
|
28
|
+
* manifestPath. It loads the consumer's Vite config from `cwd`, reads the cairnManifest plugin's
|
|
29
|
+
* options off the instance, evaluates the write-mode virtual module through the build's own
|
|
30
|
+
* resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
|
|
31
|
+
* so the write logic is testable apart from the CLI shell. */
|
|
32
|
+
export declare function writeManifest(cwd?: string): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/vite/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAWjD;gDACgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,oGAAoG;IACpG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA4ED;;;;iGAIiG;AACjG,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,EAAE,GAAG,YAAY,EAAE,CAIzF;AAED;gGACgG;AAChG,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpG;AAED;6FAC6F;AAC7F,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAErG;AAED;mGACmG;AACnG,wBAAgB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CA2BhE;AAED;;;;+DAI+D;AAC/D,wBAAsB,aAAa,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB9E"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/** The key the cairnManifest plugin stashes its options under, so the write path can read them off the
|
|
4
|
+
* plugin instance in the consumer's loaded config without re-parsing the config file. */
|
|
5
|
+
const CAIRN_OPTIONS = Symbol.for('cairn-cms.manifest-options');
|
|
6
|
+
const VIRTUAL_ID = 'virtual:cairn-manifest';
|
|
7
|
+
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
8
|
+
/** The default committed manifest path, app-root-absolute. */
|
|
9
|
+
const DEFAULT_MANIFEST_PATH = '/src/content/.cairn/index.json';
|
|
10
|
+
/** Build the virtual module source. In verify mode it throws on drift; in write mode it exports the
|
|
11
|
+
* serialized manifest as `result`. The module runs in the app graph, so its `import.meta.glob`,
|
|
12
|
+
* package, and `?raw` resolution is the build's own. */
|
|
13
|
+
function virtualSource(opts, mode) {
|
|
14
|
+
const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
|
|
15
|
+
const globEntries = Object.entries(opts.content)
|
|
16
|
+
.map(([id, pattern]) => ` ${JSON.stringify(id)}: import.meta.glob(${JSON.stringify(pattern)}, { query: '?raw', import: 'default', eager: true }),`)
|
|
17
|
+
.join('\n');
|
|
18
|
+
// In write mode the committed file may not exist yet, so do not import it.
|
|
19
|
+
const committedImport = mode === 'verify' ? `import committed from ${JSON.stringify(manifestPath + '?raw')};` : '';
|
|
20
|
+
const resultExpr = mode === 'write' ? 'serializeManifest(built)' : '(verifyManifest(built, committed), "ok")';
|
|
21
|
+
return `
|
|
22
|
+
import { buildSiteManifest } from '@glw907/cairn-cms/delivery/data';
|
|
23
|
+
import { serializeManifest, verifyManifest } from '@glw907/cairn-cms';
|
|
24
|
+
import { cairn, siteConfig } from ${JSON.stringify(opts.configModule)};
|
|
25
|
+
${committedImport}
|
|
26
|
+
const globs = {
|
|
27
|
+
${globEntries}
|
|
28
|
+
};
|
|
29
|
+
const built = buildSiteManifest(cairn, siteConfig, globs);
|
|
30
|
+
export const result = ${resultExpr};
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
/** Evaluate the virtual module in the given mode inside the consumer's own Vite resolution, then
|
|
34
|
+
* return the module's `result`. It reuses the consumer's loaded config (so `$lib`, the config
|
|
35
|
+
* module, `import.meta.glob`, and `?raw` resolve exactly as the build does) and strips the
|
|
36
|
+
* cairnManifest plugin from the nested server's plugin list, so its buildStart never recurses.
|
|
37
|
+
* This runs at build time and in the bin, never in the request lifecycle. */
|
|
38
|
+
async function evalVirtual(opts, mode, root) {
|
|
39
|
+
const { createServer, loadConfigFromFile } = await import('vite');
|
|
40
|
+
// Load the consumer's real Vite config so the nested server inherits SvelteKit's resolution
|
|
41
|
+
// (the $lib alias, the app root, the ?raw and import.meta.glob handling). Drop cairnManifest from
|
|
42
|
+
// it so the nested server's buildStart does not recurse, and add a plugin that serves only the
|
|
43
|
+
// virtual module in the requested mode.
|
|
44
|
+
const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, root);
|
|
45
|
+
const inlineConfig = loaded?.config ?? {};
|
|
46
|
+
const server = await createServer({
|
|
47
|
+
...inlineConfig,
|
|
48
|
+
root,
|
|
49
|
+
configFile: false,
|
|
50
|
+
logLevel: 'silent',
|
|
51
|
+
server: { middlewareMode: true, hmr: false, watch: null },
|
|
52
|
+
plugins: [...stripCairnManifest(inlineConfig.plugins ?? []), cairnVirtualOnly(opts, mode)],
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const mod = (await server.ssrLoadModule(VIRTUAL_ID));
|
|
56
|
+
return mod.result;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await server.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** True for any plugin object whose name is the cairnManifest plugin, so the nested server drops it
|
|
63
|
+
* and cannot recurse into another buildStart. The consumer's plugin list may nest arrays and hold
|
|
64
|
+
* falsy slots, so guard the shape. */
|
|
65
|
+
function isCairnManifestPlugin(p) {
|
|
66
|
+
return !!p && typeof p === 'object' && 'name' in p && p.name === 'cairn-manifest';
|
|
67
|
+
}
|
|
68
|
+
/** Flatten the consumer's plugins option and drop the cairnManifest plugin at any nesting depth, so
|
|
69
|
+
* the nested verify server can never re-enter its buildStart. Vite supports (and flattens) nested
|
|
70
|
+
* plugin arrays, and findCairnOptions recurses into them, so a flat single-level filter would miss a
|
|
71
|
+
* cairnManifest nested inside a shared preset's sub-array and let it survive into the nested server.
|
|
72
|
+
* This mirrors findCairnOptions's recursion. Falsy slots pass through, which Vite tolerates. */
|
|
73
|
+
export function stripCairnManifest(plugins) {
|
|
74
|
+
if (Array.isArray(plugins))
|
|
75
|
+
return plugins.flatMap(stripCairnManifest);
|
|
76
|
+
if (isCairnManifestPlugin(plugins))
|
|
77
|
+
return [];
|
|
78
|
+
return [plugins];
|
|
79
|
+
}
|
|
80
|
+
/** Verify the committed manifest against the corpus from a Vite context, throwing on drift. The bin
|
|
81
|
+
* and the plugin share this; the spike proved it runs cleanly inside the consumer's config. */
|
|
82
|
+
export async function verifyManifestFromVite(opts, root) {
|
|
83
|
+
await evalVirtual(opts, 'verify', root);
|
|
84
|
+
}
|
|
85
|
+
/** Regenerate the serialized manifest from the corpus in a Vite context, sharing the build's
|
|
86
|
+
* resolution. The cairn-manifest bin (a later task) will call this and write the result. */
|
|
87
|
+
export async function buildManifestFromVite(opts, root) {
|
|
88
|
+
return evalVirtual(opts, 'write', root);
|
|
89
|
+
}
|
|
90
|
+
/** The cairnManifest plugin. It serves the verify virtual module to the app graph and, in
|
|
91
|
+
* buildStart, evaluates it through a nested Vite SSR load so a manifest drift fails the build. */
|
|
92
|
+
export function cairnManifest(opts) {
|
|
93
|
+
let root = process.cwd();
|
|
94
|
+
const plugin = {
|
|
95
|
+
name: 'cairn-manifest',
|
|
96
|
+
configResolved(config) {
|
|
97
|
+
// Capture the resolved app root so the nested server loads the same config the build did.
|
|
98
|
+
root = config.root;
|
|
99
|
+
},
|
|
100
|
+
resolveId(id) {
|
|
101
|
+
if (id === VIRTUAL_ID)
|
|
102
|
+
return RESOLVED_ID;
|
|
103
|
+
},
|
|
104
|
+
load(id) {
|
|
105
|
+
if (id === RESOLVED_ID)
|
|
106
|
+
return virtualSource(opts, 'verify');
|
|
107
|
+
},
|
|
108
|
+
async buildStart() {
|
|
109
|
+
try {
|
|
110
|
+
await verifyManifestFromVite(opts, root);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.error(err instanceof Error ? err.message : String(err));
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
// Stash the options on the instance so the cairn-manifest bin's writeManifest can read the content
|
|
118
|
+
// globs, config module, and manifest path off the plugin in the consumer's loaded config, sharing
|
|
119
|
+
// exactly the options the build verifies with.
|
|
120
|
+
plugin[CAIRN_OPTIONS] = opts;
|
|
121
|
+
return plugin;
|
|
122
|
+
}
|
|
123
|
+
/** Regenerate the committed manifest from the consumer's corpus and write it to the configured
|
|
124
|
+
* manifestPath. It loads the consumer's Vite config from `cwd`, reads the cairnManifest plugin's
|
|
125
|
+
* options off the instance, evaluates the write-mode virtual module through the build's own
|
|
126
|
+
* resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
|
|
127
|
+
* so the write logic is testable apart from the CLI shell. */
|
|
128
|
+
export async function writeManifest(cwd = process.cwd()) {
|
|
129
|
+
const { loadConfigFromFile } = await import('vite');
|
|
130
|
+
const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, cwd);
|
|
131
|
+
if (!loaded) {
|
|
132
|
+
throw new Error(`cairn-manifest: no Vite config found in ${cwd}`);
|
|
133
|
+
}
|
|
134
|
+
const opts = findCairnOptions(loaded.config.plugins);
|
|
135
|
+
if (!opts) {
|
|
136
|
+
throw new Error('cairn-manifest: the Vite config has no cairnManifest() plugin. Add it so the bin shares the build options.');
|
|
137
|
+
}
|
|
138
|
+
const serialized = await buildManifestFromVite(opts, cwd);
|
|
139
|
+
const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
|
|
140
|
+
// The manifest path is app-root-absolute (a leading slash relative to the project), so resolve it
|
|
141
|
+
// against cwd, not the filesystem root.
|
|
142
|
+
const outPath = join(cwd, manifestPath.replace(/^\//, ''));
|
|
143
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
144
|
+
await writeFile(outPath, serialized);
|
|
145
|
+
}
|
|
146
|
+
/** Walk a Vite plugins option (which may nest arrays, hold falsy slots, or be a thenable) and return
|
|
147
|
+
* the stashed cairnManifest options from the first matching plugin, or null if there is none. */
|
|
148
|
+
function findCairnOptions(plugins) {
|
|
149
|
+
if (!plugins)
|
|
150
|
+
return null;
|
|
151
|
+
if (Array.isArray(plugins)) {
|
|
152
|
+
for (const p of plugins) {
|
|
153
|
+
const found = findCairnOptions(p);
|
|
154
|
+
if (found)
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (typeof plugins === 'object' && CAIRN_OPTIONS in plugins) {
|
|
160
|
+
return plugins[CAIRN_OPTIONS] ?? null;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/** A minimal plugin that serves only the virtual module in one mode, for the nested SSR load. It
|
|
165
|
+
* carries no buildStart, so the nested server never recurses into the verify. */
|
|
166
|
+
function cairnVirtualOnly(opts, mode) {
|
|
167
|
+
return {
|
|
168
|
+
name: 'cairn-manifest-virtual',
|
|
169
|
+
resolveId(id) {
|
|
170
|
+
if (id === VIRTUAL_ID)
|
|
171
|
+
return RESOLVED_ID;
|
|
172
|
+
},
|
|
173
|
+
load(id) {
|
|
174
|
+
if (id === RESOLVED_ID)
|
|
175
|
+
return virtualSource(opts, mode);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/glw907/cairn-cms.git"
|
|
15
15
|
},
|
|
16
|
+
"homepage": "https://github.com/glw907/cairn-cms#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/glw907/cairn-cms/issues"
|
|
19
|
+
},
|
|
16
20
|
"keywords": [
|
|
17
21
|
"cms",
|
|
18
22
|
"sveltekit",
|
|
@@ -22,9 +26,9 @@
|
|
|
22
26
|
"markdown"
|
|
23
27
|
],
|
|
24
28
|
"scripts": {
|
|
25
|
-
"package": "svelte-package",
|
|
29
|
+
"package": "svelte-package && chmod +x dist/vite/bin.js",
|
|
26
30
|
"check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
|
|
27
|
-
"prepare": "
|
|
31
|
+
"prepare": "npm run package",
|
|
28
32
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
29
33
|
"test": "vitest run",
|
|
30
34
|
"test:watch": "vitest",
|
|
@@ -58,11 +62,24 @@
|
|
|
58
62
|
"svelte": "./dist/delivery/head.js",
|
|
59
63
|
"default": "./dist/delivery/head.js"
|
|
60
64
|
},
|
|
65
|
+
"./delivery/data": {
|
|
66
|
+
"types": "./dist/delivery/data.d.ts",
|
|
67
|
+
"svelte": "./dist/delivery/data.js",
|
|
68
|
+
"default": "./dist/delivery/data.js"
|
|
69
|
+
},
|
|
70
|
+
"./vite": {
|
|
71
|
+
"types": "./dist/vite/index.d.ts",
|
|
72
|
+
"default": "./dist/vite/index.js"
|
|
73
|
+
},
|
|
61
74
|
"./package.json": "./package.json"
|
|
62
75
|
},
|
|
76
|
+
"bin": {
|
|
77
|
+
"cairn-manifest": "./dist/vite/bin.js"
|
|
78
|
+
},
|
|
63
79
|
"files": [
|
|
64
80
|
"dist",
|
|
65
|
-
"src/lib"
|
|
81
|
+
"src/lib",
|
|
82
|
+
"CHANGELOG.md"
|
|
66
83
|
],
|
|
67
84
|
"peerDependencies": {
|
|
68
85
|
"@sveltejs/kit": "^2",
|
|
@@ -3,18 +3,27 @@
|
|
|
3
3
|
// the same way and contributes the same kinds of things: nav entries, route logic,
|
|
4
4
|
// concepts, components, field types, and save hooks. Shaped now so the extension contract
|
|
5
5
|
// is additive later.
|
|
6
|
-
import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig,
|
|
6
|
+
import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig, FieldTypeDef } from './types.js';
|
|
7
7
|
import { normalizeConcepts } from './concepts.js';
|
|
8
|
+
import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
|
|
9
|
+
|
|
10
|
+
/** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
11
|
+
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
12
|
+
* adapter's concepts. */
|
|
13
|
+
export interface ComposeInput {
|
|
14
|
+
adapter: CairnAdapter;
|
|
15
|
+
siteConfig: SiteConfig;
|
|
16
|
+
extensions?: CairnExtension[];
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
11
|
-
*
|
|
20
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
|
|
21
|
+
* is derived from the site config, the same source the delivery path uses, so the runtime and
|
|
22
|
+
* delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
|
|
23
|
+
* (seam 4) passes through untouched.
|
|
12
24
|
*/
|
|
13
|
-
export function composeRuntime(
|
|
14
|
-
|
|
15
|
-
extensions: CairnExtension[] = [],
|
|
16
|
-
urlPolicy: Record<string, ConceptUrlPolicy | undefined> = {},
|
|
17
|
-
): CairnRuntime {
|
|
25
|
+
export function composeRuntime({ adapter, siteConfig, extensions = [] }: ComposeInput): CairnRuntime {
|
|
26
|
+
if (!siteConfig) throw new Error('composeRuntime needs a site config to derive the URL policy');
|
|
18
27
|
const content: Record<string, ConceptConfig | undefined> = { ...adapter.content };
|
|
19
28
|
const adminPanels: AdminPanel[] = [];
|
|
20
29
|
const fieldTypes: FieldTypeDef[] = [];
|
|
@@ -27,7 +36,7 @@ export function composeRuntime(
|
|
|
27
36
|
}
|
|
28
37
|
return {
|
|
29
38
|
siteName: adapter.siteName,
|
|
30
|
-
concepts: normalizeConcepts(content,
|
|
39
|
+
concepts: normalizeConcepts(content, urlPolicyFrom(siteConfig)),
|
|
31
40
|
backend: adapter.backend,
|
|
32
41
|
sender: adapter.sender,
|
|
33
42
|
render: adapter.render,
|
|
@@ -140,15 +140,71 @@ export function parseManifest(raw: string): Manifest {
|
|
|
140
140
|
return { version: 1, entries: obj.entries as ManifestEntry[] };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
/** A changed entry and the fields that differ between the built and committed manifests. */
|
|
144
|
+
export interface ManifestEntryDiff {
|
|
145
|
+
concept: string;
|
|
146
|
+
id: string;
|
|
147
|
+
fields: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The drift between a freshly built manifest and the committed one, keyed by concept+id. */
|
|
151
|
+
export interface ManifestDiff {
|
|
152
|
+
added: ManifestEntry[];
|
|
153
|
+
removed: ManifestEntry[];
|
|
154
|
+
changed: ManifestEntryDiff[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const keyOf = (e: ManifestEntry) => `${e.concept}/${e.id}`;
|
|
158
|
+
|
|
159
|
+
/** Compare a built manifest against a committed one, keyed by concept+id (the same identity
|
|
160
|
+
* upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
|
|
161
|
+
* unit-tested apart from any build. */
|
|
162
|
+
export function diffManifests(built: Manifest, committed: Manifest): ManifestDiff {
|
|
163
|
+
const builtByKey = new Map(built.entries.map((e) => [keyOf(e), e]));
|
|
164
|
+
const committedByKey = new Map(committed.entries.map((e) => [keyOf(e), e]));
|
|
165
|
+
const added = built.entries.filter((e) => !committedByKey.has(keyOf(e)));
|
|
166
|
+
const removed = committed.entries.filter((e) => !builtByKey.has(keyOf(e)));
|
|
167
|
+
const changed: ManifestEntryDiff[] = [];
|
|
168
|
+
for (const b of built.entries) {
|
|
169
|
+
const c = committedByKey.get(keyOf(b));
|
|
170
|
+
if (!c) continue;
|
|
171
|
+
// ManifestEntry has no index signature, so read its keys through an unknown-cast record.
|
|
172
|
+
const br = b as unknown as Record<string, unknown>;
|
|
173
|
+
const cr = c as unknown as Record<string, unknown>;
|
|
174
|
+
const fields = [...new Set([...Object.keys(b), ...Object.keys(c)])].filter(
|
|
175
|
+
(k) => JSON.stringify(br[k]) !== JSON.stringify(cr[k]),
|
|
150
176
|
);
|
|
177
|
+
if (fields.length > 0) changed.push({ concept: b.concept, id: b.id, fields });
|
|
151
178
|
}
|
|
179
|
+
return { added, removed, changed };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Format a diff into a short human-readable block for a build error. */
|
|
183
|
+
function formatDiff(d: ManifestDiff): string {
|
|
184
|
+
const lines: string[] = [];
|
|
185
|
+
for (const e of d.added) lines.push(` + ${keyOf(e)}`);
|
|
186
|
+
for (const e of d.removed) lines.push(` - ${keyOf(e)}`);
|
|
187
|
+
for (const e of d.changed) lines.push(` ~ ${e.concept}/${e.id} (${e.fields.join(', ')})`);
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
|
|
192
|
+
* is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
|
|
193
|
+
* error names the added, removed, and changed entries, so a raw-git content edit that leaves the
|
|
194
|
+
* committed manifest stale fails the build loudly with what drifted. */
|
|
195
|
+
export function verifyManifest(built: Manifest, committedRaw: string): void {
|
|
196
|
+
const builtRaw = serializeManifest(built);
|
|
197
|
+
if (committedRaw === builtRaw) return;
|
|
198
|
+
// Diff the canonical built form, not the raw one. serializeManifest sorts each entry's links, so a
|
|
199
|
+
// build whose links are in extraction order would otherwise report a false (links) drift for an
|
|
200
|
+
// entry whose link set is identical and only the order differs. Reuse the serialized form so both
|
|
201
|
+
// sides are canonical.
|
|
202
|
+
const diff = diffManifests(parseManifest(builtRaw), parseManifest(committedRaw));
|
|
203
|
+
throw new Error(
|
|
204
|
+
'content manifest is stale: the committed file does not match the corpus.\n' +
|
|
205
|
+
formatDiff(diff) +
|
|
206
|
+
'\nRegenerate it (npm run cairn:manifest) and commit the result.',
|
|
207
|
+
);
|
|
152
208
|
}
|
|
153
209
|
|
|
154
210
|
/** Replace the entry with the same concept and id, or add it. Order does not matter, since
|
|
@@ -9,7 +9,10 @@ import { dateInputValue, isCalendarDate } from './frontmatter.js';
|
|
|
9
9
|
* Validate raw frontmatter against a field list. Required text and date fields must be
|
|
10
10
|
* non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
|
|
11
11
|
* and an unchecked one is omitted; a present tag field coerces to a string array and an empty
|
|
12
|
-
* one is omitted
|
|
12
|
+
* one is omitted, so validated data carries no key for an absent tag field (`tags` or `freetags`).
|
|
13
|
+
* The delivery read model
|
|
14
|
+
* (`ContentSummary.tags`) fills that case with an empty array; the two layers differ on purpose.
|
|
15
|
+
* An empty optional text or date field is omitted, so the normalized data
|
|
13
16
|
* carries only meaningful values and committed frontmatter stays minimal. Returns the
|
|
14
17
|
* normalized data, or field-keyed errors when any required field is empty.
|
|
15
18
|
*
|
|
@@ -25,6 +25,10 @@ export interface ContentSummary {
|
|
|
25
25
|
title: string;
|
|
26
26
|
date?: string;
|
|
27
27
|
updated?: string;
|
|
28
|
+
/** The entry's tags, always present as an array and empty when the file declares none. This is the
|
|
29
|
+
* read-model normalization. It differs on purpose from the validated `frontmatter.tags`, which the
|
|
30
|
+
* validator omits when empty, so a published file carries no `tags: []` noise. Read `tags` here for
|
|
31
|
+
* a list; read `frontmatter.tags` only when you need the validated, possibly-absent value. */
|
|
28
32
|
tags: string[];
|
|
29
33
|
excerpt: string;
|
|
30
34
|
wordCount: number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// cairn-cms: the node-safe delivery data surface (@glw907/cairn-cms/delivery/data). The pure corpus
|
|
2
|
+
// projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
|
|
3
|
+
// the graph. The full ./delivery barrel re-exports this and adds the route loaders.
|
|
4
|
+
export { createContentIndex, fromGlob } from './content-index.js';
|
|
5
|
+
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
6
|
+
export { createSiteIndex } from './site-index.js';
|
|
7
|
+
export type { SiteIndex, ConceptIndex } from './site-index.js';
|
|
8
|
+
export { createSiteIndexes } from './site-indexes.js';
|
|
9
|
+
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
10
|
+
export { siteDescriptors } from './site-descriptors.js';
|
|
11
|
+
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
12
|
+
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
13
|
+
export type { FeedChannel, FeedItem } from './feeds.js';
|
|
14
|
+
export { buildSitemap } from './sitemap.js';
|
|
15
|
+
export type { SitemapUrl } from './sitemap.js';
|
|
16
|
+
export { buildRobots } from './robots.js';
|
|
17
|
+
export { buildSeoMeta } from './seo.js';
|
|
18
|
+
export type { SeoInput, SeoMeta } from './seo.js';
|
|
19
|
+
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
20
|
+
export type { SeoFields } from './seo-fields.js';
|
|
21
|
+
export { paginate } from './paginate.js';
|
|
22
|
+
export type { Page } from './paginate.js';
|
|
23
|
+
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
24
|
+
export { jsonLdScript } from './json-ld.js';
|
|
25
|
+
export { permalink } from '../content/permalink.js';
|
|
26
|
+
export { buildSiteManifest, buildLinkResolver } from './manifest.js';
|
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
// cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
export { createContentIndex, fromGlob } from './content-index.js';
|
|
7
|
-
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
8
|
-
export { createSiteIndex } from './site-index.js';
|
|
9
|
-
export type { SiteIndex, ConceptIndex } from './site-index.js';
|
|
10
|
-
export { createSiteIndexes } from './site-indexes.js';
|
|
11
|
-
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
12
|
-
export { siteDescriptors } from './site-descriptors.js';
|
|
13
|
-
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
14
|
-
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
15
|
-
export type { FeedChannel, FeedItem } from './feeds.js';
|
|
16
|
-
export { buildSitemap } from './sitemap.js';
|
|
17
|
-
export type { SitemapUrl } from './sitemap.js';
|
|
18
|
-
export { buildRobots } from './robots.js';
|
|
19
|
-
export { buildSeoMeta } from './seo.js';
|
|
20
|
-
export type { SeoInput, SeoMeta } from './seo.js';
|
|
21
|
-
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
22
|
-
export type { SeoFields } from './seo-fields.js';
|
|
23
|
-
export { paginate } from './paginate.js';
|
|
24
|
-
export type { Page } from './paginate.js';
|
|
25
|
-
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
26
|
-
export { jsonLdScript } from './json-ld.js';
|
|
27
|
-
export { permalink } from '../content/permalink.js';
|
|
28
|
-
export { buildSiteManifest, buildLinkResolver } from './manifest.js';
|
|
1
|
+
// cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The node-safe data surface
|
|
2
|
+
// (re-exported from ./delivery/data) plus the SvelteKit catch-all route loaders. The head component
|
|
3
|
+
// lives at ./delivery/head. Importing this pulls @sveltejs/kit through the route loaders, so a
|
|
4
|
+
// plain-Node tool imports from ./delivery/data instead.
|
|
5
|
+
export * from './data.js';
|
|
29
6
|
export { createPublicRoutes } from '../sveltekit/public-routes.js';
|
|
30
7
|
export type {
|
|
31
8
|
PublicRoutesDeps,
|
package/src/lib/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type {
|
|
|
31
31
|
} from './content/types.js';
|
|
32
32
|
export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
|
|
33
33
|
export { composeRuntime } from './content/compose.js';
|
|
34
|
+
export type { ComposeInput } from './content/compose.js';
|
|
34
35
|
export {
|
|
35
36
|
frontmatterFromForm,
|
|
36
37
|
dateInputValue,
|
|
@@ -59,13 +60,14 @@ export {
|
|
|
59
60
|
parseManifest,
|
|
60
61
|
emptyManifest,
|
|
61
62
|
verifyManifest,
|
|
63
|
+
diffManifests,
|
|
62
64
|
upsertEntry,
|
|
63
65
|
removeEntry,
|
|
64
66
|
manifestEntryFromFile,
|
|
65
67
|
manifestLinkResolver,
|
|
66
68
|
inboundLinks,
|
|
67
69
|
} from './content/manifest.js';
|
|
68
|
-
export type { Manifest, ManifestEntry, LinkTarget, InboundLink } from './content/manifest.js';
|
|
70
|
+
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink } from './content/manifest.js';
|
|
69
71
|
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
70
72
|
export { defineRegistry, emptyValues } from './render/registry.js';
|
|
71
73
|
export type {
|
|
@@ -13,7 +13,7 @@ import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
|
|
|
13
13
|
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
14
14
|
import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
|
|
15
15
|
import { rehypeDispatch } from './rehype-dispatch.js';
|
|
16
|
-
import type
|
|
16
|
+
import { defineRegistry, type ComponentRegistry } from './registry.js';
|
|
17
17
|
import type { LinkResolve } from '../content/links.js';
|
|
18
18
|
|
|
19
19
|
export interface RendererOptions {
|
|
@@ -39,7 +39,10 @@ export interface RendererOptions {
|
|
|
39
39
|
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
40
40
|
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
41
41
|
* rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
|
|
42
|
-
export function createRenderer(
|
|
42
|
+
export function createRenderer(
|
|
43
|
+
registry: ComponentRegistry = defineRegistry({ components: [] }),
|
|
44
|
+
options: RendererOptions = {},
|
|
45
|
+
) {
|
|
43
46
|
const remarkPlugins: PluggableList = [remarkDirective, [remarkDirectiveStamp, registry], remarkResolveCairnLinks];
|
|
44
47
|
// The sanitize floor runs after rehype-raw (so author raw HTML is parsed, then cleaned) and
|
|
45
48
|
// before the dispatch (so the site's trusted build() output and its inline SVG icons are never
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cairn-manifest: the regenerate command. It evaluates the cairnManifest virtual module in write mode
|
|
3
|
+
// through the consumer's own Vite resolution and writes the canonical content manifest. A thin shell
|
|
4
|
+
// over writeManifest so the write logic stays testable apart from the CLI.
|
|
5
|
+
import { writeManifest } from './index.js';
|
|
6
|
+
|
|
7
|
+
writeManifest(process.cwd()).catch((err) => {
|
|
8
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|