@koehler8/cms 1.0.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1491 -0
- package/README.md +108 -28
- package/bin/cms-create-extension.js +7 -0
- package/bin/cms-create-site.js +7 -0
- package/bin/cms-create-theme.js +7 -0
- package/bin/cms-ssg-build.js +13 -0
- package/bin/cms-validate-extensions.js +4 -0
- package/bin/cms-validate-themes.js +11 -0
- package/extensions/manifest.schema.json +1 -1
- package/package.json +36 -16
- package/scripts/create-extension.mjs +11 -0
- package/scripts/create-site.mjs +11 -0
- package/scripts/create-theme.mjs +11 -0
- package/scripts/generate-public-assets.js +70 -36
- package/scripts/image-variants/cache.js +40 -0
- package/scripts/image-variants/classifier.js +53 -0
- package/scripts/image-variants/config.js +54 -0
- package/scripts/image-variants/index.js +18 -0
- package/scripts/image-variants/manifest.js +76 -0
- package/scripts/image-variants/planner.js +95 -0
- package/scripts/image-variants/reconcile.js +194 -0
- package/scripts/image-variants/renderer.js +51 -0
- package/scripts/lib/extension-manifests.mjs +70 -0
- package/scripts/scaffold.mjs +242 -0
- package/scripts/ssg-build/findEmptyRenders.mjs +42 -0
- package/scripts/ssg-build/index.mjs +253 -0
- package/scripts/validate-extensions.mjs +50 -382
- package/scripts/validate-themes.mjs +37 -5
- package/src/components/About.vue +4 -2
- package/src/components/AboutValue.vue +5 -4
- package/src/components/ComingSoon.vue +12 -1
- package/src/components/ComingSoonModal.vue +33 -61
- package/src/components/Contact.vue +216 -120
- package/src/components/DraftGate.vue +211 -0
- package/src/components/Footer.vue +15 -2
- package/src/components/FooterMinimal.vue +13 -12
- package/src/components/Header.vue +184 -10
- package/src/components/Hero.vue +17 -8
- package/src/components/Home.vue +81 -48
- package/src/components/Intro.vue +4 -2
- package/src/components/IntroGate.vue +30 -73
- package/src/components/NotFound.vue +100 -0
- package/src/components/Plan.vue +10 -4
- package/src/components/Portfolio.vue +29 -3
- package/src/components/Principles.vue +3 -1
- package/src/components/Spacer.vue +23 -0
- package/src/components/Spacer15.vue +3 -4
- package/src/components/Spacer30.vue +3 -4
- package/src/components/Spacer40.vue +3 -4
- package/src/components/Spacer60.vue +3 -4
- package/src/components/StickyCTA.vue +23 -1
- package/src/components/Team.vue +3 -1
- package/src/composables/useComponentResolver.js +49 -1
- package/src/composables/useDraftGate.js +93 -0
- package/src/composables/useFocusTrap.js +85 -0
- package/src/composables/usePageConfig.js +126 -28
- package/src/composables/usePageMeta.js +178 -5
- package/src/composables/usePromoBackgroundStyles.js +30 -14
- package/src/extensions/extensionLoader.js +34 -24
- package/src/main.js +69 -6
- package/src/router/index.js +98 -37
- package/src/styles/base.css +11 -2
- package/src/themes/buildCssVarMap.js +34 -0
- package/src/utils/appInfo.js +8 -5
- package/src/utils/breadcrumbs.js +121 -0
- package/src/utils/canonicalUrl.js +53 -0
- package/src/utils/componentRegistry.js +98 -16
- package/src/utils/cookieConsent.js +38 -24
- package/src/utils/draftMode.js +65 -0
- package/src/utils/imageSources.js +9 -1
- package/src/utils/jsonLd.js +66 -0
- package/src/utils/loadConfig.js +106 -6
- package/src/utils/robotsGenerator.js +95 -0
- package/src/utils/sha256.js +20 -0
- package/src/utils/sitemapGenerator.js +112 -0
- package/src/utils/socialMeta.js +100 -0
- package/src/utils/ssgRoutes.js +40 -0
- package/src/utils/trackingContext.js +16 -22
- package/src/utils/webAppManifest.js +85 -0
- package/src/utils/webStorage.js +82 -0
- package/templates/index.html +17 -24
- package/templates/scaffolds/extension/README.md +53 -0
- package/templates/scaffolds/extension/extension.config.json +10 -0
- package/templates/scaffolds/extension/index.js +37 -0
- package/templates/scaffolds/extension/package.json +22 -0
- package/templates/scaffolds/site/.env.example +7 -0
- package/templates/scaffolds/site/.nvmrc +1 -0
- package/templates/scaffolds/site/CLAUDE.md +121 -0
- package/templates/scaffolds/site/README.md +25 -0
- package/templates/scaffolds/site/amplify.yml +22 -0
- package/templates/scaffolds/site/package.json +26 -0
- package/templates/scaffolds/site/site/assets/img/.gitkeep +0 -0
- package/templates/scaffolds/site/site/components/.gitkeep +0 -0
- package/templates/scaffolds/site/site/content/content.config.json +3 -0
- package/templates/scaffolds/site/site/content/en/pages/home.json +8 -0
- package/templates/scaffolds/site/site/content/en/shared.json +5 -0
- package/templates/scaffolds/site/site/content/en/site.json +10 -0
- package/templates/scaffolds/site/site/style.css +4 -0
- package/templates/scaffolds/site/vite.config.js +15 -0
- package/templates/scaffolds/theme/README.md +50 -0
- package/templates/scaffolds/theme/index.js +8 -0
- package/templates/scaffolds/theme/package.json +21 -0
- package/templates/scaffolds/theme/theme.config.js +228 -0
- package/templates/scaffolds/theme/theme.css +14 -0
- package/themes/base/theme.config.js +100 -57
- package/vite-plugin.js +545 -32
- package/public/robots.txt +0 -5
- package/scripts/check-overflow.mjs +0 -33
- package/scripts/migrate-to-content-dirs.mjs +0 -223
- package/scripts/patch-lru-cache-tla.js +0 -164
|
@@ -1,392 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const entry of
|
|
32
|
-
if (entry.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const nested = await collectExtensionPackages(absolute);
|
|
36
|
-
packages.push(...nested);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (entry.name === 'extension.config.json') {
|
|
40
|
-
packages.push(absolute);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return packages;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function loadJson(filePath) {
|
|
48
|
-
const raw = await readFile(filePath, 'utf-8');
|
|
49
|
-
return JSON.parse(raw);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function loadExtensionRegistry() {
|
|
53
|
-
const manifestPaths = await collectExtensionPackages(EXTENSIONS_DIR);
|
|
54
|
-
const map = new Map();
|
|
55
|
-
|
|
56
|
-
for (const manifestPath of manifestPaths) {
|
|
57
|
-
const manifestDir = manifestPath.replace(/extension\.config\.json$/i, '');
|
|
58
|
-
const manifest = await loadJson(manifestPath);
|
|
59
|
-
const slug = manifest.slug || manifestDir;
|
|
60
|
-
map.set(slug, {
|
|
61
|
-
manifest,
|
|
62
|
-
dir: manifestDir,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return map;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function importSchema(schemaPath) {
|
|
70
|
-
if (!schemaPath) return null;
|
|
71
|
-
const lower = schemaPath.toLowerCase();
|
|
72
|
-
if (lower.endsWith('.json')) {
|
|
73
|
-
const raw = await readFile(schemaPath, 'utf-8');
|
|
74
|
-
return JSON.parse(raw);
|
|
75
|
-
}
|
|
76
|
-
const url = pathToFileURL(schemaPath).href;
|
|
77
|
-
const module = await import(url);
|
|
78
|
-
return module?.default ?? module;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function buildComponentMaps(extensionRegistry) {
|
|
82
|
-
const componentMap = new Map();
|
|
83
|
-
const extensionNameToSources = new Map();
|
|
84
|
-
|
|
85
|
-
for (const { manifest, dir } of extensionRegistry.values()) {
|
|
86
|
-
if (!Array.isArray(manifest.components)) continue;
|
|
87
|
-
const slug = typeof manifest.slug === 'string' ? manifest.slug.trim() : '';
|
|
88
|
-
const normalizedSlug = slug ? slug.toLowerCase() : '';
|
|
89
|
-
|
|
90
|
-
for (const component of manifest.components) {
|
|
91
|
-
if (!component?.name) continue;
|
|
92
|
-
const entry = {
|
|
93
|
-
manifest,
|
|
94
|
-
dir,
|
|
95
|
-
component,
|
|
96
|
-
slug,
|
|
97
|
-
normalizedSlug,
|
|
98
|
-
};
|
|
99
|
-
if (normalizedSlug) {
|
|
100
|
-
const sourceKey = `${normalizedSlug}:${component.name}`;
|
|
101
|
-
componentMap.set(sourceKey, entry);
|
|
102
|
-
const existingSources = extensionNameToSources.get(component.name) || new Set();
|
|
103
|
-
existingSources.add(normalizedSlug);
|
|
104
|
-
extensionNameToSources.set(component.name, existingSources);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
componentMap,
|
|
111
|
-
extensionNameToSources,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function collectSiteConfigs(dir) {
|
|
116
|
-
const configs = [];
|
|
117
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
118
|
-
for (const entry of entries) {
|
|
119
|
-
if (entry.name.startsWith('.')) continue;
|
|
120
|
-
const absolute = join(dir, entry.name);
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
const nested = await collectSiteConfigs(absolute);
|
|
123
|
-
configs.push(...nested);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (isJsonFile(entry.name)) {
|
|
127
|
-
configs.push(absolute);
|
|
1
|
+
/**
|
|
2
|
+
* cms-validate-extensions — validate a SITE's extension manifests.
|
|
3
|
+
*
|
|
4
|
+
* Validates, against the framework's JSON Schema (the same validation the
|
|
5
|
+
* Vite plugin runs at build time):
|
|
6
|
+
* - every site-local extension at <projectRoot>/extensions/<slug>/
|
|
7
|
+
* - any extension package names passed as positional arguments
|
|
8
|
+
* (e.g. `cms-validate-extensions @koehler8/cms-ext-compliance`)
|
|
9
|
+
*
|
|
10
|
+
* The project root is the parent of --site-dir (default ./site), matching
|
|
11
|
+
* how the Vite plugin resolves it. Exits non-zero on any invalid manifest.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { validateExtensionManifests } from './lib/extension-manifests.mjs';
|
|
17
|
+
|
|
18
|
+
const siteDir = process.env.CMS_SITE_DIR
|
|
19
|
+
? path.resolve(process.env.CMS_SITE_DIR)
|
|
20
|
+
: path.resolve(process.cwd(), './site');
|
|
21
|
+
const projectRoot = path.dirname(siteDir);
|
|
22
|
+
|
|
23
|
+
const packageSpecs = (process.env.CMS_VALIDATE_PACKAGES || '')
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
|
|
28
|
+
const localSpecs = [];
|
|
29
|
+
const localExtensionsDir = path.join(projectRoot, 'extensions');
|
|
30
|
+
if (fs.existsSync(localExtensionsDir)) {
|
|
31
|
+
for (const entry of fs.readdirSync(localExtensionsDir, { withFileTypes: true })) {
|
|
32
|
+
if (!entry.isDirectory()) continue;
|
|
33
|
+
if (fs.existsSync(path.join(localExtensionsDir, entry.name, 'extension.config.json'))) {
|
|
34
|
+
localSpecs.push(`./extensions/${entry.name}`);
|
|
128
35
|
}
|
|
129
36
|
}
|
|
130
|
-
return configs;
|
|
131
37
|
}
|
|
132
38
|
|
|
133
|
-
|
|
134
|
-
siteConfigPath,
|
|
135
|
-
siteConfig,
|
|
136
|
-
componentMap,
|
|
137
|
-
extensionNameToSources,
|
|
138
|
-
appVersion,
|
|
139
|
-
}) {
|
|
140
|
-
const failures = [];
|
|
141
|
-
|
|
142
|
-
if (!siteConfig?.pages || typeof siteConfig.pages !== 'object') {
|
|
143
|
-
return failures;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
for (const [pageKey, pageConfig] of Object.entries(siteConfig.pages)) {
|
|
147
|
-
if (!pageConfig || typeof pageConfig !== 'object') continue;
|
|
148
|
-
const components = Array.isArray(pageConfig.components) ? pageConfig.components : [];
|
|
149
|
-
const content = pageConfig.content && typeof pageConfig.content === 'object' ? pageConfig.content : {};
|
|
150
|
-
const normalizedPageKey = (pageKey || '').toString().trim().toLowerCase();
|
|
151
|
-
|
|
152
|
-
for (const entry of components) {
|
|
153
|
-
let name = '';
|
|
154
|
-
let configKeyOverride;
|
|
155
|
-
let enabled = true;
|
|
156
|
-
let source;
|
|
157
|
-
|
|
158
|
-
if (typeof entry === 'string') {
|
|
159
|
-
name = entry;
|
|
160
|
-
} else if (entry && typeof entry === 'object') {
|
|
161
|
-
enabled = entry.enabled !== false;
|
|
162
|
-
name = typeof entry.name === 'string' ? entry.name : '';
|
|
163
|
-
configKeyOverride = typeof entry.configKey === 'string' && entry.configKey.trim()
|
|
164
|
-
? entry.configKey.trim()
|
|
165
|
-
: undefined;
|
|
166
|
-
source = typeof entry.source === 'string' ? entry.source : undefined;
|
|
167
|
-
} else {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (typeof name === 'string') {
|
|
172
|
-
name = name.trim();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!enabled || !name) continue;
|
|
176
|
-
|
|
177
|
-
const normalizedSource = source ? normalizeSlug(source) : '';
|
|
178
|
-
const sourcesForName = extensionNameToSources.get(name);
|
|
179
|
-
const isExtensionName = Boolean(sourcesForName && sourcesForName.size);
|
|
180
|
-
|
|
181
|
-
if (isExtensionName && !normalizedSource) {
|
|
182
|
-
failures.push({
|
|
183
|
-
siteConfigPath,
|
|
184
|
-
pageKey,
|
|
185
|
-
componentName: name,
|
|
186
|
-
message: `Component comes from an extension and must specify "source". Available sources: ${Array.from(sourcesForName).join(', ')}`,
|
|
187
|
-
});
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (isExtensionName && normalizedSource && !sourcesForName.has(normalizedSource)) {
|
|
192
|
-
failures.push({
|
|
193
|
-
siteConfigPath,
|
|
194
|
-
pageKey,
|
|
195
|
-
componentName: name,
|
|
196
|
-
source: normalizedSource,
|
|
197
|
-
message: `Component is not provided by the "${normalizedSource}" extension.`,
|
|
198
|
-
});
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!isExtensionName && normalizedSource) {
|
|
203
|
-
failures.push({
|
|
204
|
-
siteConfigPath,
|
|
205
|
-
pageKey,
|
|
206
|
-
componentName: name,
|
|
207
|
-
source: normalizedSource,
|
|
208
|
-
message: 'Component does not belong to an extension so "source" must be omitted.',
|
|
209
|
-
});
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (!isExtensionName) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const lookupKey = `${normalizedSource}:${name}`;
|
|
218
|
-
const componentMeta = componentMap.get(lookupKey);
|
|
219
|
-
if (!componentMeta) {
|
|
220
|
-
failures.push({
|
|
221
|
-
siteConfigPath,
|
|
222
|
-
pageKey,
|
|
223
|
-
componentName: name,
|
|
224
|
-
source: normalizedSource,
|
|
225
|
-
message: 'Component metadata could not be resolved for the specified source.',
|
|
226
|
-
});
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const { component, dir } = componentMeta;
|
|
230
|
-
const configKey = configKeyOverride || component.configKey;
|
|
231
|
-
if (!configKey) continue;
|
|
232
|
-
const block = content[configKey];
|
|
233
|
-
|
|
234
|
-
if (Array.isArray(component.allowedPages) && component.allowedPages.length) {
|
|
235
|
-
const allowed = component.allowedPages
|
|
236
|
-
.map((slug) => (slug || '').toString().trim().toLowerCase())
|
|
237
|
-
.filter(Boolean);
|
|
238
|
-
if (allowed.length && !allowed.includes(normalizedPageKey)) {
|
|
239
|
-
failures.push({
|
|
240
|
-
siteConfigPath,
|
|
241
|
-
pageKey,
|
|
242
|
-
componentName: name,
|
|
243
|
-
configKey,
|
|
244
|
-
source: normalizedSource || undefined,
|
|
245
|
-
message: `Component is not allowed on this page (allowed: ${allowed.join(', ')}).`,
|
|
246
|
-
});
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (component.minAppVersion && compareVersions(appVersion, component.minAppVersion) < 0) {
|
|
252
|
-
failures.push({
|
|
253
|
-
siteConfigPath,
|
|
254
|
-
pageKey,
|
|
255
|
-
componentName: name,
|
|
256
|
-
configKey,
|
|
257
|
-
source: normalizedSource || undefined,
|
|
258
|
-
message: `Component requires app version ${component.minAppVersion} or higher (current ${appVersion}).`,
|
|
259
|
-
});
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
39
|
+
const specs = [...localSpecs, ...packageSpecs];
|
|
262
40
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
siteConfigPath,
|
|
268
|
-
pageKey,
|
|
269
|
-
componentName: name,
|
|
270
|
-
configKey,
|
|
271
|
-
source: normalizedSource || undefined,
|
|
272
|
-
message: `Missing required content paths: ${missing.join(', ')}`,
|
|
273
|
-
});
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const schemaRef = component.propsInterface;
|
|
279
|
-
if (!schemaRef) continue;
|
|
280
|
-
|
|
281
|
-
const schemaPath = join(dir, schemaRef.replace(/^\.\/+/, ''));
|
|
282
|
-
let schema;
|
|
283
|
-
let importFailed = false;
|
|
284
|
-
try {
|
|
285
|
-
schema = await importSchema(schemaPath);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
importFailed = true;
|
|
288
|
-
if (!(/Cannot use import statement/.test(String(error)) || /Unexpected reserved word/.test(String(error)))) {
|
|
289
|
-
throw error;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (importFailed) {
|
|
293
|
-
schema = await loadJson(schemaPath);
|
|
294
|
-
}
|
|
295
|
-
if (!schema) {
|
|
296
|
-
failures.push({
|
|
297
|
-
siteConfigPath,
|
|
298
|
-
pageKey,
|
|
299
|
-
componentName: name,
|
|
300
|
-
configKey,
|
|
301
|
-
source: normalizedSource || undefined,
|
|
302
|
-
message: `Schema "${schemaRef}" could not be loaded.`,
|
|
303
|
-
});
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const validate = ajv.compile(schema);
|
|
308
|
-
const valid = validate(block);
|
|
309
|
-
if (!valid) {
|
|
310
|
-
failures.push({
|
|
311
|
-
siteConfigPath,
|
|
312
|
-
pageKey,
|
|
313
|
-
componentName: name,
|
|
314
|
-
configKey,
|
|
315
|
-
source: normalizedSource || undefined,
|
|
316
|
-
errors: validate.errors,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return failures;
|
|
41
|
+
if (specs.length === 0) {
|
|
42
|
+
console.log(`No extension manifests found (looked in ${localExtensionsDir}, no packages named).`);
|
|
43
|
+
console.log('Nothing to validate. Pass package names as arguments to check installed extensions.');
|
|
44
|
+
process.exit(0);
|
|
323
45
|
}
|
|
324
46
|
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
console.
|
|
329
|
-
return;
|
|
47
|
+
try {
|
|
48
|
+
const validated = await validateExtensionManifests(specs, projectRoot);
|
|
49
|
+
for (const { spec, slug, componentNames } of validated) {
|
|
50
|
+
console.log(`✓ ${spec} (slug "${slug}", ${componentNames.size} component${componentNames.size === 1 ? '' : 's'})`);
|
|
330
51
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const failures = [];
|
|
335
|
-
|
|
336
|
-
for (const siteConfigPath of siteConfigs) {
|
|
337
|
-
let siteConfig;
|
|
338
|
-
try {
|
|
339
|
-
siteConfig = await loadJson(siteConfigPath);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
failures.push({
|
|
342
|
-
siteConfigPath,
|
|
343
|
-
message: `Failed to parse JSON: ${error.message}`,
|
|
344
|
-
});
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
const blockFailures = await validateConfigBlocks({
|
|
348
|
-
siteConfigPath,
|
|
349
|
-
siteConfig,
|
|
350
|
-
componentMap,
|
|
351
|
-
extensionNameToSources,
|
|
352
|
-
appVersion: APP_VERSION,
|
|
353
|
-
});
|
|
354
|
-
failures.push(...blockFailures);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (failures.length) {
|
|
358
|
-
console.error('Extension content validation failed:');
|
|
359
|
-
failures.forEach((failure) => {
|
|
360
|
-
console.error(`\n- ${failure.siteConfigPath}`);
|
|
361
|
-
if (failure.pageKey) {
|
|
362
|
-
console.error(` Page: ${failure.pageKey}`);
|
|
363
|
-
}
|
|
364
|
-
if (failure.componentName) {
|
|
365
|
-
console.error(` Component: ${failure.componentName}`);
|
|
366
|
-
}
|
|
367
|
-
if (failure.source) {
|
|
368
|
-
console.error(` Source: ${failure.source}`);
|
|
369
|
-
}
|
|
370
|
-
if (failure.configKey) {
|
|
371
|
-
console.error(` Config key: ${failure.configKey}`);
|
|
372
|
-
}
|
|
373
|
-
if (failure.message) {
|
|
374
|
-
console.error(` Error: ${failure.message}`);
|
|
375
|
-
}
|
|
376
|
-
if (failure.errors) {
|
|
377
|
-
failure.errors.forEach((error) => {
|
|
378
|
-
console.error(` • ${error.instancePath || '/'} ${error.message}`);
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
process.exitCode = 1;
|
|
383
|
-
return;
|
|
52
|
+
const skipped = specs.length - validated.length;
|
|
53
|
+
if (skipped > 0) {
|
|
54
|
+
console.warn(`⚠ ${skipped} spec(s) had no extension.config.json to validate (see warnings above).`);
|
|
384
55
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
main().catch((error) => {
|
|
390
|
-
console.error(error);
|
|
56
|
+
console.log('\nAll extension manifests validated successfully.');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(err.message);
|
|
391
59
|
process.exitCode = 1;
|
|
392
|
-
}
|
|
60
|
+
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cms-validate-themes — validate theme manifests.
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - the framework's bundled themes (themes/ at the package root)
|
|
6
|
+
* - a site's local themes at <projectRoot>/themes/<slug>/ when --site-dir
|
|
7
|
+
* is given (project root = parent of the site dir, matching the plugin)
|
|
8
|
+
*
|
|
9
|
+
* Exits non-zero on any invalid manifest.
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
15
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
16
|
import { validateThemeManifest } from '../src/themes/themeValidator.js';
|
|
5
17
|
|
|
6
18
|
const MANIFEST_PATTERN = /theme\.config\.(js|mjs|json)$/;
|
|
7
19
|
const ROOT_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
8
|
-
const
|
|
20
|
+
const FRAMEWORK_THEMES_DIR = join(ROOT_DIR, 'themes');
|
|
21
|
+
|
|
22
|
+
const siteDir = process.env.CMS_SITE_DIR ? resolve(process.env.CMS_SITE_DIR) : '';
|
|
23
|
+
const SITE_THEMES_DIR = siteDir ? join(dirname(siteDir), 'themes') : '';
|
|
9
24
|
|
|
10
25
|
async function collectManifestPaths(dir) {
|
|
11
26
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -29,14 +44,31 @@ async function loadManifestFromPath(filePath) {
|
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
async function main() {
|
|
32
|
-
const
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const manifestPaths = [];
|
|
49
|
+
for (const dir of [FRAMEWORK_THEMES_DIR, SITE_THEMES_DIR]) {
|
|
50
|
+
if (!dir || !existsSync(dir)) continue;
|
|
51
|
+
for (const p of await collectManifestPaths(dir)) {
|
|
52
|
+
if (seen.has(p)) continue;
|
|
53
|
+
seen.add(p);
|
|
54
|
+
manifestPaths.push(p);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
if (!manifestPaths.length) {
|
|
34
|
-
console.
|
|
59
|
+
console.log('No theme manifests found (framework themes/ and site themes/ are both empty).');
|
|
35
60
|
return;
|
|
36
61
|
}
|
|
62
|
+
|
|
37
63
|
const failures = [];
|
|
38
64
|
for (const manifestPath of manifestPaths) {
|
|
39
|
-
|
|
65
|
+
let manifest;
|
|
66
|
+
try {
|
|
67
|
+
manifest = await loadManifestFromPath(manifestPath);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
failures.push({ path: manifestPath, errors: [`failed to load: ${error.message}`] });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
40
72
|
const { valid, errors } = validateThemeManifest(manifest, { throwOnError: false });
|
|
41
73
|
if (!valid) {
|
|
42
74
|
failures.push({ path: manifestPath, errors });
|
package/src/components/About.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section
|
|
3
|
+
v-if="hasContent"
|
|
3
4
|
id="about"
|
|
4
5
|
class="about-section section-shell section-shell--tight ui-section--overlay"
|
|
5
6
|
data-analytics-section="about-overview"
|
|
@@ -99,6 +100,9 @@ const pageContent = inject('pageContent', ref({}));
|
|
|
99
100
|
const { resolve: resolveComingSoon } = useComingSoonResolver(pageContent);
|
|
100
101
|
|
|
101
102
|
const aboutContent = computed(() => pageContent.value?.about || {});
|
|
103
|
+
// components[] contract: no content.about block → render nothing (never
|
|
104
|
+
// empty headings and image frames).
|
|
105
|
+
const hasContent = computed(() => Object.keys(aboutContent.value).length > 0);
|
|
102
106
|
|
|
103
107
|
const getAboutValue = (key, fallback) => {
|
|
104
108
|
const value = aboutContent.value?.[key];
|
|
@@ -115,12 +119,10 @@ const aboutTopButton = computed(() => getAboutValue('topButton', {}));
|
|
|
115
119
|
const aboutBottomButton = computed(() => getAboutValue('bottomButton', {}));
|
|
116
120
|
|
|
117
121
|
const topImage = useResponsiveImage('img/about-top', {
|
|
118
|
-
widths: [320, 900],
|
|
119
122
|
fallbackFormat: 'jpg',
|
|
120
123
|
});
|
|
121
124
|
const topImageHandleError = topImage.handleError;
|
|
122
125
|
const bottomImage = useResponsiveImage('img/about-bottom', {
|
|
123
|
-
widths: [320, 600],
|
|
124
126
|
fallbackFormat: 'jpg',
|
|
125
127
|
});
|
|
126
128
|
const bottomImageHandleError = bottomImage.handleError;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section id="about" class="about-value-section section-shell" data-analytics-section="about-value">
|
|
3
|
-
<div
|
|
2
|
+
<section v-if="hasValueProps" id="about-value" class="about-value-section section-shell" data-analytics-section="about-value">
|
|
3
|
+
<div class="container">
|
|
4
4
|
<div class="about-value__grid">
|
|
5
5
|
<div class="about-value__media">
|
|
6
6
|
<div
|
|
@@ -159,7 +159,6 @@ const hasValueProps = computed(() => {
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
const DEFAULT_VALUE_IMAGE_PATH = 'img/about-top';
|
|
162
|
-
const DEFAULT_VALUE_IMAGE_WIDTHS = [320, 900];
|
|
163
162
|
const DEFAULT_VALUE_IMAGE_FALLBACK = 'jpg';
|
|
164
163
|
|
|
165
164
|
const valueImagePath = computed(() => {
|
|
@@ -179,7 +178,9 @@ const valueImageWidths = computed(() => {
|
|
|
179
178
|
if (Array.isArray(widths) && widths.length) {
|
|
180
179
|
return widths;
|
|
181
180
|
}
|
|
182
|
-
|
|
181
|
+
// No site override → useResponsiveImage falls back to the pipeline's
|
|
182
|
+
// resolved width matrix.
|
|
183
|
+
return undefined;
|
|
183
184
|
});
|
|
184
185
|
|
|
185
186
|
const valueImageFallbackFormat = computed(() => {
|
|
@@ -390,11 +390,22 @@ function handleActionClick(event, action) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
.coming-soon-action:hover,
|
|
393
|
-
.coming-soon-action:focus {
|
|
393
|
+
.coming-soon-action:focus-visible {
|
|
394
394
|
transform: translateY(-2px);
|
|
395
395
|
opacity: 0.92;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
@media (prefers-reduced-motion: reduce) {
|
|
399
|
+
.coming-soon-action {
|
|
400
|
+
transition: none;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.coming-soon-action:hover,
|
|
404
|
+
.coming-soon-action:focus-visible {
|
|
405
|
+
transform: none;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
398
409
|
@media (max-width: 767px) {
|
|
399
410
|
.coming-soon-shell {
|
|
400
411
|
padding: 1.75rem;
|