@koehler8/cms 1.0.0-beta.5
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 +21 -0
- package/README.md +202 -0
- package/bin/cms-generate-public-assets.js +27 -0
- package/bin/cms-validate-extensions.js +18 -0
- package/bin/cms-validate-themes.js +7 -0
- package/extensions/manifest.schema.json +125 -0
- package/package.json +84 -0
- package/public/img/preloaders/preloader-black.svg +1 -0
- package/public/img/preloaders/preloader-white.svg +1 -0
- package/public/robots.txt +5 -0
- package/scripts/check-overflow.mjs +33 -0
- package/scripts/generate-public-assets.js +401 -0
- package/scripts/patch-lru-cache-tla.js +164 -0
- package/scripts/validate-extensions.mjs +392 -0
- package/scripts/validate-themes.mjs +64 -0
- package/src/App.vue +3 -0
- package/src/components/About.vue +481 -0
- package/src/components/AboutValue.vue +361 -0
- package/src/components/BackToTop.vue +42 -0
- package/src/components/ComingSoon.vue +411 -0
- package/src/components/ComingSoonModal.vue +230 -0
- package/src/components/Contact.vue +518 -0
- package/src/components/Footer.vue +65 -0
- package/src/components/FooterMinimal.vue +153 -0
- package/src/components/Header.vue +583 -0
- package/src/components/Hero.vue +327 -0
- package/src/components/Home.vue +144 -0
- package/src/components/Intro.vue +130 -0
- package/src/components/IntroGate.vue +444 -0
- package/src/components/Plan.vue +116 -0
- package/src/components/Portfolio.vue +459 -0
- package/src/components/Preloader.vue +20 -0
- package/src/components/Principles.vue +67 -0
- package/src/components/Spacer15.vue +9 -0
- package/src/components/Spacer30.vue +9 -0
- package/src/components/Spacer40.vue +9 -0
- package/src/components/Spacer60.vue +9 -0
- package/src/components/StickyCTA.vue +263 -0
- package/src/components/Team.vue +432 -0
- package/src/components/icons/IconLinkedIn.vue +22 -0
- package/src/components/icons/IconX.vue +22 -0
- package/src/components/ui/SbCard.vue +52 -0
- package/src/components/ui/SkeletonPulse.vue +117 -0
- package/src/components/ui/UnitChip.vue +69 -0
- package/src/composables/useComingSoonConfig.js +120 -0
- package/src/composables/useComingSoonInterstitial.js +27 -0
- package/src/composables/useComponentResolver.js +196 -0
- package/src/composables/useEngagementTracking.js +187 -0
- package/src/composables/useIntroGate.js +46 -0
- package/src/composables/useLazyImage.js +77 -0
- package/src/composables/usePageConfig.js +184 -0
- package/src/composables/usePageMeta.js +76 -0
- package/src/composables/usePromoBackgroundStyles.js +67 -0
- package/src/constants/locales.js +20 -0
- package/src/extensions/extensionLoader.js +512 -0
- package/src/main.js +175 -0
- package/src/router/index.js +112 -0
- package/src/styles/base.css +896 -0
- package/src/styles/layout.css +342 -0
- package/src/styles/theme-base.css +84 -0
- package/src/themes/themeLoader.js +124 -0
- package/src/themes/themeManager.js +257 -0
- package/src/themes/themeValidator.js +380 -0
- package/src/utils/analytics.js +100 -0
- package/src/utils/appInfo.js +9 -0
- package/src/utils/assetResolver.js +162 -0
- package/src/utils/componentRegistry.js +46 -0
- package/src/utils/contentRequirements.js +67 -0
- package/src/utils/cookieConsent.js +281 -0
- package/src/utils/ctaCopy.js +58 -0
- package/src/utils/formatNumber.js +115 -0
- package/src/utils/imageSources.js +179 -0
- package/src/utils/inflateFlatConfig.js +30 -0
- package/src/utils/loadConfig.js +271 -0
- package/src/utils/semver.js +49 -0
- package/src/utils/siteStyles.js +40 -0
- package/src/utils/themeColors.js +65 -0
- package/src/utils/trackingContext.js +142 -0
- package/src/utils/unwrapDefault.js +14 -0
- package/src/utils/useScrollReveal.js +48 -0
- package/templates/index.html +36 -0
- package/themes/base/README.md +23 -0
- package/themes/base/theme.config.js +214 -0
- package/vite-plugin.js +637 -0
package/vite-plugin.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @koehler8/cms Vite plugin
|
|
3
|
+
*
|
|
4
|
+
* Generates virtual modules that wire site-specific content (config, assets,
|
|
5
|
+
* styles) into the CMS framework. Site repos consume this via a 3-line
|
|
6
|
+
* vite.config.js and otherwise contain only content.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import vue from '@vitejs/plugin-vue';
|
|
13
|
+
|
|
14
|
+
import { SUPPORTED_LOCALES } from './src/constants/locales.js';
|
|
15
|
+
|
|
16
|
+
// ---- Helpers ----
|
|
17
|
+
|
|
18
|
+
function loadSplitConfig(configDir) {
|
|
19
|
+
const sitePath = path.join(configDir, 'site.json');
|
|
20
|
+
if (!fs.existsSync(sitePath)) return null;
|
|
21
|
+
|
|
22
|
+
const site = JSON.parse(fs.readFileSync(sitePath, 'utf-8'));
|
|
23
|
+
const sharedPath = path.join(configDir, 'shared.json');
|
|
24
|
+
const shared = fs.existsSync(sharedPath) ? JSON.parse(fs.readFileSync(sharedPath, 'utf-8')) : {};
|
|
25
|
+
|
|
26
|
+
const pagesDir = path.join(configDir, 'pages');
|
|
27
|
+
const pages = {};
|
|
28
|
+
if (fs.existsSync(pagesDir)) {
|
|
29
|
+
for (const file of fs.readdirSync(pagesDir)) {
|
|
30
|
+
if (!file.endsWith('.json')) continue;
|
|
31
|
+
const pageId = file.replace('.json', '');
|
|
32
|
+
pages[pageId] = JSON.parse(fs.readFileSync(path.join(pagesDir, file), 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { site, shared, pages };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeRoutePath(value = '/') {
|
|
40
|
+
if (typeof value !== 'string') return '/';
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed) return '/';
|
|
43
|
+
let normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
44
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
45
|
+
normalized = normalized.replace(/\/$/, '');
|
|
46
|
+
return normalized || '/';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectPagePaths(config) {
|
|
50
|
+
if (!config || typeof config !== 'object') return ['/'];
|
|
51
|
+
const pages = config.pages || {};
|
|
52
|
+
const paths = new Set(['/']);
|
|
53
|
+
Object.values(pages).forEach((page) => {
|
|
54
|
+
if (!page || typeof page !== 'object') return;
|
|
55
|
+
if ('path' in page) {
|
|
56
|
+
paths.add(normalizeRoutePath(page.path));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return Array.from(paths);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectExtensionSetupFiles(extensionsDir) {
|
|
63
|
+
if (!fs.existsSync(extensionsDir)) return [];
|
|
64
|
+
const setupNames = ['vitest.setup.js', 'vitest.setup.ts', 'setup.js', 'setup.ts'];
|
|
65
|
+
const setups = [];
|
|
66
|
+
for (const slug of fs.readdirSync(extensionsDir)) {
|
|
67
|
+
const testDir = path.join(extensionsDir, slug, 'tests');
|
|
68
|
+
if (!fs.existsSync(testDir)) continue;
|
|
69
|
+
setupNames.forEach((filename) => {
|
|
70
|
+
const fullPath = path.join(testDir, filename);
|
|
71
|
+
if (fs.existsSync(fullPath)) setups.push(fullPath);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return setups;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractSiteMetadata(siteConfig) {
|
|
78
|
+
const {
|
|
79
|
+
title: siteTitle = '',
|
|
80
|
+
description: siteDescription = '',
|
|
81
|
+
url: siteUrl = '',
|
|
82
|
+
googleId: siteGoogleId = '',
|
|
83
|
+
sameAs: siteSameAs = [],
|
|
84
|
+
} = siteConfig.site || {};
|
|
85
|
+
|
|
86
|
+
const siteSameAsList = Array.isArray(siteSameAs)
|
|
87
|
+
? siteSameAs.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean)
|
|
88
|
+
: [];
|
|
89
|
+
const siteSameAsJson = JSON.stringify(siteSameAsList);
|
|
90
|
+
|
|
91
|
+
return { siteTitle, siteDescription, siteUrl, siteGoogleId, siteSameAsJson };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
95
|
+
const __dirname = path.dirname(__filename);
|
|
96
|
+
|
|
97
|
+
// Virtual module IDs (config, styles, assets — the entry is a real file on disk)
|
|
98
|
+
const VIRTUAL_CONFIG = 'virtual:cms-config-loader';
|
|
99
|
+
const VIRTUAL_STYLES = 'virtual:cms-site-styles';
|
|
100
|
+
const VIRTUAL_ASSETS = 'virtual:cms-asset-resolver';
|
|
101
|
+
|
|
102
|
+
const VIRTUAL_IDS = new Set([VIRTUAL_CONFIG, VIRTUAL_STYLES, VIRTUAL_ASSETS]);
|
|
103
|
+
|
|
104
|
+
// Entry file name (written to site repo root, gitignored, cleaned up in buildEnd)
|
|
105
|
+
const ENTRY_FILENAME = '.cms-entry.js';
|
|
106
|
+
|
|
107
|
+
// Initialize runtime singletons BEFORE importing the framework's main module.
|
|
108
|
+
// Framework components import resolver functions directly from the utility
|
|
109
|
+
// modules; those modules expose stub implementations until set*() is called.
|
|
110
|
+
//
|
|
111
|
+
// When external theme packages are provided via the `themes` option, the plugin
|
|
112
|
+
// generates additional import + registerTheme() calls so themes are available
|
|
113
|
+
// before the app renders.
|
|
114
|
+
function buildEntrySource(themePackages = [], extensionPackages = []) {
|
|
115
|
+
const themeImports = themePackages.map(
|
|
116
|
+
(pkg, i) => `import __theme${i} from '${pkg}';`
|
|
117
|
+
).join('\n');
|
|
118
|
+
|
|
119
|
+
const themeRegistrations = themePackages.map(
|
|
120
|
+
(_, i) => `registerTheme(__theme${i});`
|
|
121
|
+
).join('\n');
|
|
122
|
+
|
|
123
|
+
const extImports = extensionPackages.map(
|
|
124
|
+
(pkg, i) => `import __ext${i} from '${pkg}';`
|
|
125
|
+
).join('\n');
|
|
126
|
+
|
|
127
|
+
const extRegistrations = extensionPackages.map(
|
|
128
|
+
(_, i) => `await registerExtension(__ext${i});`
|
|
129
|
+
).join('\n');
|
|
130
|
+
|
|
131
|
+
const needsThemeRegister = themePackages.length > 0;
|
|
132
|
+
const needsExtRegister = extensionPackages.length > 0;
|
|
133
|
+
|
|
134
|
+
// Use dynamic import for createCmsApp so that setConfigLoader/etc run
|
|
135
|
+
// BEFORE ViteSSG's auto-mount IIFE. Static imports are hoisted and
|
|
136
|
+
// executed first, which means ViteSSG() would run before the runtime
|
|
137
|
+
// singletons are initialized.
|
|
138
|
+
return `
|
|
139
|
+
import { setConfigLoader } from '@koehler8/cms/utils/loadConfig';
|
|
140
|
+
import { setSiteStyleLoader } from '@koehler8/cms/utils/siteStyles';
|
|
141
|
+
import { setAssetResolver } from '@koehler8/cms/utils/assetResolver';
|
|
142
|
+
${needsThemeRegister ? `import { registerTheme } from '@koehler8/cms/themes/themeLoader';` : ''}
|
|
143
|
+
${needsExtRegister ? `import { registerExtension } from '@koehler8/cms/extensions/extensionLoader';` : ''}
|
|
144
|
+
${themeImports}
|
|
145
|
+
${extImports}
|
|
146
|
+
|
|
147
|
+
import * as __cmsConfig from '${VIRTUAL_CONFIG}';
|
|
148
|
+
import * as __cmsStyles from '${VIRTUAL_STYLES}';
|
|
149
|
+
import * as __cmsAssets from '${VIRTUAL_ASSETS}';
|
|
150
|
+
|
|
151
|
+
setConfigLoader(__cmsConfig);
|
|
152
|
+
setSiteStyleLoader(__cmsStyles);
|
|
153
|
+
setAssetResolver(__cmsAssets);
|
|
154
|
+
|
|
155
|
+
${themeRegistrations}
|
|
156
|
+
${extRegistrations}
|
|
157
|
+
|
|
158
|
+
const { createCmsApp } = await import('@koehler8/cms/app');
|
|
159
|
+
export const createApp = createCmsApp();
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Vite's convention: prefix resolved virtual IDs with \0 so they're not treated as real files
|
|
164
|
+
const resolved = (id) => `\0${id}`;
|
|
165
|
+
const isResolvedVirtual = (id) => id?.startsWith('\0virtual:cms-');
|
|
166
|
+
|
|
167
|
+
const htmlEscape = (value) =>
|
|
168
|
+
String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({
|
|
169
|
+
'&': '&',
|
|
170
|
+
'<': '<',
|
|
171
|
+
'>': '>',
|
|
172
|
+
'"': '"',
|
|
173
|
+
"'": ''',
|
|
174
|
+
}[c]));
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Render the index.html template with site metadata. Replaces EJS-style
|
|
178
|
+
* <% const ... %> declarations with pre-computed values, then handles
|
|
179
|
+
* <%= expr %> (escaped) and <%- expr %> (raw) substitutions.
|
|
180
|
+
*/
|
|
181
|
+
function renderTemplate(template, data) {
|
|
182
|
+
const canonicalBase = (data.siteUrl || '').trim().replace(/\/+$/, '');
|
|
183
|
+
const STATIC_LOGO_PATH = '/logo.png';
|
|
184
|
+
const STATIC_OG_IMAGE_PATH = '/og-image.jpg';
|
|
185
|
+
const ABSOLUTE_LOGO = canonicalBase ? `${canonicalBase}${STATIC_LOGO_PATH}` : STATIC_LOGO_PATH;
|
|
186
|
+
const ABSOLUTE_OG_IMAGE = canonicalBase
|
|
187
|
+
? `${canonicalBase}${STATIC_OG_IMAGE_PATH}`
|
|
188
|
+
: STATIC_OG_IMAGE_PATH;
|
|
189
|
+
|
|
190
|
+
const scope = {
|
|
191
|
+
...data,
|
|
192
|
+
STATIC_LOGO_PATH,
|
|
193
|
+
STATIC_OG_IMAGE_PATH,
|
|
194
|
+
canonicalBase,
|
|
195
|
+
ABSOLUTE_LOGO,
|
|
196
|
+
ABSOLUTE_OG_IMAGE,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Strip <% ... %> blocks (declarations are pre-computed above)
|
|
200
|
+
let out = template.replace(/<%[^=-][\s\S]*?%>/g, '');
|
|
201
|
+
|
|
202
|
+
// <%- expr %> → raw value
|
|
203
|
+
out = out.replace(/<%-\s*([\w$]+)\s*%>/g, (_, key) => String(scope[key] ?? ''));
|
|
204
|
+
|
|
205
|
+
// <%= expr %> → HTML-escaped value
|
|
206
|
+
out = out.replace(/<%=\s*([\w$]+)\s*%>/g, (_, key) => htmlEscape(scope[key]));
|
|
207
|
+
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readIndexTemplate(frameworkRoot) {
|
|
212
|
+
const templatePath = path.join(frameworkRoot, 'templates', 'index.html');
|
|
213
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Discover CJS dependencies from extension packages that need pre-bundling.
|
|
218
|
+
*
|
|
219
|
+
* When Vite serves ESM packages raw from node_modules, their CJS deps don't
|
|
220
|
+
* get the CJS→ESM default-export shim and cause runtime SyntaxErrors. This
|
|
221
|
+
* scans each extension package.json's `dependencies` and collects any that
|
|
222
|
+
* ship CJS (no "type": "module" in their package.json).
|
|
223
|
+
*/
|
|
224
|
+
function findPkgJson(name, fromDir) {
|
|
225
|
+
// Walk up node_modules to find the package directory, then read its package.json.
|
|
226
|
+
// This avoids require.resolve which can fail when exports doesn't include ./package.json.
|
|
227
|
+
const nmDir = path.join(fromDir, 'node_modules');
|
|
228
|
+
const pkgDir = name.startsWith('@')
|
|
229
|
+
? path.join(nmDir, ...name.split('/'))
|
|
230
|
+
: path.join(nmDir, name);
|
|
231
|
+
const pkgPath = path.join(pkgDir, 'package.json');
|
|
232
|
+
if (fs.existsSync(pkgPath)) {
|
|
233
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function discoverExtensionCjsDeps(extensionPackages, projectRoot) {
|
|
239
|
+
const SKIP = new Set(['vue', 'vue-router', 'pinia']);
|
|
240
|
+
const cjsDeps = new Set();
|
|
241
|
+
const visited = new Set();
|
|
242
|
+
|
|
243
|
+
function scanDeps(pkgName) {
|
|
244
|
+
if (visited.has(pkgName)) return;
|
|
245
|
+
visited.add(pkgName);
|
|
246
|
+
if (SKIP.has(pkgName) || pkgName.startsWith('@koehler8/cms')) return;
|
|
247
|
+
|
|
248
|
+
const pkg = findPkgJson(pkgName, projectRoot);
|
|
249
|
+
if (!pkg) return;
|
|
250
|
+
|
|
251
|
+
// Both CJS and ESM packages are included so their CJS deps get shimmed
|
|
252
|
+
cjsDeps.add(pkgName);
|
|
253
|
+
|
|
254
|
+
// Recurse into dependencies
|
|
255
|
+
for (const dep of Object.keys(pkg.dependencies || {})) {
|
|
256
|
+
scanDeps(dep);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const pkg of extensionPackages) {
|
|
261
|
+
const extPkg = findPkgJson(pkg, projectRoot);
|
|
262
|
+
if (!extPkg) continue;
|
|
263
|
+
for (const dep of Object.keys(extPkg.dependencies || {})) {
|
|
264
|
+
scanDeps(dep);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return [...cjsDeps];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default function cmsPlugin(options = {}) {
|
|
271
|
+
const {
|
|
272
|
+
siteDir: siteDirOption = './site',
|
|
273
|
+
frameworkRoot: frameworkRootOption = __dirname,
|
|
274
|
+
locales = SUPPORTED_LOCALES,
|
|
275
|
+
themes: themePackages = [],
|
|
276
|
+
extensions: extensionPackages = [],
|
|
277
|
+
} = options;
|
|
278
|
+
|
|
279
|
+
// These are resolved relative to the project cwd at config time
|
|
280
|
+
let siteDir;
|
|
281
|
+
let siteRoot; // project root — parent of siteDir
|
|
282
|
+
let frameworkRoot;
|
|
283
|
+
let siteConfig;
|
|
284
|
+
let pagePaths;
|
|
285
|
+
let metadata;
|
|
286
|
+
let tempIndexPath;
|
|
287
|
+
let tempEntryPath;
|
|
288
|
+
|
|
289
|
+
const writeTempFiles = () => {
|
|
290
|
+
// Write entry file (with optional external theme registrations)
|
|
291
|
+
fs.writeFileSync(tempEntryPath, buildEntrySource(themePackages, extensionPackages), 'utf-8');
|
|
292
|
+
|
|
293
|
+
// Write index.html with site metadata injected and script src pointing
|
|
294
|
+
// at the entry file
|
|
295
|
+
const template = readIndexTemplate(frameworkRoot);
|
|
296
|
+
const html = renderTemplate(template, {
|
|
297
|
+
site: metadata.siteTitle,
|
|
298
|
+
siteDescription: metadata.siteDescription,
|
|
299
|
+
siteUrl: metadata.siteUrl,
|
|
300
|
+
siteGoogleId: metadata.siteGoogleId,
|
|
301
|
+
siteSameAsJson: metadata.siteSameAsJson,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const processed = html.replace(
|
|
305
|
+
/<script\s+type="module"\s+src="\/src\/main\.js"\s*>\s*<\/script>/,
|
|
306
|
+
`<script type="module" src="/${ENTRY_FILENAME}"></script>`
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
fs.writeFileSync(tempIndexPath, processed, 'utf-8');
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const cleanupTempFiles = () => {
|
|
313
|
+
for (const p of [tempIndexPath, tempEntryPath]) {
|
|
314
|
+
if (p && fs.existsSync(p)) {
|
|
315
|
+
try { fs.unlinkSync(p); } catch { /* noop */ }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Recursively mirror files from src → dest, skipping any file that already
|
|
321
|
+
// exists in dest (so site-specific files win over framework defaults).
|
|
322
|
+
const mirrorDir = (src, dest) => {
|
|
323
|
+
if (!fs.existsSync(src)) return;
|
|
324
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
325
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
326
|
+
const srcPath = path.join(src, entry.name);
|
|
327
|
+
const destPath = path.join(dest, entry.name);
|
|
328
|
+
if (entry.isDirectory()) {
|
|
329
|
+
mirrorDir(srcPath, destPath);
|
|
330
|
+
} else if (entry.isFile()) {
|
|
331
|
+
if (!fs.existsSync(destPath)) {
|
|
332
|
+
fs.copyFileSync(srcPath, destPath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const syncPublicDir = () => {
|
|
339
|
+
const sitePublicDir = path.join(siteRoot, 'public');
|
|
340
|
+
const frameworkPublicDir = path.join(frameworkRoot, 'public');
|
|
341
|
+
mirrorDir(frameworkPublicDir, sitePublicDir);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const cmsCore = {
|
|
345
|
+
name: '@koehler8/cms',
|
|
346
|
+
enforce: 'pre',
|
|
347
|
+
|
|
348
|
+
config(userConfig, env) {
|
|
349
|
+
// Resolve absolute paths
|
|
350
|
+
frameworkRoot = path.resolve(frameworkRootOption);
|
|
351
|
+
siteDir = path.isAbsolute(siteDirOption)
|
|
352
|
+
? siteDirOption
|
|
353
|
+
: path.resolve(process.cwd(), siteDirOption);
|
|
354
|
+
siteRoot = path.dirname(siteDir);
|
|
355
|
+
tempIndexPath = path.join(siteRoot, 'index.html');
|
|
356
|
+
tempEntryPath = path.join(siteRoot, ENTRY_FILENAME);
|
|
357
|
+
|
|
358
|
+
// Load site config at config-resolution time (needed for SSG routes + HTML injection)
|
|
359
|
+
const configDir = path.join(siteDir, 'config');
|
|
360
|
+
siteConfig = loadSplitConfig(configDir);
|
|
361
|
+
if (!siteConfig) {
|
|
362
|
+
throw new Error(`[@koehler8/cms] site config not found at ${configDir}`);
|
|
363
|
+
}
|
|
364
|
+
pagePaths = collectPagePaths(siteConfig);
|
|
365
|
+
metadata = extractSiteMetadata(siteConfig);
|
|
366
|
+
|
|
367
|
+
const extensionsDir = path.join(frameworkRoot, 'extensions');
|
|
368
|
+
const extensionSetupFiles = collectExtensionSetupFiles(extensionsDir);
|
|
369
|
+
|
|
370
|
+
const isTestEnv = process.env.VITEST === 'true';
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
resolve: {
|
|
374
|
+
alias: {
|
|
375
|
+
'@cms-site': siteDir,
|
|
376
|
+
'@cms-framework': frameworkRoot,
|
|
377
|
+
'@': path.join(frameworkRoot, 'src'),
|
|
378
|
+
'@extensions': extensionsDir,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
optimizeDeps: {
|
|
382
|
+
// vue and vue-router MUST NOT be pre-bundled (inlined) into
|
|
383
|
+
// @koehler8/cms chunks — if they are, .vue components served raw
|
|
384
|
+
// from node_modules get a different vue-router instance than
|
|
385
|
+
// the one ViteSSG used, and <RouterView> can't find the
|
|
386
|
+
// injected router context (renders empty).
|
|
387
|
+
exclude: ['vue', 'vue-router', 'pinia', '@koehler8/cms', ...extensionPackages],
|
|
388
|
+
include: [
|
|
389
|
+
// CJS deps imported directly by framework source
|
|
390
|
+
'ajv',
|
|
391
|
+
'ajv/dist/2020',
|
|
392
|
+
'ajv/dist/2020.js',
|
|
393
|
+
'ajv-formats',
|
|
394
|
+
// Auto-discovered CJS deps from extension packages.
|
|
395
|
+
// Extensions may depend on ESM packages that import CJS modules;
|
|
396
|
+
// these must be pre-bundled for proper default-export shims.
|
|
397
|
+
...discoverExtensionCjsDeps(extensionPackages, siteRoot),
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
root: siteRoot,
|
|
401
|
+
// Site repo's public dir contains generated favicon/logo/og-image.
|
|
402
|
+
// The plugin mirrors framework public files into it during
|
|
403
|
+
// configResolved so fonts, maps, etc. are also available.
|
|
404
|
+
publicDir: path.join(siteRoot, 'public'),
|
|
405
|
+
build: {
|
|
406
|
+
modulePreload: false,
|
|
407
|
+
rollupOptions: {
|
|
408
|
+
input: tempIndexPath,
|
|
409
|
+
output: {
|
|
410
|
+
manualChunks(id) {
|
|
411
|
+
if (
|
|
412
|
+
id.includes('node_modules/vue/') ||
|
|
413
|
+
id.includes('node_modules/vue-router/') ||
|
|
414
|
+
id.includes('node_modules/pinia/')
|
|
415
|
+
) {
|
|
416
|
+
return 'vendor-vue';
|
|
417
|
+
}
|
|
418
|
+
if (
|
|
419
|
+
id.includes('node_modules/chart.js/') ||
|
|
420
|
+
id.includes('node_modules/chartjs-plugin-datalabels/') ||
|
|
421
|
+
id.includes('node_modules/vue-chartjs/')
|
|
422
|
+
) {
|
|
423
|
+
return 'vendor-charts';
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
ssr: {
|
|
430
|
+
// Extension packages contain .vue SFCs and JSON imports that Node's
|
|
431
|
+
// native ESM resolver cannot handle. Marking them as noExternal
|
|
432
|
+
// tells Vite to bundle them during SSR so its Vue/JSON plugins
|
|
433
|
+
// process the files instead of Node.
|
|
434
|
+
noExternal: ['@koehler8/cms', ...extensionPackages],
|
|
435
|
+
},
|
|
436
|
+
test: {
|
|
437
|
+
environment: 'happy-dom',
|
|
438
|
+
globals: true,
|
|
439
|
+
include: ['tests/**/*.spec.{js,ts}', 'extensions/*/tests/**/*.spec.{js,ts}'],
|
|
440
|
+
setupFiles: extensionSetupFiles,
|
|
441
|
+
},
|
|
442
|
+
ssgOptions: {
|
|
443
|
+
dirStyle: 'nested',
|
|
444
|
+
includedRoutes(paths) {
|
|
445
|
+
const staticRoutes = new Set(['/admin']);
|
|
446
|
+
pagePaths.forEach((routePath) => staticRoutes.add(routePath));
|
|
447
|
+
|
|
448
|
+
const localizedRoutes = new Set();
|
|
449
|
+
locales.forEach((locale) => {
|
|
450
|
+
pagePaths.forEach((routePath) => {
|
|
451
|
+
const localized =
|
|
452
|
+
routePath === '/' ? `/${locale}` : `/${locale}${routePath}`;
|
|
453
|
+
localizedRoutes.add(localized);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const candidates = [
|
|
458
|
+
...paths.filter((routePath) => !routePath.includes(':')),
|
|
459
|
+
...Array.from(staticRoutes),
|
|
460
|
+
...Array.from(localizedRoutes),
|
|
461
|
+
];
|
|
462
|
+
return Array.from(new Set(candidates));
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
resolveId(id) {
|
|
469
|
+
if (VIRTUAL_IDS.has(id)) {
|
|
470
|
+
return resolved(id);
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
load(id) {
|
|
476
|
+
if (!isResolvedVirtual(id)) return null;
|
|
477
|
+
const virtualId = id.slice(1); // strip leading \0
|
|
478
|
+
|
|
479
|
+
if (virtualId === VIRTUAL_CONFIG) {
|
|
480
|
+
return `
|
|
481
|
+
import { createConfigLoader } from '@koehler8/cms/utils/loadConfig';
|
|
482
|
+
const allModules = import.meta.glob('@cms-site/config/**/*.json');
|
|
483
|
+
const loader = createConfigLoader(allModules);
|
|
484
|
+
export const loadConfigData = loader.loadConfigData;
|
|
485
|
+
export const mergeConfigTrees = loader.mergeConfigTrees;
|
|
486
|
+
export const cloneConfig = loader.cloneConfig;
|
|
487
|
+
`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (virtualId === VIRTUAL_STYLES) {
|
|
491
|
+
return `
|
|
492
|
+
import { createSiteStyleLoader } from '@koehler8/cms/utils/siteStyles';
|
|
493
|
+
const styleModules = import.meta.glob('@cms-site/style.css');
|
|
494
|
+
const loader = createSiteStyleLoader(styleModules);
|
|
495
|
+
export const ensureSiteStylesLoaded = loader.ensureSiteStylesLoaded;
|
|
496
|
+
`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (virtualId === VIRTUAL_ASSETS) {
|
|
500
|
+
return `
|
|
501
|
+
import { createAssetResolver } from '@koehler8/cms/utils/assetResolver';
|
|
502
|
+
const shared = import.meta.glob('@cms-framework/src/assets/**/*', {
|
|
503
|
+
eager: true,
|
|
504
|
+
query: '?url',
|
|
505
|
+
import: 'default',
|
|
506
|
+
});
|
|
507
|
+
const site = import.meta.glob([
|
|
508
|
+
'@cms-site/assets/**/*',
|
|
509
|
+
'!@cms-site/assets/_source/**',
|
|
510
|
+
], {
|
|
511
|
+
eager: true,
|
|
512
|
+
query: '?url',
|
|
513
|
+
import: 'default',
|
|
514
|
+
});
|
|
515
|
+
const resolver = createAssetResolver(shared, site);
|
|
516
|
+
export const resolveAssetUrl = resolver.resolveAssetUrl;
|
|
517
|
+
export const resolveAsset = resolver.resolveAsset;
|
|
518
|
+
export const resolveMedia = resolver.resolveMedia;
|
|
519
|
+
export const assetUrlMap = resolver.assetUrlMap;
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return null;
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
configResolved(resolvedConfig) {
|
|
527
|
+
// Write temp files as soon as config is resolved so vite-ssg's
|
|
528
|
+
// detectEntry (which runs before Vite's build lifecycle) finds them.
|
|
529
|
+
writeTempFiles();
|
|
530
|
+
|
|
531
|
+
// Mirror framework's public/ into the site's public/ so fonts, maps,
|
|
532
|
+
// robots.txt, etc. end up in dist. Site-specific files (favicon.ico,
|
|
533
|
+
// logo.png, og-image.jpg written by generate-public-assets) win over
|
|
534
|
+
// any accidental collisions because we skip files that already exist.
|
|
535
|
+
syncPublicDir();
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
buildEnd() {
|
|
539
|
+
// Only clean up during actual builds, not dev server
|
|
540
|
+
if (!this._isDevServer) cleanupTempFiles();
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
closeBundle() {
|
|
544
|
+
// Only clean up during actual builds, not dev server
|
|
545
|
+
if (!this._isDevServer) cleanupTempFiles();
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
configureServer(server) {
|
|
549
|
+
// Mark that we're running as a dev server so buildEnd/closeBundle
|
|
550
|
+
// don't prematurely delete the temp files.
|
|
551
|
+
this._isDevServer = true;
|
|
552
|
+
|
|
553
|
+
// Clean up only when the dev server actually closes
|
|
554
|
+
server.httpServer?.once('close', cleanupTempFiles);
|
|
555
|
+
process.once('exit', cleanupTempFiles);
|
|
556
|
+
process.once('SIGINT', () => { cleanupTempFiles(); process.exit(0); });
|
|
557
|
+
process.once('SIGTERM', () => { cleanupTempFiles(); process.exit(0); });
|
|
558
|
+
|
|
559
|
+
// Serve the entry JS directly from memory so it's always available
|
|
560
|
+
// even if the temp file was deleted by a stale process or race condition.
|
|
561
|
+
// Use Vite's transformRequest to resolve bare module specifiers.
|
|
562
|
+
server.middlewares.use(async (req, res, next) => {
|
|
563
|
+
const url = req.url?.split('?')[0];
|
|
564
|
+
if (url === `/${ENTRY_FILENAME}`) {
|
|
565
|
+
try {
|
|
566
|
+
// Ensure the temp file exists for Vite's module graph
|
|
567
|
+
if (!fs.existsSync(tempEntryPath)) {
|
|
568
|
+
fs.writeFileSync(tempEntryPath, buildEntrySource(themePackages, extensionPackages), 'utf-8');
|
|
569
|
+
}
|
|
570
|
+
// Let Vite transform it (resolves bare specifiers, applies plugins)
|
|
571
|
+
const result = await server.transformRequest(`/${ENTRY_FILENAME}`);
|
|
572
|
+
if (result) {
|
|
573
|
+
res.statusCode = 200;
|
|
574
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
575
|
+
res.end(result.code);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {
|
|
579
|
+
// Fall through to next middleware
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
next();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Watch site content for HMR
|
|
586
|
+
server.watcher.add(siteDir);
|
|
587
|
+
|
|
588
|
+
// Invalidate virtual modules when site content changes
|
|
589
|
+
const invalidate = (file) => {
|
|
590
|
+
if (!file.startsWith(siteDir)) return;
|
|
591
|
+
for (const id of VIRTUAL_IDS) {
|
|
592
|
+
const mod = server.moduleGraph.getModuleById(resolved(id));
|
|
593
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
594
|
+
}
|
|
595
|
+
server.ws.send({ type: 'full-reload' });
|
|
596
|
+
};
|
|
597
|
+
server.watcher.on('change', invalidate);
|
|
598
|
+
server.watcher.on('add', invalidate);
|
|
599
|
+
server.watcher.on('unlink', invalidate);
|
|
600
|
+
|
|
601
|
+
// Serve index.html on root request in dev mode
|
|
602
|
+
return () => {
|
|
603
|
+
server.middlewares.use(async (req, res, next) => {
|
|
604
|
+
if (req.url !== '/' && req.url !== '/index.html') {
|
|
605
|
+
return next();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const template = readIndexTemplate(frameworkRoot);
|
|
610
|
+
let html = renderTemplate(template, {
|
|
611
|
+
site: metadata.siteTitle,
|
|
612
|
+
siteDescription: metadata.siteDescription,
|
|
613
|
+
siteUrl: metadata.siteUrl,
|
|
614
|
+
siteGoogleId: metadata.siteGoogleId,
|
|
615
|
+
siteSameAsJson: metadata.siteSameAsJson,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
html = html.replace(
|
|
619
|
+
/<script\s+type="module"\s+src="\/src\/main\.js"\s*>\s*<\/script>/,
|
|
620
|
+
`<script type="module" src="/${ENTRY_FILENAME}"></script>`
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
html = await server.transformIndexHtml(req.url, html, req.originalUrl);
|
|
624
|
+
|
|
625
|
+
res.statusCode = 200;
|
|
626
|
+
res.setHeader('Content-Type', 'text/html');
|
|
627
|
+
res.end(html);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
next(err);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
return [vue(), cmsCore];
|
|
637
|
+
}
|