@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
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { defineAsyncComponent } from 'vue';
|
|
2
|
+
|
|
3
|
+
import manifestSchema from '../../extensions/manifest.schema.json' with { type: 'json' };
|
|
4
|
+
import { unwrapDefault as toPlainModule } from '../utils/unwrapDefault.js';
|
|
5
|
+
|
|
6
|
+
const manifestModules = import.meta.glob('../../extensions/**/extension.config.json', {
|
|
7
|
+
eager: true,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const contentDefaultModules = import.meta.glob('../../extensions/**/content.defaults.json', {
|
|
11
|
+
eager: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const entryModules = import.meta.glob([
|
|
15
|
+
'../../extensions/**/*.{js,ts,mjs,cjs}',
|
|
16
|
+
'!../../extensions/**/tests/**',
|
|
17
|
+
'!**/*.spec.*',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const componentModules = import.meta.glob([
|
|
21
|
+
'../../extensions/**/*.{vue,js,ts,mjs,cjs}',
|
|
22
|
+
'!../../extensions/**/tests/**',
|
|
23
|
+
'!**/*.spec.*',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// Lazy-load ajv via dynamic import — the static `import Ajv from 'ajv/...'`
|
|
27
|
+
// fails when Vite serves this file outside the dep optimizer (CJS module
|
|
28
|
+
// without a default export shim). Dynamic import always gets the shim.
|
|
29
|
+
let validateManifest;
|
|
30
|
+
async function getValidator() {
|
|
31
|
+
if (!validateManifest) {
|
|
32
|
+
const [{ default: Ajv }, ajvFormats] = await Promise.all([
|
|
33
|
+
import('ajv/dist/2020'),
|
|
34
|
+
import('ajv-formats'),
|
|
35
|
+
]);
|
|
36
|
+
const addFormats = ajvFormats.default || ajvFormats;
|
|
37
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
38
|
+
addFormats(ajv);
|
|
39
|
+
validateManifest = ajv.compile(manifestSchema);
|
|
40
|
+
}
|
|
41
|
+
return validateManifest;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const extensionComponentRegistry = {};
|
|
45
|
+
const extensionComponentRegistryBySource = {};
|
|
46
|
+
const extensionComponentSourcesByName = {};
|
|
47
|
+
const extensionComponentCatalog = [];
|
|
48
|
+
const extensionContentDefaults = {};
|
|
49
|
+
const extensionSetupFns = [];
|
|
50
|
+
|
|
51
|
+
const DEV = import.meta.env.DEV;
|
|
52
|
+
|
|
53
|
+
const warn = (...args) => {
|
|
54
|
+
if (DEV) {
|
|
55
|
+
console.warn('[extensions]', ...args);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const resolveEntryPath = (manifestPath, entryPath) => {
|
|
60
|
+
if (!manifestPath || !entryPath) return null;
|
|
61
|
+
const normalizedEntry = entryPath.replace(/^\.\/+/, '');
|
|
62
|
+
if (!normalizedEntry) return null;
|
|
63
|
+
const manifestDir = manifestPath.replace(/extension\.config\.json$/i, '');
|
|
64
|
+
return `${manifestDir}${normalizedEntry}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const registerComponentMeta = (args) => {
|
|
68
|
+
extensionComponentCatalog.push({
|
|
69
|
+
name: args.name,
|
|
70
|
+
description: args.description || '',
|
|
71
|
+
configKey: args.configKey,
|
|
72
|
+
allowedPages: Array.isArray(args.allowedPages) && args.allowedPages.length
|
|
73
|
+
? [...args.allowedPages]
|
|
74
|
+
: null,
|
|
75
|
+
propsInterface: args.propsInterface || null,
|
|
76
|
+
requiredContent: Array.isArray(args.requiredContent) && args.requiredContent.length
|
|
77
|
+
? [...args.requiredContent]
|
|
78
|
+
: null,
|
|
79
|
+
minAppVersion: args.minAppVersion || null,
|
|
80
|
+
extension: {
|
|
81
|
+
slug: args.slug,
|
|
82
|
+
version: args.version,
|
|
83
|
+
provider: args.provider,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const createAsyncComponent = (loader, context) => {
|
|
89
|
+
return defineAsyncComponent(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const mod = await loader();
|
|
92
|
+
const component = toPlainModule(mod);
|
|
93
|
+
if (!component) {
|
|
94
|
+
throw new Error(`Module did not export a usable component.`);
|
|
95
|
+
}
|
|
96
|
+
return component;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
warn(
|
|
99
|
+
`Failed to load component module "${context?.componentName || ''}" from "${context?.slug || 'unknown'}".`,
|
|
100
|
+
error,
|
|
101
|
+
);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const resolveComponentModule = (manifestPath, modulePath = '', context = {}) => {
|
|
108
|
+
if (!modulePath) return null;
|
|
109
|
+
const normalizedModule = modulePath.replace(/^\.\/+/, '');
|
|
110
|
+
if (!normalizedModule) return null;
|
|
111
|
+
const manifestDir = manifestPath.replace(/extension\.config\.json$/i, '');
|
|
112
|
+
const fullPath = `${manifestDir}${normalizedModule}`;
|
|
113
|
+
const loader = componentModules[fullPath];
|
|
114
|
+
if (!loader) {
|
|
115
|
+
warn(`Module "${modulePath}" declared in manifest "${context?.slug || 'unknown'}" could not be found.`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return createAsyncComponent(loader, context);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const manifestEntries = Object.entries(manifestModules);
|
|
122
|
+
|
|
123
|
+
await Promise.all(
|
|
124
|
+
manifestEntries.map(async ([manifestPath, module]) => {
|
|
125
|
+
const manifest = toPlainModule(module);
|
|
126
|
+
if (!manifest) {
|
|
127
|
+
warn(`Manifest at ${manifestPath} did not export a value.`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const validate = await getValidator();
|
|
132
|
+
const isValid = validate(manifest);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
warn(`Manifest at ${manifestPath} failed validation.`, validate.errors);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const needsEntry = manifest.components.some((component) => !component?.module);
|
|
139
|
+
let exportedComponents = {};
|
|
140
|
+
|
|
141
|
+
if (needsEntry) {
|
|
142
|
+
const entryPath = resolveEntryPath(manifestPath, manifest.entry);
|
|
143
|
+
if (!entryPath) {
|
|
144
|
+
warn(`Manifest "${manifest.slug}" is missing a valid entry file.`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const entryLoader = entryModules[entryPath];
|
|
149
|
+
if (!entryLoader) {
|
|
150
|
+
warn(`Entry file "${entryPath}" for manifest "${manifest.slug}" could not be found.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let entryModule;
|
|
155
|
+
try {
|
|
156
|
+
entryModule = await entryLoader();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
warn(`Entry module "${entryPath}" for manifest "${manifest.slug}" failed to load.`, error);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let entry = toPlainModule(entryModule);
|
|
163
|
+
if (typeof entry === 'function') {
|
|
164
|
+
try {
|
|
165
|
+
entry = entry();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
warn(`Entry module "${entryPath}" threw during execution.`, error);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!entry) {
|
|
172
|
+
warn(`Entry module "${entryPath}" did not export anything usable.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
exportedComponents = entry.components && typeof entry.components === 'object'
|
|
177
|
+
? entry.components
|
|
178
|
+
: entry;
|
|
179
|
+
|
|
180
|
+
if (!exportedComponents || typeof exportedComponents !== 'object') {
|
|
181
|
+
warn(`Entry module "${entryPath}" must export a components object.`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const componentMeta of manifest.components) {
|
|
187
|
+
const componentName = componentMeta?.name;
|
|
188
|
+
if (!componentName) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const canonicalSlug = typeof manifest.slug === 'string' ? manifest.slug.trim() : '';
|
|
192
|
+
const normalizedSlug = canonicalSlug ? canonicalSlug.toLowerCase() : '';
|
|
193
|
+
const existingEntry = extensionComponentRegistry[componentName];
|
|
194
|
+
const hasExistingName = Boolean(existingEntry);
|
|
195
|
+
if (hasExistingName) {
|
|
196
|
+
warn(
|
|
197
|
+
`Component "${componentName}" from "${manifest.slug}" conflicts with existing registration from "${existingEntry.slug}". ` +
|
|
198
|
+
`The global lookup will resolve to the "${existingEntry.slug}" version. ` +
|
|
199
|
+
`Use source-qualified syntax "${normalizedSlug}:${componentName}" in site configs to reference this version.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let normalizedComponent = null;
|
|
204
|
+
|
|
205
|
+
if (componentMeta.module) {
|
|
206
|
+
normalizedComponent = resolveComponentModule(manifestPath, componentMeta.module, {
|
|
207
|
+
slug: manifest.slug,
|
|
208
|
+
componentName,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!normalizedComponent) {
|
|
213
|
+
const exported = exportedComponents[componentName];
|
|
214
|
+
normalizedComponent = toPlainModule(exported) || exported;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!normalizedComponent) {
|
|
218
|
+
warn(`Component "${componentName}" declared in manifest "${manifest.slug}" is missing from the entry exports.`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const definition = {
|
|
223
|
+
component: normalizedComponent,
|
|
224
|
+
configKey: componentMeta.configKey || null,
|
|
225
|
+
allowedPages: Array.isArray(componentMeta.allowedPages) && componentMeta.allowedPages.length
|
|
226
|
+
? [...componentMeta.allowedPages]
|
|
227
|
+
: null,
|
|
228
|
+
minAppVersion: typeof componentMeta.minAppVersion === 'string' && componentMeta.minAppVersion.trim()
|
|
229
|
+
? componentMeta.minAppVersion.trim()
|
|
230
|
+
: null,
|
|
231
|
+
requiredContent: Array.isArray(componentMeta.requiredContent) && componentMeta.requiredContent.length
|
|
232
|
+
? [...componentMeta.requiredContent]
|
|
233
|
+
: null,
|
|
234
|
+
propsInterface: componentMeta.propsInterface || null,
|
|
235
|
+
slug: canonicalSlug || manifest.slug,
|
|
236
|
+
normalizedSlug,
|
|
237
|
+
};
|
|
238
|
+
if (!hasExistingName) {
|
|
239
|
+
extensionComponentRegistry[componentName] = definition;
|
|
240
|
+
}
|
|
241
|
+
if (normalizedSlug) {
|
|
242
|
+
const sourceKey = `${normalizedSlug}:${componentName}`;
|
|
243
|
+
extensionComponentRegistryBySource[sourceKey] = definition;
|
|
244
|
+
const existingSources = extensionComponentSourcesByName[componentName] || new Set();
|
|
245
|
+
existingSources.add(normalizedSlug);
|
|
246
|
+
extensionComponentSourcesByName[componentName] = existingSources;
|
|
247
|
+
}
|
|
248
|
+
registerComponentMeta({
|
|
249
|
+
name: componentName,
|
|
250
|
+
description: componentMeta.description,
|
|
251
|
+
configKey: componentMeta.configKey,
|
|
252
|
+
allowedPages: componentMeta.allowedPages,
|
|
253
|
+
propsInterface: componentMeta.propsInterface,
|
|
254
|
+
requiredContent: componentMeta.requiredContent,
|
|
255
|
+
minAppVersion: componentMeta.minAppVersion,
|
|
256
|
+
slug: manifest.slug,
|
|
257
|
+
version: manifest.version,
|
|
258
|
+
provider: manifest.provider,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Load content defaults keyed by extension slug
|
|
265
|
+
for (const [path, module] of Object.entries(contentDefaultModules)) {
|
|
266
|
+
const defaults = toPlainModule(module);
|
|
267
|
+
if (!defaults || typeof defaults !== 'object') continue;
|
|
268
|
+
const slugMatch = path.match(/extensions\/([^/]+)\/content\.defaults\.json$/);
|
|
269
|
+
if (!slugMatch) continue;
|
|
270
|
+
const slug = slugMatch[1].toLowerCase();
|
|
271
|
+
extensionContentDefaults[slug] = Object.freeze(defaults);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Register an external extension package.
|
|
276
|
+
*
|
|
277
|
+
* Called by the Vite plugin for each extension passed via the `extensions` option.
|
|
278
|
+
* Must be called before the first render (during app initialisation).
|
|
279
|
+
*
|
|
280
|
+
* @param {object} extensionModule - Default export from a @koehler8/cms-ext-* package
|
|
281
|
+
* Expected shape: { manifest, components (import.meta.glob result), contentDefaults? }
|
|
282
|
+
*/
|
|
283
|
+
export async function registerExtension(extensionModule) {
|
|
284
|
+
if (!extensionModule || typeof extensionModule !== 'object') {
|
|
285
|
+
throw new Error('[extensions] registerExtension() requires an extension module object');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const manifest = extensionModule.manifest;
|
|
289
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
290
|
+
throw new Error('[extensions] registerExtension() module must include a manifest');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const validate = await getValidator();
|
|
294
|
+
const isValid = validate(manifest);
|
|
295
|
+
if (!isValid) {
|
|
296
|
+
warn(`External extension "${manifest.slug}" failed validation.`, validate.errors);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const externalComponents = extensionModule.components || {};
|
|
301
|
+
const externalDefaults = extensionModule.contentDefaults;
|
|
302
|
+
|
|
303
|
+
// Build a component lookup from the external glob result.
|
|
304
|
+
// Keys are relative paths like './components/Cookies.vue'; manifest module paths
|
|
305
|
+
// also start with './'. We normalise both for matching.
|
|
306
|
+
const normalisePath = (p) => p.replace(/^\.\/+/, '');
|
|
307
|
+
|
|
308
|
+
const componentLoaders = {};
|
|
309
|
+
for (const [key, loader] of Object.entries(externalComponents)) {
|
|
310
|
+
componentLoaders[normalisePath(key)] = loader;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If the extension has an entry file (like compliance), try to load it
|
|
314
|
+
let exportedComponents = {};
|
|
315
|
+
const needsEntry = manifest.components.some((c) => !c?.module);
|
|
316
|
+
if (needsEntry && manifest.entry) {
|
|
317
|
+
const entryNorm = normalisePath(manifest.entry);
|
|
318
|
+
const entryLoader = componentLoaders[entryNorm];
|
|
319
|
+
if (entryLoader) {
|
|
320
|
+
try {
|
|
321
|
+
const entryModule = await entryLoader();
|
|
322
|
+
let entry = toPlainModule(entryModule);
|
|
323
|
+
if (typeof entry === 'function') entry = entry();
|
|
324
|
+
exportedComponents = entry?.components || entry || {};
|
|
325
|
+
} catch (error) {
|
|
326
|
+
warn(`External extension "${manifest.slug}" entry failed to load.`, error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const componentMeta of manifest.components) {
|
|
332
|
+
const componentName = componentMeta?.name;
|
|
333
|
+
if (!componentName) continue;
|
|
334
|
+
|
|
335
|
+
const canonicalSlug = (manifest.slug || '').trim();
|
|
336
|
+
const normalizedSlug = canonicalSlug.toLowerCase();
|
|
337
|
+
const hasExistingName = Boolean(extensionComponentRegistry[componentName]);
|
|
338
|
+
|
|
339
|
+
let normalizedComponent = null;
|
|
340
|
+
|
|
341
|
+
// Try module path from manifest
|
|
342
|
+
if (componentMeta.module) {
|
|
343
|
+
const moduleNorm = normalisePath(componentMeta.module);
|
|
344
|
+
const loader = componentLoaders[moduleNorm];
|
|
345
|
+
if (loader) {
|
|
346
|
+
normalizedComponent = createAsyncComponent(loader, {
|
|
347
|
+
slug: manifest.slug,
|
|
348
|
+
componentName,
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
warn(`Component "${componentName}" module "${componentMeta.module}" not found in "${manifest.slug}" package.`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fall back to entry exports
|
|
356
|
+
if (!normalizedComponent) {
|
|
357
|
+
const exported = exportedComponents[componentName];
|
|
358
|
+
normalizedComponent = toPlainModule(exported) || exported;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!normalizedComponent) {
|
|
362
|
+
warn(`Component "${componentName}" from external "${manifest.slug}" could not be resolved.`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (hasExistingName) {
|
|
367
|
+
warn(
|
|
368
|
+
`Component "${componentName}" from "${manifest.slug}" conflicts with existing registration. ` +
|
|
369
|
+
`Use source-qualified syntax "${normalizedSlug}:${componentName}".`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const definition = {
|
|
374
|
+
component: normalizedComponent,
|
|
375
|
+
configKey: componentMeta.configKey || null,
|
|
376
|
+
allowedPages: Array.isArray(componentMeta.allowedPages) && componentMeta.allowedPages.length
|
|
377
|
+
? [...componentMeta.allowedPages]
|
|
378
|
+
: null,
|
|
379
|
+
minAppVersion: typeof componentMeta.minAppVersion === 'string' && componentMeta.minAppVersion.trim()
|
|
380
|
+
? componentMeta.minAppVersion.trim()
|
|
381
|
+
: null,
|
|
382
|
+
requiredContent: Array.isArray(componentMeta.requiredContent) && componentMeta.requiredContent.length
|
|
383
|
+
? [...componentMeta.requiredContent]
|
|
384
|
+
: null,
|
|
385
|
+
propsInterface: componentMeta.propsInterface || null,
|
|
386
|
+
slug: canonicalSlug || manifest.slug,
|
|
387
|
+
normalizedSlug,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (!hasExistingName) {
|
|
391
|
+
extensionComponentRegistry[componentName] = definition;
|
|
392
|
+
}
|
|
393
|
+
if (normalizedSlug) {
|
|
394
|
+
extensionComponentRegistryBySource[`${normalizedSlug}:${componentName}`] = definition;
|
|
395
|
+
const existingSources = extensionComponentSourcesByName[componentName] || new Set();
|
|
396
|
+
existingSources.add(normalizedSlug);
|
|
397
|
+
extensionComponentSourcesByName[componentName] = existingSources;
|
|
398
|
+
}
|
|
399
|
+
registerComponentMeta({
|
|
400
|
+
name: componentName,
|
|
401
|
+
description: componentMeta.description,
|
|
402
|
+
configKey: componentMeta.configKey,
|
|
403
|
+
allowedPages: componentMeta.allowedPages,
|
|
404
|
+
propsInterface: componentMeta.propsInterface,
|
|
405
|
+
requiredContent: componentMeta.requiredContent,
|
|
406
|
+
minAppVersion: componentMeta.minAppVersion,
|
|
407
|
+
slug: manifest.slug,
|
|
408
|
+
version: manifest.version,
|
|
409
|
+
provider: manifest.provider,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Load content defaults
|
|
414
|
+
if (externalDefaults && typeof externalDefaults === 'object') {
|
|
415
|
+
const slug = (manifest.slug || '').toLowerCase();
|
|
416
|
+
if (slug) {
|
|
417
|
+
extensionContentDefaults[slug] = Object.freeze(
|
|
418
|
+
typeof externalDefaults.default === 'object' ? externalDefaults.default : externalDefaults,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Register setup function if provided
|
|
424
|
+
if (typeof extensionModule.setup === 'function') {
|
|
425
|
+
extensionSetupFns.push({ slug: manifest.slug, setup: extensionModule.setup });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// NOTE: These frozen snapshots are taken after bundled extension discovery.
|
|
430
|
+
// External extensions registered via registerExtension() write directly to the
|
|
431
|
+
// mutable registries above, which the getter functions read from.
|
|
432
|
+
const extensionComponentDefinitions = extensionComponentRegistry;
|
|
433
|
+
const extensionComponentDefinitionsBySource = extensionComponentRegistryBySource;
|
|
434
|
+
// Live proxy so external extensions registered via registerExtension() are
|
|
435
|
+
// visible to the component resolver (the old frozen snapshot missed them).
|
|
436
|
+
const extensionComponentSources = new Proxy({}, {
|
|
437
|
+
get(_, name) {
|
|
438
|
+
const sourceSet = extensionComponentSourcesByName[name];
|
|
439
|
+
return sourceSet ? [...sourceSet] : undefined;
|
|
440
|
+
},
|
|
441
|
+
has(_, name) {
|
|
442
|
+
return name in extensionComponentSourcesByName;
|
|
443
|
+
},
|
|
444
|
+
ownKeys() {
|
|
445
|
+
return Object.keys(extensionComponentSourcesByName);
|
|
446
|
+
},
|
|
447
|
+
getOwnPropertyDescriptor(_, name) {
|
|
448
|
+
if (name in extensionComponentSourcesByName) {
|
|
449
|
+
return { configurable: true, enumerable: true, value: [...extensionComponentSourcesByName[name]] };
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
export const registeredExtensionComponents = new Proxy({}, {
|
|
456
|
+
get(_, name) {
|
|
457
|
+
const entry = extensionComponentRegistry[name];
|
|
458
|
+
return entry ? entry.component : null;
|
|
459
|
+
},
|
|
460
|
+
ownKeys() {
|
|
461
|
+
return Object.keys(extensionComponentRegistry);
|
|
462
|
+
},
|
|
463
|
+
getOwnPropertyDescriptor(_, name) {
|
|
464
|
+
if (name in extensionComponentRegistry) {
|
|
465
|
+
return { configurable: true, enumerable: true, value: extensionComponentRegistry[name].component };
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
export const extensionComponentsCatalog = extensionComponentCatalog;
|
|
471
|
+
|
|
472
|
+
export function getExtensionComponent(name) {
|
|
473
|
+
const entry = extensionComponentRegistry[name];
|
|
474
|
+
return entry ? entry.component : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function getExtensionComponentDefinition(name) {
|
|
478
|
+
return extensionComponentRegistry[name] || null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function getExtensionContentDefaults(slug, configKey) {
|
|
482
|
+
if (!slug || !configKey) return undefined;
|
|
483
|
+
const normalizedSlug = slug.toLowerCase();
|
|
484
|
+
const defaults = extensionContentDefaults[normalizedSlug];
|
|
485
|
+
if (!defaults || typeof defaults !== 'object') return undefined;
|
|
486
|
+
return defaults[configKey] || undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Run setup functions registered by external extensions.
|
|
491
|
+
*
|
|
492
|
+
* Called once during app initialisation (inside the ViteSSG callback) after
|
|
493
|
+
* Pinia and the router are available. Gives extensions access to the Vue app
|
|
494
|
+
* instance so they can install plugins, register stores, and set up providers.
|
|
495
|
+
*
|
|
496
|
+
* @param {object} ctx - { app, router, pinia, siteData, isClient }
|
|
497
|
+
*/
|
|
498
|
+
export async function runExtensionSetups(ctx) {
|
|
499
|
+
for (const { slug, setup } of extensionSetupFns) {
|
|
500
|
+
try {
|
|
501
|
+
await setup(ctx);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
warn(`Setup for extension "${slug}" failed.`, err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export {
|
|
509
|
+
extensionComponentDefinitions,
|
|
510
|
+
extensionComponentDefinitionsBySource,
|
|
511
|
+
extensionComponentSources,
|
|
512
|
+
};
|
package/src/main.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ViteSSG } from 'vite-ssg';
|
|
2
|
+
import { createPinia } from 'pinia';
|
|
3
|
+
import { createHead } from '@unhead/vue/client';
|
|
4
|
+
|
|
5
|
+
import App from './App.vue';
|
|
6
|
+
import { routes, resolveHistory, applyRouterGuards } from './router/index.js';
|
|
7
|
+
|
|
8
|
+
import { shouldEnableAnalytics, scheduleAnalyticsLoad } from './utils/cookieConsent.js';
|
|
9
|
+
import { loadConfigData } from './utils/loadConfig.js';
|
|
10
|
+
import { persistAttributionFromLocation } from './utils/trackingContext.js';
|
|
11
|
+
import { applyThemeVariables } from './themes/themeManager.js';
|
|
12
|
+
import { setActiveThemeKey } from './utils/themeColors.js';
|
|
13
|
+
import { ensureSiteStylesLoaded } from './utils/siteStyles.js';
|
|
14
|
+
import { runExtensionSetups } from './extensions/extensionLoader.js';
|
|
15
|
+
|
|
16
|
+
const normalizeThemeKey = (value) => {
|
|
17
|
+
if (typeof value !== 'string') return '';
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed.length ? trimmed.toLowerCase() : '';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const extractThemeKey = (siteData) => {
|
|
23
|
+
if (!siteData || typeof siteData !== 'object') return '';
|
|
24
|
+
const site = siteData.site && typeof siteData.site === 'object' ? siteData.site : {};
|
|
25
|
+
if (typeof site.theme === 'string' && site.theme.trim()) {
|
|
26
|
+
return site.theme.trim();
|
|
27
|
+
}
|
|
28
|
+
if (site.theme && typeof site.theme === 'object') {
|
|
29
|
+
if (typeof site.theme.key === 'string' && site.theme.key.trim()) {
|
|
30
|
+
return site.theme.key.trim();
|
|
31
|
+
}
|
|
32
|
+
if (typeof site.theme.name === 'string' && site.theme.name.trim()) {
|
|
33
|
+
return site.theme.name.trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (typeof site.themeKey === 'string' && site.themeKey.trim()) {
|
|
37
|
+
return site.themeKey.trim();
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const applySiteTheme = (themeKey) => {
|
|
43
|
+
if (typeof document === 'undefined') return;
|
|
44
|
+
const normalized = normalizeThemeKey(themeKey);
|
|
45
|
+
const resolved = normalized || 'base';
|
|
46
|
+
document.documentElement.dataset.siteTheme = resolved;
|
|
47
|
+
applyThemeVariables(resolved);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function createCmsApp() {
|
|
51
|
+
ensureSiteStylesLoaded();
|
|
52
|
+
|
|
53
|
+
if (typeof window !== 'undefined') {
|
|
54
|
+
persistAttributionFromLocation();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return ViteSSG(
|
|
58
|
+
App,
|
|
59
|
+
{
|
|
60
|
+
routes,
|
|
61
|
+
history: resolveHistory(),
|
|
62
|
+
},
|
|
63
|
+
async (ctx) => {
|
|
64
|
+
const { app, router, isClient, initialState } = ctx;
|
|
65
|
+
const pinia = createPinia();
|
|
66
|
+
app.use(pinia);
|
|
67
|
+
|
|
68
|
+
const existingHead = app._context?.provides?.usehead;
|
|
69
|
+
const headInstance = existingHead || createHead();
|
|
70
|
+
if (!existingHead) {
|
|
71
|
+
app.use(headInstance);
|
|
72
|
+
}
|
|
73
|
+
ctx.head = headInstance;
|
|
74
|
+
|
|
75
|
+
if (initialState.siteTheme) {
|
|
76
|
+
setActiveThemeKey(initialState.siteTheme);
|
|
77
|
+
if (typeof document !== 'undefined') {
|
|
78
|
+
applySiteTheme(initialState.siteTheme);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
applyRouterGuards(router);
|
|
83
|
+
|
|
84
|
+
const pickLocaleParam = (value) => {
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
for (const entry of value) {
|
|
87
|
+
if (typeof entry === 'string') {
|
|
88
|
+
const trimmed = entry.trim();
|
|
89
|
+
if (trimmed.length) {
|
|
90
|
+
return trimmed;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === 'string') {
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
return trimmed.length ? trimmed : undefined;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeLocaleKey = (value) => {
|
|
104
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
105
|
+
return 'default';
|
|
106
|
+
}
|
|
107
|
+
return value.trim().toLowerCase();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const extractCurrentRoute = () => {
|
|
111
|
+
if (ctx?.route) {
|
|
112
|
+
return ctx.route;
|
|
113
|
+
}
|
|
114
|
+
const maybeRoute = router?.currentRoute;
|
|
115
|
+
if (maybeRoute && typeof maybeRoute === 'object' && 'value' in maybeRoute) {
|
|
116
|
+
return maybeRoute.value;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const loadSiteConfig = async (options = {}) => {
|
|
122
|
+
const requestedLocale =
|
|
123
|
+
pickLocaleParam(options.locale) ??
|
|
124
|
+
pickLocaleParam(extractCurrentRoute()?.params?.locale);
|
|
125
|
+
|
|
126
|
+
const localeKey = normalizeLocaleKey(requestedLocale);
|
|
127
|
+
|
|
128
|
+
if (initialState.siteConfig && initialState.siteConfigLocale === localeKey) {
|
|
129
|
+
const existingTheme =
|
|
130
|
+
initialState.siteTheme ?? normalizeThemeKey(extractThemeKey(initialState.siteConfig));
|
|
131
|
+
initialState.siteTheme = existingTheme;
|
|
132
|
+
setActiveThemeKey(existingTheme);
|
|
133
|
+
applySiteTheme(existingTheme);
|
|
134
|
+
return initialState.siteConfig;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const siteData = await loadConfigData({ locale: requestedLocale });
|
|
138
|
+
const themeKey = normalizeThemeKey(extractThemeKey(siteData));
|
|
139
|
+
initialState.siteConfig = siteData;
|
|
140
|
+
initialState.siteConfigLocale = localeKey;
|
|
141
|
+
initialState.siteTheme = themeKey;
|
|
142
|
+
setActiveThemeKey(themeKey);
|
|
143
|
+
applySiteTheme(themeKey);
|
|
144
|
+
return siteData;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (!isClient) {
|
|
148
|
+
try {
|
|
149
|
+
await loadSiteConfig();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Failed to load site configuration during SSG build', error);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let siteData;
|
|
157
|
+
try {
|
|
158
|
+
siteData = await loadSiteConfig();
|
|
159
|
+
if (shouldEnableAnalytics()) {
|
|
160
|
+
const googleId = siteData?.site?.googleId;
|
|
161
|
+
if (googleId) scheduleAnalyticsLoad(googleId);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Failed to load site configuration on client', error);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await runExtensionSetups({ app, router, pinia, siteData, isClient });
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// CSS imports — framework-owned styles
|
|
173
|
+
import './styles/base.css';
|
|
174
|
+
import './styles/layout.css';
|
|
175
|
+
import './styles/theme-base.css';
|