@koehler8/cms 1.0.0-beta.10
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 +212 -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 +88 -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 +412 -0
- package/scripts/migrate-to-content-dirs.mjs +223 -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 +161 -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 +235 -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 +187 -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/buildCssVarMap.js +246 -0
- package/src/themes/themeLoader.js +156 -0
- package/src/themes/themeManager.js +34 -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 +146 -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 +319 -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 +726 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Patches all nested lru-cache copies in node_modules to remove top-level
|
|
4
|
+
* await, which breaks Node 20's sync CJS loader when cssstyle (via jsdom, via
|
|
5
|
+
* vite-ssg) tries to `require()` @asamuzakjp/css-color.
|
|
6
|
+
*
|
|
7
|
+
* The fix:
|
|
8
|
+
* 1. Overwrite `dist/esm/diagnostics-channel.js` with a TLA-free no-op that
|
|
9
|
+
* uses createRequire() for node:diagnostics_channel.
|
|
10
|
+
* 2. Rewrite each nested lru-cache `package.json` to point the `import`
|
|
11
|
+
* conditional at `dist/esm/index.js` (non-minified), which imports from
|
|
12
|
+
* our patched `diagnostics-channel.js` instead of inlining TLA.
|
|
13
|
+
*
|
|
14
|
+
* Safe to run repeatedly; idempotent.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
// Prefer INIT_CWD (set by npm during postinstall to the consumer's project
|
|
24
|
+
// root). Fall back to the parent of scripts/ (package root) or CWD.
|
|
25
|
+
const candidateRoots = [
|
|
26
|
+
process.env.INIT_CWD,
|
|
27
|
+
path.resolve(__dirname, '..'),
|
|
28
|
+
process.cwd(),
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
|
|
31
|
+
let repoRoot;
|
|
32
|
+
let nodeModules;
|
|
33
|
+
for (const root of candidateRoots) {
|
|
34
|
+
const nm = path.join(root, 'node_modules');
|
|
35
|
+
if (fs.existsSync(nm)) {
|
|
36
|
+
repoRoot = root;
|
|
37
|
+
nodeModules = nm;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!nodeModules) {
|
|
43
|
+
console.log('[patch-lru-cache-tla] node_modules not found; skipping');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const REPLACEMENT = `// Patched by scripts/patch-lru-cache-tla.js — removes top-level await.
|
|
48
|
+
// Uses createRequire to load node:diagnostics_channel synchronously from ESM.
|
|
49
|
+
import { createRequire } from 'node:module';
|
|
50
|
+
const dummy = { hasSubscribers: false };
|
|
51
|
+
let _metrics = dummy;
|
|
52
|
+
let _tracing = dummy;
|
|
53
|
+
try {
|
|
54
|
+
const require = createRequire(import.meta.url);
|
|
55
|
+
const dc = require('node:diagnostics_channel');
|
|
56
|
+
_metrics = dc.channel('lru-cache:metrics');
|
|
57
|
+
_tracing = dc.tracingChannel('lru-cache');
|
|
58
|
+
} catch {
|
|
59
|
+
/* non-node runtime: keep dummy */
|
|
60
|
+
}
|
|
61
|
+
export const metrics = _metrics;
|
|
62
|
+
export const tracing = _tracing;
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
function findLruCacheDirs(root) {
|
|
66
|
+
const found = [];
|
|
67
|
+
function walk(dir) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
const full = path.join(dir, entry.name);
|
|
77
|
+
if (entry.name === 'lru-cache' && fs.existsSync(path.join(full, 'package.json'))) {
|
|
78
|
+
found.push(full);
|
|
79
|
+
// don't recurse into lru-cache's own node_modules
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('@')) {
|
|
83
|
+
walk(full);
|
|
84
|
+
} else if (entry.name === 'node_modules') {
|
|
85
|
+
walk(full);
|
|
86
|
+
} else {
|
|
87
|
+
// Only recurse into scoped/package dirs that could contain nested node_modules
|
|
88
|
+
const nested = path.join(full, 'node_modules');
|
|
89
|
+
if (fs.existsSync(nested)) walk(nested);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
walk(root);
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function patchLruCache(lruDir) {
|
|
98
|
+
const diagFile = path.join(lruDir, 'dist', 'esm', 'diagnostics-channel.js');
|
|
99
|
+
const pkgFile = path.join(lruDir, 'package.json');
|
|
100
|
+
const indexJs = path.join(lruDir, 'dist', 'esm', 'index.js');
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(diagFile) || !fs.existsSync(pkgFile) || !fs.existsSync(indexJs)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let patched = false;
|
|
107
|
+
|
|
108
|
+
// 1. Overwrite diagnostics-channel.js if it still contains TLA
|
|
109
|
+
const current = fs.readFileSync(diagFile, 'utf-8');
|
|
110
|
+
if (current.includes('await import')) {
|
|
111
|
+
fs.writeFileSync(diagFile, REPLACEMENT, 'utf-8');
|
|
112
|
+
patched = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Rewrite package.json exports to use non-minified index.js
|
|
116
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
|
|
117
|
+
let pkgChanged = false;
|
|
118
|
+
|
|
119
|
+
const rewriteExports = (exports) => {
|
|
120
|
+
if (!exports || typeof exports !== 'object') return;
|
|
121
|
+
for (const key of Object.keys(exports)) {
|
|
122
|
+
const value = exports[key];
|
|
123
|
+
if (value && typeof value === 'object') {
|
|
124
|
+
if (value.import && typeof value.import === 'object' && value.import.default) {
|
|
125
|
+
if (value.import.default.includes('index.min.js')) {
|
|
126
|
+
value.import.default = value.import.default.replace('index.min.js', 'index.js');
|
|
127
|
+
pkgChanged = true;
|
|
128
|
+
}
|
|
129
|
+
} else if (typeof value.import === 'string' && value.import.includes('index.min.js')) {
|
|
130
|
+
value.import = value.import.replace('index.min.js', 'index.js');
|
|
131
|
+
pkgChanged = true;
|
|
132
|
+
}
|
|
133
|
+
rewriteExports(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
rewriteExports(pkg.exports);
|
|
139
|
+
|
|
140
|
+
if (pkg.module && pkg.module.includes('index.min.js')) {
|
|
141
|
+
pkg.module = pkg.module.replace('index.min.js', 'index.js');
|
|
142
|
+
pkgChanged = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (pkgChanged) {
|
|
146
|
+
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
147
|
+
patched = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return patched;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lruDirs = findLruCacheDirs(nodeModules);
|
|
154
|
+
let patchedCount = 0;
|
|
155
|
+
for (const dir of lruDirs) {
|
|
156
|
+
if (patchLruCache(dir)) {
|
|
157
|
+
patchedCount++;
|
|
158
|
+
console.log(`[patch-lru-cache-tla] patched ${path.relative(repoRoot, dir)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(
|
|
163
|
+
`[patch-lru-cache-tla] processed ${lruDirs.length} lru-cache dirs, patched ${patchedCount}`
|
|
164
|
+
);
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import Ajv from 'ajv/dist/2020.js';
|
|
7
|
+
import addFormats from 'ajv-formats';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const packageJson = require('../package.json');
|
|
11
|
+
import { compareVersions } from '../src/utils/semver.js';
|
|
12
|
+
import { validateRequiredContentPaths } from '../src/utils/contentRequirements.js';
|
|
13
|
+
|
|
14
|
+
const ROOT_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
15
|
+
const EXTENSIONS_DIR = join(ROOT_DIR, 'extensions');
|
|
16
|
+
const SITES_DIR = join(ROOT_DIR, 'sites');
|
|
17
|
+
|
|
18
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
19
|
+
addFormats(ajv);
|
|
20
|
+
const APP_VERSION = process.env.VITE_APP_VERSION || packageJson.version || '0.0.0';
|
|
21
|
+
const normalizeSlug = (value) => (value || '').toString().trim().toLowerCase();
|
|
22
|
+
|
|
23
|
+
function isJsonFile(fileName) {
|
|
24
|
+
return fileName.toLowerCase().endsWith('.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function collectExtensionPackages(dir) {
|
|
28
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
29
|
+
const packages = [];
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.name.startsWith('.')) continue;
|
|
33
|
+
const absolute = join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
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);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return configs;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function validateConfigBlocks({
|
|
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
|
+
}
|
|
262
|
+
|
|
263
|
+
if (Array.isArray(component.requiredContent) && component.requiredContent.length) {
|
|
264
|
+
const { isValid, missing } = validateRequiredContentPaths(block, component.requiredContent);
|
|
265
|
+
if (!isValid) {
|
|
266
|
+
failures.push({
|
|
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;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function main() {
|
|
326
|
+
const extensionRegistry = await loadExtensionRegistry();
|
|
327
|
+
if (!extensionRegistry.size) {
|
|
328
|
+
console.warn('No extension manifests found under /extensions.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const { componentMap, extensionNameToSources } = buildComponentMaps(extensionRegistry);
|
|
332
|
+
|
|
333
|
+
const siteConfigs = await collectSiteConfigs(SITES_DIR);
|
|
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;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log('All extension content blocks validated successfully.');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
main().catch((error) => {
|
|
390
|
+
console.error(error);
|
|
391
|
+
process.exitCode = 1;
|
|
392
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { validateThemeManifest } from '../src/themes/themeValidator.js';
|
|
5
|
+
|
|
6
|
+
const MANIFEST_PATTERN = /theme\.config\.(js|mjs|json)$/;
|
|
7
|
+
const ROOT_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
8
|
+
const THEMES_DIR = join(ROOT_DIR, 'themes');
|
|
9
|
+
|
|
10
|
+
async function collectManifestPaths(dir) {
|
|
11
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
12
|
+
const paths = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (entry.name.startsWith('.')) continue;
|
|
15
|
+
const absolutePath = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
const nested = await collectManifestPaths(absolutePath);
|
|
18
|
+
paths.push(...nested);
|
|
19
|
+
} else if (MANIFEST_PATTERN.test(entry.name)) {
|
|
20
|
+
paths.push(absolutePath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return paths;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadManifestFromPath(filePath) {
|
|
27
|
+
const module = await import(pathToFileURL(filePath).href);
|
|
28
|
+
return module?.default ?? module;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const manifestPaths = await collectManifestPaths(THEMES_DIR);
|
|
33
|
+
if (!manifestPaths.length) {
|
|
34
|
+
console.warn('No theme manifests found under /themes.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const failures = [];
|
|
38
|
+
for (const manifestPath of manifestPaths) {
|
|
39
|
+
const manifest = await loadManifestFromPath(manifestPath);
|
|
40
|
+
const { valid, errors } = validateThemeManifest(manifest, { throwOnError: false });
|
|
41
|
+
if (!valid) {
|
|
42
|
+
failures.push({ path: manifestPath, errors });
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`✓ ${manifest.slug || manifestPath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (failures.length) {
|
|
49
|
+
console.error('\nTheme manifest validation failed:');
|
|
50
|
+
failures.forEach(({ path, errors }) => {
|
|
51
|
+
console.error(`\n- ${path}`);
|
|
52
|
+
errors.forEach((error) => console.error(` • ${error}`));
|
|
53
|
+
});
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\nAll theme manifests validated successfully.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main().catch((error) => {
|
|
62
|
+
console.error(error);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
});
|
package/src/App.vue
ADDED