@contentrain/emitter-astro 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Contentrain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @contentrain/emitter-astro
2
+
3
+ Open Astro emitter for Contentrain migrations: renders a **`ProjectIR`** (route model, layout families, component variants, query bindings, design tokens — see `@contentrain/types`) plus prepared content into a complete Astro project, returned as a pure file map.
4
+
5
+ The analysis that *produces* a good ProjectIR is the hard part and lives elsewhere. Rendering one is deliberately boring — which is why this half is open (MIT), portable, and replaceable by community emitters for other frameworks.
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import { emitAstroProject, writeEmit } from '@contentrain/emitter-astro'
11
+
12
+ const result = emitAstroProject({ ir, content, css })
13
+ console.warn(result.warnings)
14
+ await writeEmit(result, './out/site')
15
+ ```
16
+
17
+ ## What it emits
18
+
19
+ | Piece | How |
20
+ |---|---|
21
+ | Layouts (`src/layouts/*.astro`) | One per `LayoutFamily`. Chrome travels as **data** (`src/data/chrome/*.json`) injected via `set:html` — the Astro compiler never parses theme markup. `@@mark@@` placeholders (title, author, `date{n}`, `term{n}`, `feat{n}`, slug) are filled per page. |
22
+ | Pages (`src/pages/…`) | From `RouteModel` patterns: `single` routes render post bodies; list routes render items through the extracted `item_template` (plain fallback list + warning when absent). Pagination is a route param, never a separate family. |
23
+ | Legacy CSS (`public/styles/legacy/`) | Quarantined in `@layer legacy { … }` with leading `@import`s hoisted and layered; a mid-file `@import` is left as-is with a warning. |
24
+ | Evolution layer (`src/styles/modern.css`) | Tailwind 4, CSS-first: extracted design tokens land in `@theme`. Migrated layouts never load it; new pages build on it. |
25
+ | Components (`src/components/*.astro`) | `<cr-component>` placeholders carrying type/source/variants; `runtime`-sourced ones point to the migration handoff offers. |
26
+ | Split viewports | With `viewport_strategy: 'split'`, `build:desktop` / `build:mobile` scripts scaffold per-device production. |
27
+
28
+ Everything content-shaped flows through JSON data files, never through generated template source — determinism and safe escaping fall out of that one rule.
@@ -0,0 +1,97 @@
1
+ import { ProjectIR } from "@contentrain/types";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * One piece of content ready to render: body already HTML, assets already
6
+ * rewritten to local paths, dates already formatted in the site's observed
7
+ * formats. The emitter renders; it does not fetch, localize, or format —
8
+ * those are producer concerns, and keeping them out is what makes this
9
+ * package portable.
10
+ */
11
+ interface EmitPost {
12
+ slug: string;
13
+ title: string;
14
+ /** Rendered, asset-rewritten HTML body. */
15
+ body: string;
16
+ /** The site's date-format catalog applied to this post's date (index-aligned with `date{n}` marks). */
17
+ dates?: string[];
18
+ author?: string;
19
+ author_first?: string;
20
+ author_last?: string;
21
+ terms?: string[];
22
+ /** Local featured-image file names (largest first). */
23
+ featured?: string[];
24
+ excerpt?: string;
25
+ }
26
+ /** One static path of a list route: its params and the items its list renders. */
27
+ interface QueryPage {
28
+ params: Record<string, string>;
29
+ items: EmitPost[];
30
+ /**
31
+ * Item markup with `@@mark@@` placeholders (title, date{n}, author, excerpt,
32
+ * feat, slug), extracted from the source list. When present, list pages
33
+ * render each item by filling the template; when absent, a plain fallback
34
+ * list is emitted and a warning recorded — a silent fallback would read as
35
+ * fidelity when it is not.
36
+ */
37
+ item_template?: string;
38
+ }
39
+ interface EmitContent {
40
+ /** Content for `single` routes, keyed by nothing — slug lives on the post. */
41
+ posts?: EmitPost[];
42
+ /** QueryBinding id → the static paths (and items) that query produces. */
43
+ queries?: Record<string, QueryPage[]>;
44
+ }
45
+ /** A legacy stylesheet provided by the producer, referenced from `LayoutFamily.css.files`. */
46
+ interface EmitCssFile {
47
+ path: string;
48
+ content: string;
49
+ }
50
+ interface EmitOptions {
51
+ /** Generated project's package name. Default: "migrated-site". */
52
+ projectName?: string;
53
+ /** Include Tailwind 4 as the evolution layer (default true). */
54
+ tailwind?: boolean;
55
+ }
56
+ interface EmitInput {
57
+ ir: ProjectIR;
58
+ content?: EmitContent;
59
+ css?: EmitCssFile[];
60
+ options?: EmitOptions;
61
+ }
62
+ /**
63
+ * Pure result: every generated file as path → content. Writing to disk is a
64
+ * separate, trivial step (`writeEmit`) so the core stays testable and usable
65
+ * from any host — CLI, service, or another emitter wrapping this one.
66
+ */
67
+ interface EmitResult {
68
+ files: Record<string, string>;
69
+ warnings: string[];
70
+ }
71
+ //#endregion
72
+ //#region src/css.d.ts
73
+ interface WrapCssResult {
74
+ content: string;
75
+ warning?: string;
76
+ }
77
+ declare function wrapLegacyCss(path: string, css: string): WrapCssResult;
78
+ //#endregion
79
+ //#region src/util.d.ts
80
+ /** "f-archive" → "FArchive", "posts" → "Posts" — Astro component file names. */
81
+ declare function pascalCase(id: string): string;
82
+ /**
83
+ * Route pattern → Astro pages path.
84
+ * `/` → `index.astro`; `/category/:term/page/:page` → `category/[term]/page/[page].astro`.
85
+ * Returns null for patterns Astro's file routing cannot express (recorded as a warning upstream).
86
+ */
87
+ declare function patternToPagePath(pattern: string): string | null;
88
+ /** JSON with stable key order and trailing newline — deterministic output, stable diffs. */
89
+ declare function stableJson(value: unknown): string;
90
+ //#endregion
91
+ //#region src/index.d.ts
92
+ declare function emitAstroProject(input: EmitInput): EmitResult;
93
+ /** Write an EmitResult to disk. Separate from emit so the core stays pure. */
94
+ declare function writeEmit(result: EmitResult, dir: string): Promise<void>;
95
+ //#endregion
96
+ export { type EmitContent, type EmitCssFile, type EmitInput, type EmitOptions, type EmitPost, type EmitResult, type QueryPage, emitAstroProject, pascalCase, patternToPagePath, stableJson, wrapLegacyCss, writeEmit };
97
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/css.ts","../src/util.ts","../src/index.ts"],"mappings":";;;;;AAWA;;;;;UAAiB,QAAA;EACf,IAAA;EACA,KAAA;EAKA;EAHA,IAAA;EAKA;EAHA,KAAA;EACA,MAAA;EACA,YAAA;EACA,WAAA;EACA,KAAA;EAOe;EALf,QAAA;EACA,OAAA;AAAA;;UAIe,SAAA;EACf,MAAA,EAAQ,MAAA;EACR,KAAA,EAAO,QAAA;EAQP;;;AAGF;;;;EAHE,aAAA;AAAA;AAAA,UAGe,WAAA;EAIC;EAFhB,KAAA,GAAQ,QAAA;EAAA;EAER,OAAA,GAAU,MAAA,SAAe,SAAA;AAAA;;UAIV,WAAA;EACf,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,WAAA;EAJf;EAMA,WAAA;EAFe;EAIf,QAAA;AAAA;AAAA,UAGe,SAAA;EACf,EAAA,EAAI,SAAA;EACJ,OAAA,GAAU,WAAA;EACV,GAAA,GAAM,WAAA;EACN,OAAA,GAAU,WAAA;AAAA;;;;;;UAUK,UAAA;EACf,KAAA,EAAO,MAAA;EACP,QAAA;AAAA;;;UC7De,aAAA;EACf,OAAA;EACA,OAAA;AAAA;AAAA,iBAKc,aAAA,CAAc,IAAA,UAAc,GAAA,WAAc,aAAA;;;;iBCtB1C,UAAA,CAAW,EAAA;;AFU3B;;;;iBEGgB,iBAAA,CAAkB,OAAA;;iBAuBlB,UAAA,CAAW,KAAA;;;iBCrBX,gBAAA,CAAiB,KAAA,EAAO,SAAA,GAAY,UAAA;;iBA8C9B,SAAA,CAAU,MAAA,EAAQ,UAAA,EAAY,GAAA,WAAc,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,347 @@
1
+ import { LEGACY_CSS_LAYER } from "@contentrain/types";
2
+ //#region src/util.ts
3
+ /** "f-archive" → "FArchive", "posts" → "Posts" — Astro component file names. */
4
+ function pascalCase(id) {
5
+ return id.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
6
+ }
7
+ /**
8
+ * Route pattern → Astro pages path.
9
+ * `/` → `index.astro`; `/category/:term/page/:page` → `category/[term]/page/[page].astro`.
10
+ * Returns null for patterns Astro's file routing cannot express (recorded as a warning upstream).
11
+ */
12
+ function patternToPagePath(pattern) {
13
+ if (pattern === "/" || pattern === "") return "index.astro";
14
+ if (pattern.includes("*")) return null;
15
+ const mapped = pattern.replace(/^\/+|\/+$/g, "").split("/").map((seg) => seg.startsWith(":") ? `[${seg.slice(1)}]` : seg);
16
+ if (mapped.some((seg) => !/^[[\]a-zA-Z0-9._-]+$/.test(seg))) return null;
17
+ const last = mapped.pop();
18
+ return [...mapped, `${last}.astro`].join("/");
19
+ }
20
+ const sortDeep = (v) => {
21
+ if (Array.isArray(v)) return v.map(sortDeep);
22
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, val]) => [k, sortDeep(val)]));
23
+ return v;
24
+ };
25
+ /** JSON with stable key order and trailing newline — deterministic output, stable diffs. */
26
+ function stableJson(value) {
27
+ return `${JSON.stringify(sortDeep(value), null, 2)}\n`;
28
+ }
29
+ //#endregion
30
+ //#region src/scaffold.ts
31
+ function scaffoldFiles(ir, options) {
32
+ const tailwind = options.tailwind !== false;
33
+ const split = ir.viewport_strategy === "split";
34
+ const files = {};
35
+ files["package.json"] = stableJson({
36
+ name: options.projectName ?? "migrated-site",
37
+ private: true,
38
+ type: "module",
39
+ scripts: {
40
+ dev: "astro dev",
41
+ build: "astro build",
42
+ preview: "astro preview",
43
+ ...split ? {
44
+ "build:desktop": "VIEWPORT=desktop astro build --outDir dist/desktop",
45
+ "build:mobile": "VIEWPORT=mobile astro build --outDir dist/mobile"
46
+ } : {}
47
+ },
48
+ dependencies: {
49
+ astro: "^5.0.0",
50
+ ...tailwind ? {
51
+ tailwindcss: "^4.0.0",
52
+ "@tailwindcss/vite": "^4.0.0"
53
+ } : {}
54
+ }
55
+ });
56
+ files["astro.config.mjs"] = [
57
+ `import { defineConfig } from 'astro/config'`,
58
+ ...tailwind ? [`import tailwindcss from '@tailwindcss/vite'`] : [],
59
+ ``,
60
+ `export default defineConfig({`,
61
+ ` build: { format: 'directory' },`,
62
+ ...tailwind ? [` vite: { plugins: [tailwindcss()] },`] : [],
63
+ `})`,
64
+ ``
65
+ ].join("\n");
66
+ if (tailwind) files["src/styles/modern.css"] = modernCss(ir.tokens);
67
+ files["src/lib/fill.ts"] = FILL_TS;
68
+ return files;
69
+ }
70
+ /** Tailwind 4 evolution layer: CSS-first config with the site's extracted tokens. */
71
+ function modernCss(tokens) {
72
+ const lines = [`@import 'tailwindcss';`, ``];
73
+ const theme = [];
74
+ const push = (prefix, map) => {
75
+ for (const [key, value] of Object.entries(map ?? {})) theme.push(` --${prefix}-${key}: ${value};`);
76
+ };
77
+ push("color", tokens?.colors);
78
+ push("font", tokens?.font_families);
79
+ push("text", tokens?.font_sizes);
80
+ push("spacing", tokens?.spacing);
81
+ push("breakpoint", tokens?.breakpoints);
82
+ if (theme.length) lines.push(`@theme {`, ...theme, `}`);
83
+ return `${lines.join("\n")}\n`;
84
+ }
85
+ const FILL_TS = `// Emitted by @contentrain/emitter-astro — shared mark-filling helpers.
86
+ // Chrome and item templates carry @@mark@@ placeholders; pages fill them from content.
87
+
88
+ export const esc = (value: unknown): string =>
89
+ String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
90
+
91
+ export function fillMarks(html: string, marks: Record<string, unknown>): string {
92
+ return html.replace(/@@([a-z0-9_]+)@@/gi, (_all, key: string) => esc(marks[key] ?? ''))
93
+ }
94
+
95
+ export interface MarkablePost {
96
+ slug: string
97
+ title: string
98
+ dates?: string[]
99
+ author?: string
100
+ author_first?: string
101
+ author_last?: string
102
+ terms?: string[]
103
+ featured?: string[]
104
+ excerpt?: string
105
+ }
106
+
107
+ /** The mark vocabulary the measurement chain proved out: title, author (+ first/last), date{n}, term{n}, feat{n}, excerpt, slug. */
108
+ export function postMarks(post: MarkablePost): Record<string, string> {
109
+ const marks: Record<string, string> = {
110
+ title: post.title,
111
+ author: post.author ?? '',
112
+ author_first: post.author_first ?? '',
113
+ author_last: post.author_last ?? '',
114
+ excerpt: post.excerpt ?? '',
115
+ slug: post.slug,
116
+ feat: post.featured?.[0] ?? '',
117
+ }
118
+ for (const [i, d] of (post.dates ?? []).entries()) marks[\`date\${i}\`] = d
119
+ for (const [i, t] of (post.terms ?? []).entries()) marks[\`term\${i}\`] = t
120
+ for (const [i, f] of (post.featured ?? []).entries()) marks[\`feat\${i}\`] = f
121
+ return marks
122
+ }
123
+ `;
124
+ //#endregion
125
+ //#region src/layouts.ts
126
+ function familyFiles(family) {
127
+ const name = pascalCase(family.id);
128
+ const chunks = family.chrome ?? [];
129
+ const joined = (position) => chunks.filter((c) => c.position === position).map((c) => c.html).join("\n");
130
+ const files = {};
131
+ files[`src/data/chrome/${family.id}.json`] = stableJson({
132
+ head: joined("head"),
133
+ before: joined("before_body"),
134
+ after: joined("after_body")
135
+ });
136
+ const cssLinks = (family.css.files ?? []).map((f) => `<link rel="stylesheet" href="/styles/legacy/${f.split("/").pop()}" />`).join("\n");
137
+ files[`src/layouts/${name}.astro`] = `---
138
+ // Family: ${family.id}${family.name ? ` (${family.name})` : ""} — emitted by @contentrain/emitter-astro
139
+ import chrome from '../data/chrome/${family.id}.json'
140
+ import { fillMarks } from '../lib/fill'
141
+ const { title = '', marks = {} } = Astro.props
142
+ const head = fillMarks(chrome.head, marks)
143
+ const before = fillMarks(chrome.before, marks)
144
+ const after = fillMarks(chrome.after, marks)
145
+ ---
146
+ <!doctype html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="utf-8" />
150
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
151
+ ${cssLinks.split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n")}
152
+ <Fragment set:html={head} />
153
+ <title>{title}</title>
154
+ </head>
155
+ <body>
156
+ <Fragment set:html={before} />
157
+ <slot />
158
+ <Fragment set:html={after} />
159
+ </body>
160
+ </html>
161
+ `;
162
+ return files;
163
+ }
164
+ //#endregion
165
+ //#region src/pages.ts
166
+ function routeFiles(route, family, content) {
167
+ const warnings = [];
168
+ const files = {};
169
+ const pagePath = patternToPagePath(route.pattern);
170
+ if (!pagePath) return {
171
+ files,
172
+ warnings: [`route ${route.id}: pattern "${route.pattern}" cannot be expressed as an Astro page — skipped`]
173
+ };
174
+ if (!family) return {
175
+ files,
176
+ warnings: [`route ${route.id}: family "${route.family}" not found — skipped`]
177
+ };
178
+ const layout = pascalCase(family.id);
179
+ const depth = pagePath.split("/").length - 1;
180
+ const up = "../".repeat(depth + 1) || "../";
181
+ const hasParams = pagePath.includes("[");
182
+ if (route.kind === "single") {
183
+ const posts = content.posts ?? [];
184
+ if (!posts.length) warnings.push(`route ${route.id}: single route with no posts — page emitted, data file empty`);
185
+ files["src/data/posts.json"] = stableJson(posts);
186
+ files[`src/pages/${pagePath}`] = singlePage(route, layout, up);
187
+ return {
188
+ files,
189
+ warnings
190
+ };
191
+ }
192
+ const queryPages = route.query ? content.queries?.[route.query] ?? [] : [];
193
+ if (route.query) {
194
+ if (!queryPages.length) warnings.push(`route ${route.id}: query "${route.query}" has no data — page emitted, data file empty`);
195
+ if (queryPages.some((p) => !p.item_template)) warnings.push(`route ${route.id}: item template missing on some pages — plain fallback list rendered (not fidelity)`);
196
+ files[`src/data/queries/${route.query}.json`] = stableJson(queryPages);
197
+ files[`src/pages/${pagePath}`] = listPage(route, layout, up, hasParams);
198
+ return {
199
+ files,
200
+ warnings
201
+ };
202
+ }
203
+ files[`src/pages/${pagePath}`] = staticPage(route, layout, up, hasParams, warnings);
204
+ return {
205
+ files,
206
+ warnings
207
+ };
208
+ }
209
+ function singlePage(route, layout, up) {
210
+ return `---
211
+ // Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro
212
+ import Layout from '${up}layouts/${layout}.astro'
213
+ import posts from '${up}data/posts.json'
214
+ import { postMarks } from '${up}lib/fill'
215
+
216
+ export function getStaticPaths() {
217
+ return posts.map((post) => ({ params: { slug: post.slug }, props: { post } }))
218
+ }
219
+ const { post } = Astro.props
220
+ ---
221
+ <Layout title={post.title} marks={postMarks(post)}>
222
+ <Fragment set:html={post.body} />
223
+ </Layout>
224
+ `;
225
+ }
226
+ function listPage(route, layout, up, hasParams) {
227
+ const paths = hasParams ? `export function getStaticPaths() {
228
+ return pages.map((page) => ({ params: page.params, props: { page } }))
229
+ }
230
+ const { page } = Astro.props` : `const page = pages[0] ?? { params: {}, items: [] }`;
231
+ return `---
232
+ // Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro
233
+ import Layout from '${up}layouts/${layout}.astro'
234
+ import pages from '${up}data/queries/${route.query}.json'
235
+ import { fillMarks, postMarks } from '${up}lib/fill'
236
+
237
+ ${paths}
238
+ const items = page.items.map((item) =>
239
+ page.item_template ? fillMarks(page.item_template, postMarks(item)) : undefined,
240
+ )
241
+ const fallback = items.some((h) => h === undefined)
242
+ ---
243
+ <Layout title={page.params.term ?? ''} marks={page.params}>
244
+ {fallback ? (
245
+ <ul class="cr-post-list">
246
+ {page.items.map((item) => (
247
+ <li><a href={'/' + item.slug + '/'}>{item.title}</a></li>
248
+ ))}
249
+ </ul>
250
+ ) : (
251
+ <Fragment set:html={items.join('')} />
252
+ )}
253
+ </Layout>
254
+ `;
255
+ }
256
+ function staticPage(route, layout, up, hasParams, warnings) {
257
+ if (hasParams) warnings.push(`route ${route.id}: parameterized route without a query — emitted with empty paths`);
258
+ const paths = hasParams ? `export function getStaticPaths() {
259
+ return []
260
+ }
261
+ ` : "";
262
+ return `---
263
+ // Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro
264
+ import Layout from '${up}layouts/${layout}.astro'
265
+ ${paths}---
266
+ <Layout title="" marks={{}} />
267
+ `;
268
+ }
269
+ //#endregion
270
+ //#region src/components.ts
271
+ function componentFiles(components) {
272
+ const files = {};
273
+ for (const c of components) {
274
+ const variants = (c.variants ?? []).map((v) => v.key);
275
+ files[`src/components/${pascalCase(c.id)}.astro`] = `---
276
+ /**
277
+ * cr-component: type=${c.type} source=${c.source}${c.name ? ` — ${c.name}` : ""}
278
+ * Emitted by @contentrain/emitter-astro as a placeholder.${c.source === "runtime" ? "\n * Needs a live provider — see the migration handoff offers." : ""}${variants.length ? `\n * Variants: ${variants.join(", ")}` : ""}
279
+ */
280
+ const { variant = ${JSON.stringify(variants[0] ?? "default")} } = Astro.props
281
+ ---
282
+ <cr-component data-type=${JSON.stringify(c.type)} data-variant={variant}></cr-component>
283
+ `;
284
+ }
285
+ return files;
286
+ }
287
+ //#endregion
288
+ //#region src/css.ts
289
+ const LEADING = /^(\s*(?:@charset[^;]+;|@import[^;]+;|\/\*[\s\S]*?\*\/)\s*)+/;
290
+ function wrapLegacyCss(path, css) {
291
+ const rest = css.replace(LEADING, "");
292
+ if (/@import\s/.test(rest)) return {
293
+ content: css,
294
+ warning: `${path}: @import after other rules — left unwrapped (fix the source stylesheet; mid-file imports are ignored by browsers)`
295
+ };
296
+ return { content: `${css.slice(0, css.length - rest.length).replace(/@import\s+([^;]+);/g, (full, spec) => /\blayer\s*\(/.test(spec) ? full : `@import ${spec.trim()} layer(${LEGACY_CSS_LAYER});`)}@layer ${LEGACY_CSS_LAYER} {\n${rest}\n}\n` };
297
+ }
298
+ //#endregion
299
+ //#region src/index.ts
300
+ function emitAstroProject(input) {
301
+ const { ir } = input;
302
+ const warnings = [];
303
+ const files = {};
304
+ const add = (batch) => {
305
+ for (const [path, content] of Object.entries(batch)) {
306
+ if (files[path] !== void 0 && files[path] !== content) {
307
+ warnings.push(`duplicate file with different content: ${path} — keeping the first`);
308
+ continue;
309
+ }
310
+ files[path] = content;
311
+ }
312
+ };
313
+ add(scaffoldFiles(ir, input.options ?? {}));
314
+ const familiesById = new Map(ir.families.map((f) => [f.id, f]));
315
+ for (const family of ir.families) add(familyFiles(family));
316
+ for (const css of input.css ?? []) {
317
+ const wrapped = wrapLegacyCss(css.path, css.content);
318
+ if (wrapped.warning) warnings.push(wrapped.warning);
319
+ add({ [`public/styles/legacy/${css.path.split("/").pop() ?? css.path}`]: wrapped.content });
320
+ }
321
+ for (const family of ir.families) for (const ref of family.css.files ?? []) {
322
+ const base = ref.split("/").pop();
323
+ if (!(input.css ?? []).some((c) => (c.path.split("/").pop() ?? c.path) === base)) warnings.push(`family ${family.id}: css file "${ref}" not provided in input.css`);
324
+ }
325
+ for (const route of ir.routes) {
326
+ const result = routeFiles(route, familiesById.get(route.family), input.content ?? {});
327
+ add(result.files);
328
+ warnings.push(...result.warnings);
329
+ }
330
+ add(componentFiles(ir.components ?? []));
331
+ return {
332
+ files,
333
+ warnings
334
+ };
335
+ }
336
+ /** Write an EmitResult to disk. Separate from emit so the core stays pure. */
337
+ async function writeEmit(result, dir) {
338
+ const { mkdir, writeFile } = await import("node:fs/promises");
339
+ const { dirname, join } = await import("node:path");
340
+ const dirs = new Set(Object.keys(result.files).map((p) => dirname(join(dir, p))));
341
+ await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
342
+ await Promise.all(Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, "utf8")));
343
+ }
344
+ //#endregion
345
+ export { emitAstroProject, pascalCase, patternToPagePath, stableJson, wrapLegacyCss, writeEmit };
346
+
347
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/util.ts","../src/scaffold.ts","../src/layouts.ts","../src/pages.ts","../src/components.ts","../src/css.ts","../src/index.ts"],"sourcesContent":["/** \"f-archive\" → \"FArchive\", \"posts\" → \"Posts\" — Astro component file names. */\nexport function pascalCase(id: string): string {\n return id\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\n/**\n * Route pattern → Astro pages path.\n * `/` → `index.astro`; `/category/:term/page/:page` → `category/[term]/page/[page].astro`.\n * Returns null for patterns Astro's file routing cannot express (recorded as a warning upstream).\n */\nexport function patternToPagePath(pattern: string): string | null {\n if (pattern === '/' || pattern === '') return 'index.astro'\n if (pattern.includes('*')) return null\n const segments = pattern.replace(/^\\/+|\\/+$/g, '').split('/')\n const mapped = segments.map((seg) => (seg.startsWith(':') ? `[${seg.slice(1)}]` : seg))\n if (mapped.some((seg) => !/^[[\\]a-zA-Z0-9._-]+$/.test(seg))) return null\n const last = mapped.pop()\n return [...mapped, `${last}.astro`].join('/')\n}\n\nconst sortDeep = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(sortDeep)\n if (v && typeof v === 'object') {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n .toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, val]) => [k, sortDeep(val)]),\n )\n }\n return v\n}\n\n/** JSON with stable key order and trailing newline — deterministic output, stable diffs. */\nexport function stableJson(value: unknown): string {\n return `${JSON.stringify(sortDeep(value), null, 2)}\\n`\n}\n","// Project scaffolding: manifest, config, styles, and the shared fill helper\n// the generated pages use to pour content into `@@mark@@` placeholders.\n\nimport type { DesignTokens, ProjectIR } from '@contentrain/types'\nimport type { EmitOptions } from './types.js'\nimport { stableJson } from './util.js'\n\nexport function scaffoldFiles(ir: ProjectIR, options: EmitOptions): Record<string, string> {\n const tailwind = options.tailwind !== false\n const split = ir.viewport_strategy === 'split'\n const files: Record<string, string> = {}\n\n const pkg: Record<string, unknown> = {\n name: options.projectName ?? 'migrated-site',\n private: true,\n type: 'module',\n scripts: {\n dev: 'astro dev',\n build: 'astro build',\n preview: 'astro preview',\n // Split viewport production (skeleton): one build per device class, served by device.\n ...(split ? { 'build:desktop': 'VIEWPORT=desktop astro build --outDir dist/desktop', 'build:mobile': 'VIEWPORT=mobile astro build --outDir dist/mobile' } : {}),\n },\n dependencies: {\n astro: '^5.0.0',\n ...(tailwind ? { tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' } : {}),\n },\n }\n files['package.json'] = stableJson(pkg)\n\n files['astro.config.mjs'] = [\n `import { defineConfig } from 'astro/config'`,\n ...(tailwind ? [`import tailwindcss from '@tailwindcss/vite'`] : []),\n ``,\n `export default defineConfig({`,\n ` build: { format: 'directory' },`,\n ...(tailwind ? [` vite: { plugins: [tailwindcss()] },`] : []),\n `})`,\n ``,\n ].join('\\n')\n\n if (tailwind) files['src/styles/modern.css'] = modernCss(ir.tokens)\n\n files['src/lib/fill.ts'] = FILL_TS\n return files\n}\n\n/** Tailwind 4 evolution layer: CSS-first config with the site's extracted tokens. */\nfunction modernCss(tokens: DesignTokens | undefined): string {\n const lines: string[] = [`@import 'tailwindcss';`, ``]\n const theme: string[] = []\n const push = (prefix: string, map: Record<string, string> | undefined) => {\n for (const [key, value] of Object.entries(map ?? {})) theme.push(` --${prefix}-${key}: ${value};`)\n }\n push('color', tokens?.colors)\n push('font', tokens?.font_families)\n push('text', tokens?.font_sizes)\n push('spacing', tokens?.spacing)\n push('breakpoint', tokens?.breakpoints)\n if (theme.length) lines.push(`@theme {`, ...theme, `}`)\n return `${lines.join('\\n')}\\n`\n}\n\nconst FILL_TS = `// Emitted by @contentrain/emitter-astro — shared mark-filling helpers.\n// Chrome and item templates carry @@mark@@ placeholders; pages fill them from content.\n\nexport const esc = (value: unknown): string =>\n String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n\nexport function fillMarks(html: string, marks: Record<string, unknown>): string {\n return html.replace(/@@([a-z0-9_]+)@@/gi, (_all, key: string) => esc(marks[key] ?? ''))\n}\n\nexport interface MarkablePost {\n slug: string\n title: string\n dates?: string[]\n author?: string\n author_first?: string\n author_last?: string\n terms?: string[]\n featured?: string[]\n excerpt?: string\n}\n\n/** The mark vocabulary the measurement chain proved out: title, author (+ first/last), date{n}, term{n}, feat{n}, excerpt, slug. */\nexport function postMarks(post: MarkablePost): Record<string, string> {\n const marks: Record<string, string> = {\n title: post.title,\n author: post.author ?? '',\n author_first: post.author_first ?? '',\n author_last: post.author_last ?? '',\n excerpt: post.excerpt ?? '',\n slug: post.slug,\n feat: post.featured?.[0] ?? '',\n }\n for (const [i, d] of (post.dates ?? []).entries()) marks[\\`date\\${i}\\`] = d\n for (const [i, t] of (post.terms ?? []).entries()) marks[\\`term\\${i}\\`] = t\n for (const [i, f] of (post.featured ?? []).entries()) marks[\\`feat\\${i}\\`] = f\n return marks\n}\n`\n","// Layout families → Astro layouts.\n//\n// Chrome travels as DATA, not as Astro template source: the compiler must\n// never parse browser-tolerated theme markup (that distinction alone took a\n// measured corpus from build failures to zero). Each family's chrome chunks\n// land in a JSON data file; the layout injects them with `set:html` and fills\n// `@@mark@@` placeholders from the page's props.\n\nimport type { LayoutFamily } from '@contentrain/types'\nimport { pascalCase, stableJson } from './util.js'\n\nexport function familyFiles(family: LayoutFamily): Record<string, string> {\n const name = pascalCase(family.id)\n const chunks = family.chrome ?? []\n const joined = (position: 'head' | 'before_body' | 'after_body') =>\n chunks.filter((c) => c.position === position).map((c) => c.html).join('\\n')\n\n const files: Record<string, string> = {}\n files[`src/data/chrome/${family.id}.json`] = stableJson({\n head: joined('head'),\n before: joined('before_body'),\n after: joined('after_body'),\n })\n\n const cssLinks = (family.css.files ?? [])\n .map((f) => `<link rel=\"stylesheet\" href=\"/styles/legacy/${f.split('/').pop()}\" />`)\n .join('\\n')\n\n files[`src/layouts/${name}.astro`] = `---\n// Family: ${family.id}${family.name ? ` (${family.name})` : ''} — emitted by @contentrain/emitter-astro\nimport chrome from '../data/chrome/${family.id}.json'\nimport { fillMarks } from '../lib/fill'\nconst { title = '', marks = {} } = Astro.props\nconst head = fillMarks(chrome.head, marks)\nconst before = fillMarks(chrome.before, marks)\nconst after = fillMarks(chrome.after, marks)\n---\n<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n${cssLinks\n .split('\\n')\n .filter(Boolean)\n .map((l) => ` ${l}`)\n .join('\\n')}\n <Fragment set:html={head} />\n <title>{title}</title>\n </head>\n <body>\n <Fragment set:html={before} />\n <slot />\n <Fragment set:html={after} />\n </body>\n</html>\n`\n return files\n}\n","// Routes → Astro pages.\n//\n// Three shapes, matching what a migrated site actually needs:\n// - `single`: one page per post, body injected with `set:html` inside the family layout.\n// - list kinds (`archive`/`term`/`author`/`date`/`front` with a query): static\n// paths come from the producer's query data; items render through the\n// extracted item template when one exists.\n// - static kinds (no query): the family chrome IS the page.\n//\n// Pagination is a route parameter (`/news/page/:page`), never a separate\n// family — the generated paths simply include the page number in params.\n\nimport type { RouteModel, LayoutFamily } from '@contentrain/types'\nimport type { EmitContent, QueryPage } from './types.js'\nimport { pascalCase, patternToPagePath, stableJson } from './util.js'\n\nexport interface PageGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\nexport function routeFiles(\n route: RouteModel,\n family: LayoutFamily | undefined,\n content: EmitContent,\n): PageGenResult {\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) {\n return { files, warnings: [`route ${route.id}: pattern \"${route.pattern}\" cannot be expressed as an Astro page — skipped`] }\n }\n if (!family) {\n return { files, warnings: [`route ${route.id}: family \"${route.family}\" not found — skipped`] }\n }\n const layout = pascalCase(family.id)\n const depth = pagePath.split('/').length - 1\n const up = '../'.repeat(depth + 1) || '../'\n const hasParams = pagePath.includes('[')\n\n if (route.kind === 'single') {\n const posts = content.posts ?? []\n if (!posts.length) warnings.push(`route ${route.id}: single route with no posts — page emitted, data file empty`)\n files['src/data/posts.json'] = stableJson(posts)\n files[`src/pages/${pagePath}`] = singlePage(route, layout, up)\n return { files, warnings }\n }\n\n const queryPages: QueryPage[] = route.query ? (content.queries?.[route.query] ?? []) : []\n if (route.query) {\n if (!queryPages.length) warnings.push(`route ${route.id}: query \"${route.query}\" has no data — page emitted, data file empty`)\n if (queryPages.some((p) => !p.item_template)) {\n warnings.push(`route ${route.id}: item template missing on some pages — plain fallback list rendered (not fidelity)`)\n }\n files[`src/data/queries/${route.query}.json`] = stableJson(queryPages)\n files[`src/pages/${pagePath}`] = listPage(route, layout, up, hasParams)\n return { files, warnings }\n }\n\n files[`src/pages/${pagePath}`] = staticPage(route, layout, up, hasParams, warnings)\n return { files, warnings }\n}\n\nfunction singlePage(route: RouteModel, layout: string, up: string): string {\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport posts from '${up}data/posts.json'\nimport { postMarks } from '${up}lib/fill'\n\nexport function getStaticPaths() {\n return posts.map((post) => ({ params: { slug: post.slug }, props: { post } }))\n}\nconst { post } = Astro.props\n---\n<Layout title={post.title} marks={postMarks(post)}>\n <Fragment set:html={post.body} />\n</Layout>\n`\n}\n\nfunction listPage(route: RouteModel, layout: string, up: string, hasParams: boolean): string {\n const paths = hasParams\n ? `export function getStaticPaths() {\n return pages.map((page) => ({ params: page.params, props: { page } }))\n}\nconst { page } = Astro.props`\n : `const page = pages[0] ?? { params: {}, items: [] }`\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport pages from '${up}data/queries/${route.query}.json'\nimport { fillMarks, postMarks } from '${up}lib/fill'\n\n${paths}\nconst items = page.items.map((item) =>\n page.item_template ? fillMarks(page.item_template, postMarks(item)) : undefined,\n)\nconst fallback = items.some((h) => h === undefined)\n---\n<Layout title={page.params.term ?? ''} marks={page.params}>\n {fallback ? (\n <ul class=\"cr-post-list\">\n {page.items.map((item) => (\n <li><a href={'/' + item.slug + '/'}>{item.title}</a></li>\n ))}\n </ul>\n ) : (\n <Fragment set:html={items.join('')} />\n )}\n</Layout>\n`\n}\n\nfunction staticPage(route: RouteModel, layout: string, up: string, hasParams: boolean, warnings: string[]): string {\n if (hasParams) warnings.push(`route ${route.id}: parameterized route without a query — emitted with empty paths`)\n const paths = hasParams\n ? `export function getStaticPaths() {\n return []\n}\n`\n : ''\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\n${paths}---\n<Layout title=\"\" marks={{}} />\n`\n}\n","// Component placeholders.\n//\n// The emitter cannot invent a comments form or an ad slot — it marks the\n// spot. `rest`-sourced components are candidates for real implementations\n// against content data; `runtime` components need a live provider (that\n// conversation belongs to MigrationHandoff offers); `chrome` components\n// already travel inside the family chrome and get a stub for refactoring\n// toward.\n\nimport type { ComponentDef } from '@contentrain/types'\nimport { pascalCase } from './util.js'\n\nexport function componentFiles(components: ComponentDef[]): Record<string, string> {\n const files: Record<string, string> = {}\n for (const c of components) {\n const variants = (c.variants ?? []).map((v) => v.key)\n files[`src/components/${pascalCase(c.id)}.astro`] = `---\n/**\n * cr-component: type=${c.type} source=${c.source}${c.name ? ` — ${c.name}` : ''}\n * Emitted by @contentrain/emitter-astro as a placeholder.${\n c.source === 'runtime' ? '\\n * Needs a live provider — see the migration handoff offers.' : ''\n }${variants.length ? `\\n * Variants: ${variants.join(', ')}` : ''}\n */\nconst { variant = ${JSON.stringify(variants[0] ?? 'default')} } = Astro.props\n---\n<cr-component data-type=${JSON.stringify(c.type)} data-variant={variant}></cr-component>\n`\n }\n return files\n}\n","// Legacy CSS quarantine (the fidelity layer).\n//\n// Migrated pages must render pixel-identically, and a utility framework's\n// preflight/reset would fight the theme's CSS. The truce is a cascade layer:\n// every legacy stylesheet is wrapped in `@layer legacy { … }`, and modern CSS\n// lives in its own layer — the two never meet in specificity.\n//\n// One CSS rule constrains the wrapping: `@import` (and `@charset`) must\n// precede all other statements, so a file cannot simply be wrapped whole.\n// Leading imports are hoisted above the wrapper and given `layer(legacy)`;\n// an import *after* other rules is already invalid CSS that browsers ignore,\n// so the file is left unwrapped and a warning recorded instead of silently\n// producing different invalidity.\n\nimport { LEGACY_CSS_LAYER } from '@contentrain/types'\n\nexport interface WrapCssResult {\n content: string\n warning?: string\n}\n\nconst LEADING = /^(\\s*(?:@charset[^;]+;|@import[^;]+;|\\/\\*[\\s\\S]*?\\*\\/)\\s*)+/\n\nexport function wrapLegacyCss(path: string, css: string): WrapCssResult {\n const rest = css.replace(LEADING, '')\n if (/@import\\s/.test(rest)) {\n return {\n content: css,\n warning: `${path}: @import after other rules — left unwrapped (fix the source stylesheet; mid-file imports are ignored by browsers)`,\n }\n }\n const leading = css.slice(0, css.length - rest.length)\n const hoisted = leading.replace(/@import\\s+([^;]+);/g, (full, spec: string) =>\n /\\blayer\\s*\\(/.test(spec) ? full : `@import ${spec.trim()} layer(${LEGACY_CSS_LAYER});`,\n )\n return { content: `${hoisted}@layer ${LEGACY_CSS_LAYER} {\\n${rest}\\n}\\n` }\n}\n","// @contentrain/emitter-astro — the open half of the migration pipeline.\n//\n// A ProjectIR (route model, layout families, component variants, query\n// bindings, design tokens) plus prepared content goes in; a complete Astro\n// project comes out as a pure file map. The analysis that *produces* a good\n// ProjectIR is hard and lives elsewhere; rendering one is deliberately\n// boring — which is exactly why it can be open, portable, and replaceable\n// by community emitters for other frameworks.\n\nimport type { EmitInput, EmitResult } from './types.js'\nimport { scaffoldFiles } from './scaffold.js'\nimport { familyFiles } from './layouts.js'\nimport { routeFiles } from './pages.js'\nimport { componentFiles } from './components.js'\nimport { wrapLegacyCss } from './css.js'\n\nexport function emitAstroProject(input: EmitInput): EmitResult {\n const { ir } = input\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const add = (batch: Record<string, string>) => {\n for (const [path, content] of Object.entries(batch)) {\n if (files[path] !== undefined && files[path] !== content) {\n warnings.push(`duplicate file with different content: ${path} — keeping the first`)\n continue\n }\n files[path] = content\n }\n }\n\n add(scaffoldFiles(ir, input.options ?? {}))\n\n const familiesById = new Map(ir.families.map((f) => [f.id, f]))\n for (const family of ir.families) add(familyFiles(family))\n\n for (const css of input.css ?? []) {\n const wrapped = wrapLegacyCss(css.path, css.content)\n if (wrapped.warning) warnings.push(wrapped.warning)\n const base = css.path.split('/').pop() ?? css.path\n add({ [`public/styles/legacy/${base}`]: wrapped.content })\n }\n for (const family of ir.families) {\n for (const ref of family.css.files ?? []) {\n const base = ref.split('/').pop()\n if (!(input.css ?? []).some((c) => (c.path.split('/').pop() ?? c.path) === base)) {\n warnings.push(`family ${family.id}: css file \"${ref}\" not provided in input.css`)\n }\n }\n }\n\n for (const route of ir.routes) {\n const result = routeFiles(route, familiesById.get(route.family), input.content ?? {})\n add(result.files)\n warnings.push(...result.warnings)\n }\n\n add(componentFiles(ir.components ?? []))\n\n return { files, warnings }\n}\n\n/** Write an EmitResult to disk. Separate from emit so the core stays pure. */\nexport async function writeEmit(result: EmitResult, dir: string): Promise<void> {\n const { mkdir, writeFile } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n const dirs = new Set(Object.keys(result.files).map((p) => dirname(join(dir, p))))\n await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })))\n await Promise.all(\n Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, 'utf8')),\n )\n}\n\nexport type {\n EmitInput,\n EmitResult,\n EmitContent,\n EmitPost,\n QueryPage,\n EmitCssFile,\n EmitOptions,\n} from './types.js'\nexport { wrapLegacyCss } from './css.js'\nexport { pascalCase, patternToPagePath, stableJson } from './util.js'\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@contentrain/emitter-astro",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Open Astro emitter for Contentrain migrations — renders a ProjectIR into a working Astro project",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Contentrain/ai.git",
10
+ "directory": "packages/emitter-astro"
11
+ },
12
+ "homepage": "https://ai.contentrain.io",
13
+ "bugs": {
14
+ "url": "https://github.com/Contentrain/ai/issues"
15
+ },
16
+ "keywords": [
17
+ "contentrain",
18
+ "astro",
19
+ "emitter",
20
+ "migration",
21
+ "wordpress",
22
+ "static-site"
23
+ ],
24
+ "sideEffects": false,
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.mts",
31
+ "import": "./dist/index.mjs"
32
+ }
33
+ },
34
+ "main": "./dist/index.mjs",
35
+ "types": "./dist/index.d.mts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "dependencies": {
40
+ "@contentrain/types": "1.4.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.0",
44
+ "tsdown": "^0.21.0",
45
+ "typescript": "^5.7.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown src/index.ts --format esm --dts",
49
+ "dev": "tsdown src/index.ts --format esm --dts --watch",
50
+ "test": "vitest run",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist"
53
+ }
54
+ }