create-refrakt 0.14.2 → 0.14.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-refrakt",
3
3
  "description": "Scaffold a new refrakt.md project",
4
- "version": "0.14.2",
4
+ "version": "0.14.4",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.2.0",
33
- "@refrakt-md/plan": "0.14.2",
34
- "@refrakt-md/runes": "0.14.2",
35
- "@refrakt-md/transform": "0.14.2"
33
+ "@refrakt-md/plan": "0.14.4",
34
+ "@refrakt-md/runes": "0.14.4",
35
+ "@refrakt-md/transform": "0.14.4"
36
36
  }
37
37
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getTransform, getSite, getTheme, getHighlightTransform } from '../setup';
2
+ import { getTransform, getSite, getTheme, getHighlightTransform, seoSiteFields } from '../setup';
3
3
  import { renderPage, buildSeoHead } from '@refrakt-md/astro';
4
4
  import type { RendererNode } from '@refrakt-md/types';
5
5
 
@@ -51,7 +51,7 @@ export async function getStaticPaths() {
51
51
  const { page, seo, highlightCss } = Astro.props;
52
52
  const theme = await getTheme();
53
53
  const html = renderPage({ theme, page });
54
- const head = buildSeoHead({ title: page.title, frontmatter: page.frontmatter, seo });
54
+ const head = buildSeoHead({ title: page.title, frontmatter: page.frontmatter, seo, ...seoSiteFields });
55
55
  const needsBehaviors = html.includes('data-layout-behaviors') || html.includes('data-rune=');
56
56
  const contextData = JSON.stringify({ pages: page.pages, currentUrl: page.url });
57
57
  ---
@@ -1,91 +1,57 @@
1
- import { loadContent, buildHighlightOptions } from '@refrakt-md/content';
2
- import { assembleThemeConfig, createTransform } from '@refrakt-md/transform';
1
+ import { createRefraktLoader } from '@refrakt-md/content';
3
2
  import { loadRefraktConfig, resolveSite } from '@refrakt-md/transform/node';
4
- import { loadPlugin, mergePlugins, runes as coreRunes } from '@refrakt-md/runes';
5
3
  import { getThemePackage } from '@refrakt-md/types';
6
- import type { Schema } from '@markdoc/markdoc';
7
4
  import { readFileSync } from 'node:fs';
8
- import * as path from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { resolve } from 'node:path';
9
7
 
10
- const config = loadRefraktConfig(path.resolve('refrakt.config.json'));
8
+ const configPath = resolve('refrakt.config.json');
9
+ const config = loadRefraktConfig(configPath);
11
10
  const { site } = resolveSite(config);
12
- const themePackage = getThemePackage(site.theme);
13
- const contentDir = path.resolve(site.contentDir);
14
11
 
15
- const routeRules = site.routeRules ?? [{ pattern: '**', layout: 'default' }];
16
-
17
- let _transform: ((tree: any) => any) | null = null;
18
- let _hl: { (tree: any): any; css: string } | null = null;
12
+ const loader = createRefraktLoader({ configPath });
13
+
14
+ export const getTransform = () => loader.getTransform();
15
+ export const getHighlightTransform = () => loader.getHighlightTransform();
16
+ export const getSite = () => loader.getSite();
17
+
18
+ /** Site-level SEO fields surfaced for `buildSeoHead`. Read once from refrakt.config.json. */
19
+ export const seoSiteFields = {
20
+ siteName: site.siteName,
21
+ baseUrl: site.baseUrl,
22
+ defaultImage: site.defaultImage,
23
+ logo: site.logo,
24
+ };
25
+
26
+ /**
27
+ * Build the AstroTheme `{ manifest, layouts }` shape for `renderPage`.
28
+ *
29
+ * Loads the theme package's manifest + layouts and bakes site-level fields
30
+ * (`routeRules`, `siteName`, `baseUrl`, `defaultImage`, `logo`) into the
31
+ * manifest so the SEO and route-resolution paths have what they need.
32
+ *
33
+ * Memoised — the theme is resolved once per process.
34
+ */
19
35
  let _theme: { manifest: any; layouts: any } | null = null;
20
- let _communityTags: Record<string, Schema> | undefined;
21
- let _packages: any[] | undefined;
22
-
23
- async function init() {
24
- if (_transform) return;
36
+ export async function getTheme() {
37
+ if (_theme) return _theme;
25
38
 
26
- const [themeModule, layoutsModule] = await Promise.all([
27
- import(themePackage + '/transform'),
28
- import(themePackage + '/layouts'),
29
- ]);
39
+ const themePackage = getThemePackage(site.theme);
40
+ const layouts = (await import(themePackage + '/layouts')).layouts;
30
41
 
31
- // Manifest is a JSON file — resolve its path and read directly
32
- const { createRequire: cr } = await import('node:module');
33
- const manifestPath = cr(import.meta.url).resolve(themePackage + '/manifest');
42
+ const manifestPath = createRequire(import.meta.url).resolve(themePackage + '/manifest');
34
43
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
35
44
 
36
45
  _theme = {
37
- manifest: { ...manifest, routeRules },
38
- layouts: layoutsModule.layouts,
46
+ manifest: {
47
+ ...manifest,
48
+ routeRules: site.routeRules ?? [{ pattern: '**', layout: 'default' }],
49
+ ...(site.siteName && { siteName: site.siteName }),
50
+ ...(site.baseUrl && { baseUrl: site.baseUrl }),
51
+ ...(site.defaultImage && { defaultImage: site.defaultImage }),
52
+ ...(site.logo && { logo: site.logo }),
53
+ },
54
+ layouts,
39
55
  };
40
-
41
- const themeConfig = themeModule.themeConfig ?? themeModule.luminaConfig ?? themeModule.default;
42
-
43
- let transformConfig = themeConfig;
44
-
45
- const pluginNames = site.plugins ?? config.plugins ?? [];
46
- if (pluginNames.length > 0) {
47
- const loaded = await Promise.all(
48
- pluginNames.map((name: string) => loadPlugin(name))
49
- );
50
- const coreRuneNames = new Set(Object.keys(coreRunes));
51
- const merged = mergePlugins(loaded, coreRuneNames, site.runes?.prefer);
52
-
53
- _communityTags = Object.keys(merged.tags).length > 0 ? merged.tags : undefined;
54
- _packages = loaded.map((l: any) => l.pkg);
55
-
56
- const { config: assembledConfig } = assembleThemeConfig({
57
- coreConfig: themeConfig,
58
- pluginRunes: merged.themeRunes,
59
- pluginIcons: merged.themeIcons,
60
- pluginBackgrounds: merged.themeBackgrounds,
61
- extensions: merged.extensions as any,
62
- provenance: merged.provenance,
63
- });
64
-
65
- transformConfig = assembledConfig;
66
- }
67
-
68
- _transform = createTransform(transformConfig);
69
- }
70
-
71
- export async function getTransform() {
72
- await init();
73
- return _transform!;
74
- }
75
-
76
- export async function getTheme() {
77
- await init();
78
- return _theme!;
79
- }
80
-
81
- export async function getHighlightTransform() {
82
- if (_hl) return _hl;
83
- const { createHighlightTransform } = await import('@refrakt-md/highlight');
84
- _hl = await createHighlightTransform(buildHighlightOptions(site));
85
- return _hl;
86
- }
87
-
88
- export async function getSite() {
89
- await init();
90
- return loadContent(contentDir, '/', {}, _communityTags, _packages);
56
+ return _theme;
91
57
  }
@@ -1,4 +1,14 @@
1
- import { refraktPlugin } from '@refrakt-md/eleventy';
1
+ import { refraktPlugin, writeSiteTokensCss } from '@refrakt-md/eleventy';
2
+ import { resolve } from 'node:path';
3
+
4
+ // Compose site-level token overrides (SPEC-048 presets + tokens + modes,
5
+ // SPEC-056 scoped tint projections) once at config-load time and write the
6
+ // CSS to a build-input directory. Eleventy's passthrough copy picks it up
7
+ // and ships it as `/css/site-tokens.css`.
8
+ await writeSiteTokensCss(
9
+ resolve('refrakt.config.json'),
10
+ resolve('src/_generated/site-tokens.css'),
11
+ );
2
12
 
3
13
  export default function (eleventyConfig) {
4
14
  eleventyConfig.addPlugin(refraktPlugin, {
@@ -6,11 +16,15 @@ export default function (eleventyConfig) {
6
16
  cssPrefix: '/css',
7
17
  behaviorFile: 'node_modules/@refrakt-md/behaviors/dist/index.js',
8
18
  jsPrefix: '/js',
19
+ // Watch content (and any sandbox examples) for --serve mode so edits
20
+ // trigger a rebuild + browser reload.
21
+ contentDir: resolve('content'),
9
22
  });
10
23
 
11
24
  eleventyConfig.addPassthroughCopy({
12
25
  'node_modules/@refrakt-md/lumina/tokens': '/css/tokens',
13
26
  'node_modules/@refrakt-md/lumina/styles': '/css/styles',
27
+ 'src/_generated/site-tokens.css': '/css/site-tokens.css',
14
28
  });
15
29
 
16
30
  return {
@@ -1,5 +1,19 @@
1
1
  import { createDataFile } from '@refrakt-md/eleventy';
2
+ import { loadRefraktConfig, resolveSite } from '@refrakt-md/transform/node';
2
3
  import manifest from '@refrakt-md/lumina/manifest';
3
4
  import { layouts } from '@refrakt-md/lumina/layouts';
5
+ import { resolve } from 'node:path';
4
6
 
5
- export default createDataFile({ theme: { manifest, layouts } });
7
+ const config = loadRefraktConfig(resolve('refrakt.config.json'));
8
+ const { site } = resolveSite(config);
9
+
10
+ export default createDataFile({
11
+ theme: { manifest, layouts },
12
+ contentDir: site.contentDir,
13
+ seo: {
14
+ siteName: site.siteName,
15
+ baseUrl: site.baseUrl,
16
+ defaultImage: site.defaultImage,
17
+ logo: site.logo,
18
+ },
19
+ });
@@ -7,6 +7,7 @@
7
7
  {{ seo.metaTags | safe }}
8
8
  {{ seo.jsonLd | safe }}
9
9
  <link rel="stylesheet" href="/css/index.css">
10
+ <link rel="stylesheet" href="/css/site-tokens.css">
10
11
  </head>
11
12
  <body>
12
13
  {{ html | safe }}
@@ -1,5 +1,15 @@
1
- import { loadContent, buildHighlightOptions } from '@refrakt-md/content';
2
- import { renderFullPage } from '@refrakt-md/html';
1
+ import {
2
+ loadContent,
3
+ buildHighlightOptions,
4
+ analyzeRuneUsage,
5
+ formatPipelineSummary,
6
+ } from '@refrakt-md/content';
7
+ import {
8
+ renderFullPage,
9
+ composeSiteTokensCss,
10
+ computeUsedCssBlocks,
11
+ buildUsedCssImports,
12
+ } from '@refrakt-md/html';
3
13
  import type { HtmlTheme } from '@refrakt-md/html';
4
14
  import { assembleThemeConfig, createTransform, defaultLayout } from '@refrakt-md/transform';
5
15
  import { loadRefraktConfig, resolveSite } from '@refrakt-md/transform/node';
@@ -9,13 +19,16 @@ import { getThemePackage } from '@refrakt-md/types';
9
19
  import type { RendererNode } from '@refrakt-md/types';
10
20
  import type { Schema } from '@markdoc/markdoc';
11
21
  import { mkdirSync, writeFileSync, cpSync, existsSync } from 'node:fs';
22
+ import { fileURLToPath } from 'node:url';
12
23
  import * as path from 'node:path';
13
24
 
14
25
  // --- Configuration -------------------------------------------------------
15
26
 
16
- const config = loadRefraktConfig(path.resolve('refrakt.config.json'));
27
+ const configPath = path.resolve('refrakt.config.json');
28
+ const config = loadRefraktConfig(configPath);
17
29
  const { site } = resolveSite(config);
18
30
  const contentDir = path.resolve(site.contentDir);
31
+ const configDir = path.dirname(configPath);
19
32
  const outDir = 'build';
20
33
 
21
34
  // --- Helpers --------------------------------------------------------------
@@ -88,6 +101,10 @@ async function build() {
88
101
  // Create highlight transform
89
102
  const hl = await createHighlightTransform(buildHighlightOptions(site));
90
103
 
104
+ // Compose site-level token overrides CSS (SPEC-048 + SPEC-056).
105
+ // Empty string when the site has no overrides; safe to inline either way.
106
+ const siteTokensCss = await composeSiteTokensCss(site, configDir);
107
+
91
108
  // Build theme object for HTML adapter
92
109
  const themeManifestModule = await import(themePackage + '/manifest', { with: { type: 'json' } });
93
110
  const manifest = themeManifestModule.default;
@@ -102,13 +119,55 @@ async function build() {
102
119
  },
103
120
  };
104
121
 
105
- // Load content
106
- const site = await loadContent(contentDir, '/', icons, communityTags);
122
+ // Load content. The HTML adapter's build script doesn't surface
123
+ // `security` / `variables` via a CLI flag — for hosted-product use, edit
124
+ // this file to pass them through to `loadContent` (matches the option
125
+ // shape `createRefraktLoader` accepts for the Vite-based adapters).
126
+ const loadedSite = await loadContent(contentDir, '/', icons, communityTags);
127
+
128
+ // Print the standard Phase 1/2/3/4 + warnings summary so the HTML build
129
+ // gets the same visibility into the cross-page pipeline that the SvelteKit
130
+ // reference adapter prints.
131
+ process.stderr.write(
132
+ formatPipelineSummary(loadedSite.pipelineStats, loadedSite.pipelineWarnings),
133
+ );
107
134
 
108
135
  mkdirSync(outDir, { recursive: true });
109
136
 
137
+ // Tree-shake per-rune CSS: only ship blocks that actually appear in the
138
+ // page corpus. Falls back to the theme barrel if analysis fails.
139
+ const usageReport = analyzeRuneUsage(loadedSite.pages);
140
+ let stylesheets: string[];
141
+ let blocksToCopy: { src: string; dest: string }[] = [];
142
+ try {
143
+ const { usedBlocks, stylesDir } = await computeUsedCssBlocks(
144
+ usageReport.allTypes,
145
+ finalConfig,
146
+ themePackage,
147
+ );
148
+ const themeEntryUrl = import.meta.resolve(themePackage);
149
+ const themeDir = path.dirname(fileURLToPath(themeEntryUrl));
150
+ stylesheets = ['/base.css'];
151
+ blocksToCopy.push({
152
+ src: path.join(themeDir, 'base.css'),
153
+ dest: path.join(outDir, 'base.css'),
154
+ });
155
+ for (const block of [...usedBlocks].sort()) {
156
+ stylesheets.push(`/styles/runes/${block}.css`);
157
+ blocksToCopy.push({
158
+ src: path.join(stylesDir, `${block}.css`),
159
+ dest: path.join(outDir, 'styles', 'runes', `${block}.css`),
160
+ });
161
+ }
162
+ } catch (err) {
163
+ console.warn(
164
+ `Tree-shaking skipped (${(err as Error).message}); shipping full theme barrel.`,
165
+ );
166
+ stylesheets = ['/styles.css'];
167
+ }
168
+
110
169
  // Collect page metadata for navigation
111
- const pages = site.pages
170
+ const pages = loadedSite.pages
112
171
  .filter(p => !p.route.draft)
113
172
  .map(p => ({
114
173
  url: p.route.url,
@@ -118,7 +177,7 @@ async function build() {
118
177
 
119
178
  let count = 0;
120
179
 
121
- for (const page of site.pages) {
180
+ for (const page of loadedSite.pages) {
122
181
  if (page.route.draft) continue;
123
182
 
124
183
  // Serialize → identity transform → highlight
@@ -147,9 +206,17 @@ async function build() {
147
206
  },
148
207
  },
149
208
  {
150
- stylesheets: ['/styles.css'],
151
- headExtra: hl.css ? `<style>${hl.css}</style>` : '',
209
+ stylesheets,
210
+ // Order matters: highlight CSS first, site-tokens CSS second so
211
+ // site-level `--rf-*` overrides resolve last in the cascade.
212
+ headExtra:
213
+ (hl.css ? `<style>${hl.css}</style>` : '') +
214
+ (siteTokensCss ? `<style>${siteTokensCss}</style>` : ''),
152
215
  seo: page.seo,
216
+ baseUrl: site.baseUrl,
217
+ siteName: site.siteName,
218
+ defaultImage: site.defaultImage,
219
+ logo: site.logo,
153
220
  },
154
221
  );
155
222
 
@@ -163,16 +230,27 @@ async function build() {
163
230
  count++;
164
231
  }
165
232
 
166
- // Copy theme CSS to build directory
167
- try {
168
- const themePkg = themePackage;
169
- const themeDir = path.dirname(require.resolve(themePkg + '/package.json'));
170
- const cssPath = path.join(themeDir, 'index.css');
171
- if (existsSync(cssPath)) {
172
- cpSync(cssPath, path.join(outDir, 'styles.css'));
233
+ // Copy the per-rune CSS files (or the theme barrel as fallback)
234
+ if (blocksToCopy.length > 0) {
235
+ for (const { src, dest } of blocksToCopy) {
236
+ if (existsSync(src)) {
237
+ mkdirSync(path.dirname(dest), { recursive: true });
238
+ cpSync(src, dest);
239
+ }
240
+ }
241
+ } else {
242
+ try {
243
+ const themePkg = themePackage;
244
+ const themeDir = path.dirname(require.resolve(themePkg + '/package.json'));
245
+ const cssPath = path.join(themeDir, 'index.css');
246
+ if (existsSync(cssPath)) {
247
+ cpSync(cssPath, path.join(outDir, 'styles.css'));
248
+ }
249
+ } catch {
250
+ console.warn(
251
+ 'Warning: Could not copy theme CSS. Add a styles.css to the build directory manually.',
252
+ );
173
253
  }
174
- } catch {
175
- console.warn('Warning: Could not copy theme CSS. Add a styles.css to the build directory manually.');
176
254
  }
177
255
 
178
256
  console.log(`Built ${count} pages to ${outDir}/`);
@@ -1,25 +1,35 @@
1
1
  import { loadContent } from '@refrakt-md/content';
2
2
  import { assembleThemeConfig, createTransform } from '@refrakt-md/transform';
3
+ import { loadRefraktConfig, resolveSite } from '@refrakt-md/transform/node';
3
4
  import { loadPlugin, mergePlugins, runes as coreRunes } from '@refrakt-md/runes';
5
+ import { getThemePackage } from '@refrakt-md/types';
4
6
  import manifest from '@refrakt-md/lumina/manifest';
5
7
  import { layouts } from '@refrakt-md/lumina/layouts';
6
8
  const theme = { manifest, layouts };
7
- import { RefraktContent, buildMetadata, buildUrlFromParams, hasInteractiveRunes } from '@refrakt-md/next';
9
+ import { RefraktContent, buildMetadata, buildJsonLd, buildUrlFromParams, hasInteractiveRunes } from '@refrakt-md/next';
8
10
  import { BehaviorInit } from '@refrakt-md/next/client';
9
11
  import type { RendererNode } from '@refrakt-md/types';
10
- import type { RefraktConfig } from '@refrakt-md/types';
11
12
  import type { Schema } from '@markdoc/markdoc';
12
- import { readFileSync } from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
 
15
- const config: RefraktConfig = JSON.parse(readFileSync(path.resolve('refrakt.config.json'), 'utf-8'));
16
- const contentDir = path.resolve(config.contentDir);
15
+ const config = loadRefraktConfig(path.resolve('refrakt.config.json'));
16
+ const { site } = resolveSite(config);
17
+ const contentDir = path.resolve(site.contentDir);
18
+
19
+ // Site-level SEO fields surfaced into buildMetadata + buildJsonLd.
20
+ const seoSite = {
21
+ siteName: site.siteName,
22
+ baseUrl: site.baseUrl,
23
+ defaultImage: site.defaultImage,
24
+ logo: site.logo,
25
+ };
17
26
 
18
27
  async function getTransformAndTags() {
19
- const themeModule = await import(config.theme + '/transform');
28
+ const themePackage = getThemePackage(site.theme);
29
+ const themeModule = await import(themePackage + '/transform');
20
30
  const themeConfig = themeModule.themeConfig ?? themeModule.luminaConfig ?? themeModule.default;
21
31
 
22
- const pluginNames = config.plugins ?? [];
32
+ const pluginNames = site.plugins ?? [];
23
33
  if (pluginNames.length === 0) {
24
34
  return { transform: createTransform(themeConfig), communityTags: undefined };
25
35
  }
@@ -28,7 +38,7 @@ async function getTransformAndTags() {
28
38
  pluginNames.map((name: string) => loadPlugin(name))
29
39
  );
30
40
  const coreRuneNames = new Set(Object.keys(coreRunes));
31
- const merged = mergePlugins(loaded, coreRuneNames, config.runes?.prefer);
41
+ const merged = mergePlugins(loaded, coreRuneNames, site.runes?.prefer);
32
42
 
33
43
  const communityTags: Record<string, Schema> | undefined =
34
44
  Object.keys(merged.tags).length > 0 ? merged.tags : undefined;
@@ -73,6 +83,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug?: st
73
83
  title: (page.frontmatter.title as string) ?? '',
74
84
  frontmatter: page.frontmatter,
75
85
  seo: page.seo,
86
+ ...seoSite,
76
87
  });
77
88
  }
78
89
 
@@ -1,9 +1,21 @@
1
1
  import '@refrakt-md/lumina';
2
+ import { getSiteTokensCss } from '@refrakt-md/next';
2
3
  import type { ReactNode } from 'react';
3
4
 
4
- export default function RootLayout({ children }: { children: ReactNode }) {
5
+ // Site-level token overrides (SPEC-048 + SPEC-056). Computed once per request
6
+ // in the Server Component scope; the resulting CSS layers on top of the
7
+ // theme package's barrel so `--rf-*` overrides resolve last.
8
+ const siteTokensCssPromise = getSiteTokensCss();
9
+
10
+ export default async function RootLayout({ children }: { children: ReactNode }) {
11
+ const siteTokensCss = await siteTokensCssPromise;
5
12
  return (
6
13
  <html lang="en">
14
+ <head>
15
+ {siteTokensCss && (
16
+ <style dangerouslySetInnerHTML={{ __html: siteTokensCss }} />
17
+ )}
18
+ </head>
7
19
  <body>{children}</body>
8
20
  </html>
9
21
  );