@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// cairn-cms: the cairnManifest Vite plugin (@glw907/cairn-cms/vite). It owns a virtual module that
|
|
2
|
+
// runs import.meta.glob over the content dirs inside the app's own Vite graph, builds the manifest
|
|
3
|
+
// with the engine builder, and verifies it against the committed file. The verify runs in the
|
|
4
|
+
// plugin's buildStart hook through a nested Vite SSR module load, so a drift throws there and fails
|
|
5
|
+
// the build as a hard build error, outside the prerender request lifecycle (where handleHttpError
|
|
6
|
+
// could downgrade it). The same virtual module in write mode produces the serialized manifest, which
|
|
7
|
+
// the cairn-manifest bin uses to regenerate. See the design spec, locked decision 1.
|
|
8
|
+
import type { Plugin, PluginOption } from 'vite';
|
|
9
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/** The key the cairnManifest plugin stashes its options under, so the write path can read them off the
|
|
13
|
+
* plugin instance in the consumer's loaded config without re-parsing the config file. */
|
|
14
|
+
const CAIRN_OPTIONS = Symbol.for('cairn-cms.manifest-options');
|
|
15
|
+
|
|
16
|
+
/** A cairnManifest plugin instance with its options stashed for the write path to read. */
|
|
17
|
+
type CairnManifestPlugin = Plugin & { [CAIRN_OPTIONS]?: CairnManifestOptions };
|
|
18
|
+
|
|
19
|
+
/** Options for {@link cairnManifest}. Paths are app-root-absolute (the form `import.meta.glob` wants),
|
|
20
|
+
* so they match the build's own resolution. */
|
|
21
|
+
export interface CairnManifestOptions {
|
|
22
|
+
/** The module exporting the `cairn` adapter and the parsed `siteConfig`, app-root-absolute. */
|
|
23
|
+
configModule: string;
|
|
24
|
+
/** Per-concept content globs, keyed by concept id, app-root-absolute. */
|
|
25
|
+
content: Record<string, string>;
|
|
26
|
+
/** The committed manifest path, app-root-absolute. Defaults to `/src/content/.cairn/index.json`. */
|
|
27
|
+
manifestPath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VIRTUAL_ID = 'virtual:cairn-manifest';
|
|
31
|
+
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
32
|
+
|
|
33
|
+
/** The default committed manifest path, app-root-absolute. */
|
|
34
|
+
const DEFAULT_MANIFEST_PATH = '/src/content/.cairn/index.json';
|
|
35
|
+
|
|
36
|
+
/** Build the virtual module source. In verify mode it throws on drift; in write mode it exports the
|
|
37
|
+
* serialized manifest as `result`. The module runs in the app graph, so its `import.meta.glob`,
|
|
38
|
+
* package, and `?raw` resolution is the build's own. */
|
|
39
|
+
function virtualSource(opts: CairnManifestOptions, mode: 'verify' | 'write'): string {
|
|
40
|
+
const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
|
|
41
|
+
const globEntries = Object.entries(opts.content)
|
|
42
|
+
.map(
|
|
43
|
+
([id, pattern]) =>
|
|
44
|
+
` ${JSON.stringify(id)}: import.meta.glob(${JSON.stringify(pattern)}, { query: '?raw', import: 'default', eager: true }),`,
|
|
45
|
+
)
|
|
46
|
+
.join('\n');
|
|
47
|
+
// In write mode the committed file may not exist yet, so do not import it.
|
|
48
|
+
const committedImport = mode === 'verify' ? `import committed from ${JSON.stringify(manifestPath + '?raw')};` : '';
|
|
49
|
+
const resultExpr =
|
|
50
|
+
mode === 'write' ? 'serializeManifest(built)' : '(verifyManifest(built, committed), "ok")';
|
|
51
|
+
return `
|
|
52
|
+
import { buildSiteManifest } from '@glw907/cairn-cms/delivery/data';
|
|
53
|
+
import { serializeManifest, verifyManifest } from '@glw907/cairn-cms';
|
|
54
|
+
import { cairn, siteConfig } from ${JSON.stringify(opts.configModule)};
|
|
55
|
+
${committedImport}
|
|
56
|
+
const globs = {
|
|
57
|
+
${globEntries}
|
|
58
|
+
};
|
|
59
|
+
const built = buildSiteManifest(cairn, siteConfig, globs);
|
|
60
|
+
export const result = ${resultExpr};
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Evaluate the virtual module in the given mode inside the consumer's own Vite resolution, then
|
|
65
|
+
* return the module's `result`. It reuses the consumer's loaded config (so `$lib`, the config
|
|
66
|
+
* module, `import.meta.glob`, and `?raw` resolve exactly as the build does) and strips the
|
|
67
|
+
* cairnManifest plugin from the nested server's plugin list, so its buildStart never recurses.
|
|
68
|
+
* This runs at build time and in the bin, never in the request lifecycle. */
|
|
69
|
+
async function evalVirtual(
|
|
70
|
+
opts: CairnManifestOptions,
|
|
71
|
+
mode: 'verify' | 'write',
|
|
72
|
+
root: string,
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
const { createServer, loadConfigFromFile } = await import('vite');
|
|
75
|
+
// Load the consumer's real Vite config so the nested server inherits SvelteKit's resolution
|
|
76
|
+
// (the $lib alias, the app root, the ?raw and import.meta.glob handling). Drop cairnManifest from
|
|
77
|
+
// it so the nested server's buildStart does not recurse, and add a plugin that serves only the
|
|
78
|
+
// virtual module in the requested mode.
|
|
79
|
+
const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, root);
|
|
80
|
+
const inlineConfig = loaded?.config ?? {};
|
|
81
|
+
const server = await createServer({
|
|
82
|
+
...inlineConfig,
|
|
83
|
+
root,
|
|
84
|
+
configFile: false,
|
|
85
|
+
logLevel: 'silent',
|
|
86
|
+
server: { middlewareMode: true, hmr: false, watch: null },
|
|
87
|
+
plugins: [...stripCairnManifest(inlineConfig.plugins ?? []), cairnVirtualOnly(opts, mode)],
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
const mod = (await server.ssrLoadModule(VIRTUAL_ID)) as { result: string };
|
|
91
|
+
return mod.result;
|
|
92
|
+
} finally {
|
|
93
|
+
await server.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True for any plugin object whose name is the cairnManifest plugin, so the nested server drops it
|
|
98
|
+
* and cannot recurse into another buildStart. The consumer's plugin list may nest arrays and hold
|
|
99
|
+
* falsy slots, so guard the shape. */
|
|
100
|
+
function isCairnManifestPlugin(p: unknown): boolean {
|
|
101
|
+
return !!p && typeof p === 'object' && 'name' in p && (p as { name?: unknown }).name === 'cairn-manifest';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Flatten the consumer's plugins option and drop the cairnManifest plugin at any nesting depth, so
|
|
105
|
+
* the nested verify server can never re-enter its buildStart. Vite supports (and flattens) nested
|
|
106
|
+
* plugin arrays, and findCairnOptions recurses into them, so a flat single-level filter would miss a
|
|
107
|
+
* cairnManifest nested inside a shared preset's sub-array and let it survive into the nested server.
|
|
108
|
+
* This mirrors findCairnOptions's recursion. Falsy slots pass through, which Vite tolerates. */
|
|
109
|
+
export function stripCairnManifest(plugins: PluginOption | PluginOption[]): PluginOption[] {
|
|
110
|
+
if (Array.isArray(plugins)) return plugins.flatMap(stripCairnManifest);
|
|
111
|
+
if (isCairnManifestPlugin(plugins)) return [];
|
|
112
|
+
return [plugins];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Verify the committed manifest against the corpus from a Vite context, throwing on drift. The bin
|
|
116
|
+
* and the plugin share this; the spike proved it runs cleanly inside the consumer's config. */
|
|
117
|
+
export async function verifyManifestFromVite(opts: CairnManifestOptions, root: string): Promise<void> {
|
|
118
|
+
await evalVirtual(opts, 'verify', root);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Regenerate the serialized manifest from the corpus in a Vite context, sharing the build's
|
|
122
|
+
* resolution. The cairn-manifest bin (a later task) will call this and write the result. */
|
|
123
|
+
export async function buildManifestFromVite(opts: CairnManifestOptions, root: string): Promise<string> {
|
|
124
|
+
return evalVirtual(opts, 'write', root);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The cairnManifest plugin. It serves the verify virtual module to the app graph and, in
|
|
128
|
+
* buildStart, evaluates it through a nested Vite SSR load so a manifest drift fails the build. */
|
|
129
|
+
export function cairnManifest(opts: CairnManifestOptions): Plugin {
|
|
130
|
+
let root = process.cwd();
|
|
131
|
+
const plugin: CairnManifestPlugin = {
|
|
132
|
+
name: 'cairn-manifest',
|
|
133
|
+
configResolved(config) {
|
|
134
|
+
// Capture the resolved app root so the nested server loads the same config the build did.
|
|
135
|
+
root = config.root;
|
|
136
|
+
},
|
|
137
|
+
resolveId(id) {
|
|
138
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
139
|
+
},
|
|
140
|
+
load(id) {
|
|
141
|
+
if (id === RESOLVED_ID) return virtualSource(opts, 'verify');
|
|
142
|
+
},
|
|
143
|
+
async buildStart() {
|
|
144
|
+
try {
|
|
145
|
+
await verifyManifestFromVite(opts, root);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.error(err instanceof Error ? err.message : String(err));
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
// Stash the options on the instance so the cairn-manifest bin's writeManifest can read the content
|
|
152
|
+
// globs, config module, and manifest path off the plugin in the consumer's loaded config, sharing
|
|
153
|
+
// exactly the options the build verifies with.
|
|
154
|
+
plugin[CAIRN_OPTIONS] = opts;
|
|
155
|
+
return plugin;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Regenerate the committed manifest from the consumer's corpus and write it to the configured
|
|
159
|
+
* manifestPath. It loads the consumer's Vite config from `cwd`, reads the cairnManifest plugin's
|
|
160
|
+
* options off the instance, evaluates the write-mode virtual module through the build's own
|
|
161
|
+
* resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
|
|
162
|
+
* so the write logic is testable apart from the CLI shell. */
|
|
163
|
+
export async function writeManifest(cwd: string = process.cwd()): Promise<void> {
|
|
164
|
+
const { loadConfigFromFile } = await import('vite');
|
|
165
|
+
const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, cwd);
|
|
166
|
+
if (!loaded) {
|
|
167
|
+
throw new Error(`cairn-manifest: no Vite config found in ${cwd}`);
|
|
168
|
+
}
|
|
169
|
+
const opts = findCairnOptions(loaded.config.plugins);
|
|
170
|
+
if (!opts) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'cairn-manifest: the Vite config has no cairnManifest() plugin. Add it so the bin shares the build options.',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const serialized = await buildManifestFromVite(opts, cwd);
|
|
176
|
+
const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
|
|
177
|
+
// The manifest path is app-root-absolute (a leading slash relative to the project), so resolve it
|
|
178
|
+
// against cwd, not the filesystem root.
|
|
179
|
+
const outPath = join(cwd, manifestPath.replace(/^\//, ''));
|
|
180
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
181
|
+
await writeFile(outPath, serialized);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Walk a Vite plugins option (which may nest arrays, hold falsy slots, or be a thenable) and return
|
|
185
|
+
* the stashed cairnManifest options from the first matching plugin, or null if there is none. */
|
|
186
|
+
function findCairnOptions(plugins: unknown): CairnManifestOptions | null {
|
|
187
|
+
if (!plugins) return null;
|
|
188
|
+
if (Array.isArray(plugins)) {
|
|
189
|
+
for (const p of plugins) {
|
|
190
|
+
const found = findCairnOptions(p);
|
|
191
|
+
if (found) return found;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (typeof plugins === 'object' && CAIRN_OPTIONS in plugins) {
|
|
196
|
+
return (plugins as CairnManifestPlugin)[CAIRN_OPTIONS] ?? null;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A minimal plugin that serves only the virtual module in one mode, for the nested SSR load. It
|
|
202
|
+
* carries no buildStart, so the nested server never recurses into the verify. */
|
|
203
|
+
function cairnVirtualOnly(opts: CairnManifestOptions, mode: 'verify' | 'write'): Plugin {
|
|
204
|
+
return {
|
|
205
|
+
name: 'cairn-manifest-virtual',
|
|
206
|
+
resolveId(id) {
|
|
207
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
208
|
+
},
|
|
209
|
+
load(id) {
|
|
210
|
+
if (id === RESOLVED_ID) return virtualSource(opts, mode);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|