@eleventy-plugin-themer/build-vite 0.1.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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Vite configuration helper for Eleventy themes
3
+ *
4
+ * Provides theme-agnostic Vite configuration with auto-import support
5
+ */
6
+
7
+ import path from 'path';
8
+
9
+ import { getThemeRoot } from '@eleventy-plugin-themer/core/internal/api';
10
+ import { DEFAULT_ASSET_ENTRIES } from '@eleventy-plugin-themer/core/internal/defaults';
11
+ import { logger } from '@eleventy-plugin-themer/core/logger';
12
+ import { UNSAFE_KEYS } from '@eleventy-plugin-themer/core/internal/safe-keys';
13
+
14
+ import { themeAutoImportPlugin } from './plugins/auto-import.mjs';
15
+ import { featureServePlugin } from './plugins/feature-serve.mjs';
16
+ import { prismThemePlugin } from './plugins/prism-theme.mjs';
17
+ import { runOptimizations } from './utils/plugin-orchestrator.mjs';
18
+ import { getFeaturePathsForBuild } from './utils/features.mjs';
19
+ import { deepMergeViteConfig } from './utils/merge-config.mjs';
20
+ import { mergeStringArrays } from './utils/merge-arrays.mjs';
21
+
22
+ /**
23
+ * Merge theme build hints into user optimization config.
24
+ *
25
+ * Themes declare build hints (e.g. PurgeCSS safelist) in `theme.json#build`.
26
+ * These are merged with the user's `optimizations` config so the underlying
27
+ * optimization plugin receives the union.
28
+ *
29
+ * Merge precedence:
30
+ * - Theme defaults come **first** (preserved at the head of array merges).
31
+ * - User values **append** (deduped) for safelist arrays so user input
32
+ * never silently shadows theme requirements, but also can't break
33
+ * greedy patterns relied on by the theme.
34
+ * - Object merges put user values **last** (user wins for primitive fields).
35
+ *
36
+ * Unsafe keys (`__proto__`, `constructor`, `prototype`) on `themeBuild` are
37
+ * silently skipped to prevent prototype pollution via parsed JSON.
38
+ *
39
+ * @param {Object} optimizations - User-supplied optimization config.
40
+ * @param {Object} themeBuild - Theme-supplied `build` block from `theme.json`.
41
+ * @returns {Object} Merged optimizations object (new reference).
42
+ */
43
+ function mergeThemeBuildHints(optimizations, themeBuild) {
44
+ if (!optimizations || !themeBuild) return optimizations;
45
+
46
+ const merged = { ...optimizations };
47
+
48
+ for (const [pluginName, themeConfig] of Object.entries(themeBuild)) {
49
+ if (UNSAFE_KEYS.has(pluginName)) continue;
50
+ if (!(pluginName in merged) || !merged[pluginName]) continue;
51
+
52
+ if (merged[pluginName] === true) {
53
+ merged[pluginName] = { ...themeConfig };
54
+ } else if (typeof merged[pluginName] === 'object') {
55
+ if (themeConfig.safelist || merged[pluginName].safelist) {
56
+ const userSafelist = merged[pluginName].safelist || {};
57
+ const themeSafelist = themeConfig.safelist || {};
58
+ merged[pluginName] = {
59
+ ...merged[pluginName],
60
+ safelist: {
61
+ standard: mergeStringArrays(themeSafelist.standard, userSafelist.standard),
62
+ deep: mergeStringArrays(themeSafelist.deep, userSafelist.deep),
63
+ greedy: mergeStringArrays(themeSafelist.greedy, userSafelist.greedy),
64
+ },
65
+ };
66
+ } else {
67
+ merged[pluginName] = { ...merged[pluginName], ...themeConfig };
68
+ }
69
+ }
70
+ }
71
+
72
+ return merged;
73
+ }
74
+
75
+ function buildFeatureAliases(discoveredFeatures) {
76
+ const featurePaths = getFeaturePathsForBuild(discoveredFeatures);
77
+ const aliases = {};
78
+ featurePaths.forEach((featurePath, featureName) => {
79
+ aliases[`/${featureName}.js`] = featurePath;
80
+ });
81
+ return aliases;
82
+ }
83
+
84
+ function buildResolveAliases({ themeRoot, projectRoot, resolvedOverridePaths, featureAliases }) {
85
+ return {
86
+ '@theme': themeRoot,
87
+ '/overrides/styles': path.resolve(projectRoot, resolvedOverridePaths.styles),
88
+ '/overrides/scripts': path.resolve(projectRoot, resolvedOverridePaths.scripts),
89
+ '/overrides/features': path.resolve(projectRoot, resolvedOverridePaths.features),
90
+ ...featureAliases,
91
+ };
92
+ }
93
+
94
+ function buildScssConfig({ projectRoot, stylesPath, themeRoot, themeName }) {
95
+ return {
96
+ api: 'modern-compiler',
97
+ includePaths: [
98
+ path.resolve(projectRoot, 'node_modules'),
99
+ path.resolve(projectRoot, stylesPath),
100
+ path.join(themeRoot, 'styles'),
101
+ ],
102
+ additionalData: `$theme-name: '${themeName}';\n`,
103
+ };
104
+ }
105
+
106
+ function buildOptimizationPlugin(mergedOptimizations, dirs) {
107
+ let resolvedViteConfig;
108
+ return {
109
+ name: 'eleventy-themes-optimization',
110
+ apply: 'build',
111
+ configResolved(config) {
112
+ resolvedViteConfig = config;
113
+ },
114
+ async closeBundle() {
115
+ try {
116
+ const finalDirs = { ...dirs, output: resolvedViteConfig.build.outDir };
117
+ await runOptimizations(mergedOptimizations, finalDirs);
118
+ logger.info('✅ Build optimization complete!\n');
119
+ } catch (error) {
120
+ logger.error('\n❌ Build optimization failed!');
121
+ logger.error(` ${error.message}\n`);
122
+ throw error;
123
+ }
124
+ },
125
+ };
126
+ }
127
+
128
+ function buildPluginsArray({
129
+ projectRoot,
130
+ themeName,
131
+ themeMetadata,
132
+ codeHighlighting,
133
+ stylesEntry,
134
+ scriptsEntry,
135
+ resolvedOverridePaths,
136
+ discoveredFeatures,
137
+ userPlugins,
138
+ mergedOptimizations,
139
+ dirs,
140
+ }) {
141
+ const themePlugins = [
142
+ themeAutoImportPlugin({
143
+ projectRoot,
144
+ themeName,
145
+ stylesEntry,
146
+ scriptsEntry,
147
+ resolvedOverridePaths,
148
+ }),
149
+ featureServePlugin({ discoveredFeatures }),
150
+ // Use the merged (theme.json + user theme.js) codeHighlighting so a user's
151
+ // prismTheme/diffHighlight override reaches the build; fall back to theme
152
+ // defaults when no merged config was provided.
153
+ prismThemePlugin(codeHighlighting ?? themeMetadata.config?.codeHighlighting),
154
+ ...userPlugins,
155
+ ];
156
+
157
+ if (mergedOptimizations && Object.keys(mergedOptimizations).length > 0) {
158
+ themePlugins.push(buildOptimizationPlugin(mergedOptimizations, dirs));
159
+ }
160
+
161
+ return themePlugins;
162
+ }
163
+
164
+ /**
165
+ * Create Vite configuration for any Eleventy theme.
166
+ *
167
+ * Wraps `@eleventy-plugin-themer/build-vite` with theme-specific features:
168
+ * - Auto-imports theme CSS and JS
169
+ * - `@theme` alias for imports
170
+ * - SCSS preprocessor configuration with theme paths
171
+ *
172
+ * @param {Object} themeMetadata - Theme metadata from `theme.json`.
173
+ * @param {Object} options
174
+ * @param {string} options.projectRoot - Project root path (required).
175
+ * @param {Object} [options.resolvedOverridePaths] - Resolved override paths.
176
+ * @param {Array} [options.plugins] - Additional Vite plugins to append.
177
+ * @param {Object} [options.optimizations] - Optimization config (purgeCSS, etc.).
178
+ * @param {Object} [options.dirs] - Eleventy dirs config.
179
+ * @param {Map} [options.discoveredFeatures] - Pre-discovered feature map.
180
+ * @param {...any} [options.viteOptions] - Additional Vite config to deep-merge.
181
+ * @returns {Object} Vite configuration object.
182
+ */
183
+ export function createThemeViteConfig(themeMetadata, options = {}) {
184
+ const {
185
+ projectRoot,
186
+ mergedConfig,
187
+ resolvedOverridePaths = {},
188
+ plugins = [],
189
+ optimizations,
190
+ dirs,
191
+ discoveredFeatures,
192
+ ...viteOptions
193
+ } = options;
194
+
195
+ if (!projectRoot) {
196
+ throw new Error('createThemeViteConfig: projectRoot is required');
197
+ }
198
+ if (!themeMetadata || !themeMetadata.name) {
199
+ throw new Error('createThemeViteConfig: themeMetadata with name is required');
200
+ }
201
+
202
+ const themeName = themeMetadata.name;
203
+ const themeRoot = getThemeRoot(projectRoot, themeName);
204
+ const stylesEntry = themeMetadata.assets?.styles?.entry || DEFAULT_ASSET_ENTRIES.styles;
205
+ const scriptsEntry = themeMetadata.assets?.scripts?.entry || DEFAULT_ASSET_ENTRIES.scripts;
206
+
207
+ const featureAliases = buildFeatureAliases(discoveredFeatures);
208
+ const mergedOptimizations = mergeThemeBuildHints(optimizations, themeMetadata.build);
209
+
210
+ const themeConfig = {
211
+ resolve: {
212
+ alias: buildResolveAliases({
213
+ themeRoot,
214
+ projectRoot,
215
+ resolvedOverridePaths,
216
+ featureAliases,
217
+ }),
218
+ },
219
+ css: {
220
+ preprocessorOptions: {
221
+ scss: buildScssConfig({
222
+ projectRoot,
223
+ stylesPath: resolvedOverridePaths.styles,
224
+ themeRoot,
225
+ themeName,
226
+ }),
227
+ },
228
+ },
229
+ plugins: buildPluginsArray({
230
+ projectRoot,
231
+ themeName,
232
+ themeMetadata,
233
+ codeHighlighting: mergedConfig?.codeHighlighting,
234
+ stylesEntry,
235
+ scriptsEntry,
236
+ resolvedOverridePaths,
237
+ discoveredFeatures,
238
+ userPlugins: plugins,
239
+ mergedOptimizations,
240
+ dirs,
241
+ }),
242
+ };
243
+
244
+ return deepMergeViteConfig(themeConfig, viteOptions);
245
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared constants for build plugins
3
+ *
4
+ * Centralizes glob patterns and asset paths used across multiple plugins.
5
+ */
6
+
7
+ /**
8
+ * Glob pattern generators for build output files
9
+ */
10
+ export const GLOB_PATTERNS = {
11
+ html: (outputDir) => `${outputDir}/**/*.html`,
12
+ css: (outputDir) => `${outputDir}/assets/css/*.css`,
13
+ };
14
+
15
+ /**
16
+ * Asset output paths used in rollup configuration and plugins
17
+ */
18
+ export const ASSET_PATHS = {
19
+ scripts: 'assets/scripts',
20
+ css: 'assets/css',
21
+ fonts: 'assets/fonts',
22
+ images: 'assets/images',
23
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Utilities for resolving page-specific features for Vite builds
3
+ *
4
+ * Provides entry point discovery for features referenced in page front matter.
5
+ * Works with any theme that follows the @eleventy-plugin-themer conventions.
6
+ */
7
+
8
+ import path from 'path';
9
+
10
+ import { getAvailableFeatures, resolveResource } from '@eleventy-plugin-themer/core/internal/api';
11
+ import { DEFAULT_ASSET_ENTRIES } from '@eleventy-plugin-themer/core/internal/defaults';
12
+ import { logger } from '@eleventy-plugin-themer/core/logger';
13
+
14
+ /**
15
+ * @internal Get feature paths for Vite aliases/serving.
16
+ *
17
+ * Used inside build-vite by `theme-config.mjs` and `feature-serve.mjs`. Exported
18
+ * for adapter authors building custom Vite integrations on top of build-vite,
19
+ * but not part of the documented public API surface — signature may change
20
+ * without a major version bump.
21
+ *
22
+ * The caller must have already discovered features via `getAvailableFeatures()`
23
+ * and pass the resulting Map — this avoids redundant filesystem scans during
24
+ * plugin init.
25
+ *
26
+ * @param {Map<string, {path: string}>} discoveredFeatures - Required. Output of `getAvailableFeatures()`.
27
+ * @returns {Map<string, string>} Map of feature name to absolute file path.
28
+ */
29
+ export function getFeaturePathsForBuild(discoveredFeatures) {
30
+ if (!(discoveredFeatures instanceof Map)) {
31
+ throw new TypeError(
32
+ 'getFeaturePathsForBuild: discoveredFeatures (Map) is required. ' +
33
+ 'Call getAvailableFeatures() once during plugin init and pass the result.',
34
+ );
35
+ }
36
+ const featurePaths = new Map();
37
+ discoveredFeatures.forEach((info, name) => {
38
+ featurePaths.set(name, info.path);
39
+ });
40
+ return featurePaths;
41
+ }
42
+
43
+ /**
44
+ * Get Vite entry points for all features.
45
+ *
46
+ * Returns entry points for `main.js` plus all available features (theme + user,
47
+ * with user overrides taking precedence). Core handles cascade logic via
48
+ * `themeMetadata`.
49
+ *
50
+ * @param {string} projectRoot - Project root path.
51
+ * @param {Object} themeMetadata - Theme metadata object from `theme.json`.
52
+ * @param {Object} [opts]
53
+ * @param {Object} [opts.resolvedOverridePaths] - Pre-resolved override paths.
54
+ * @param {Map} [opts.discoveredFeatures] - Pre-discovered features. If omitted,
55
+ * `getAvailableFeatures()` runs once internally.
56
+ * @returns {Object} Entry points object for Vite `build.rollupOptions.input`.
57
+ *
58
+ * @example
59
+ * import { getFeatureEntries } from '@eleventy-plugin-themer/build-vite';
60
+ * import { metadata } from '@eleventy-plugin-themer/theme-base';
61
+ *
62
+ * const input = getFeatureEntries(__dirname, metadata);
63
+ */
64
+ export function getFeatureEntries(projectRoot, themeMetadata, opts = {}) {
65
+ const { resolvedOverridePaths, discoveredFeatures } = opts;
66
+ const mainScriptEntry = themeMetadata.assets?.scripts?.entry || DEFAULT_ASSET_ENTRIES.scripts;
67
+
68
+ const mainScript = resolveResource({
69
+ projectRoot,
70
+ themeName: themeMetadata.name,
71
+ resolvedOverridePaths,
72
+ resourceType: 'scripts',
73
+ filename: path.basename(mainScriptEntry),
74
+ throwOnMissing: true,
75
+ });
76
+
77
+ const entries = {
78
+ main: mainScript.path,
79
+ };
80
+
81
+ const features =
82
+ discoveredFeatures || getAvailableFeatures(projectRoot, themeMetadata, resolvedOverridePaths);
83
+
84
+ features.forEach((feature) => {
85
+ const entryKey = `/${feature.name}.js`;
86
+ entries[entryKey] = feature.path;
87
+ });
88
+
89
+ if (features.size > 0) {
90
+ const featureList = Array.from(features.entries())
91
+ .map(([name, info]) => `${name} (${info.source})`)
92
+ .join(', ');
93
+ logger.info(`✨ Discovered features: ${featureList}`);
94
+ logger.info(`✅ Added ${features.size} feature(s) as Vite entry points`);
95
+ }
96
+
97
+ return entries;
98
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Generic file processor utility for build optimization plugins
3
+ *
4
+ * Provides consistent error handling, logging, and reporting across all plugins.
5
+ * Follows DRY principle by extracting common file processing patterns.
6
+ */
7
+
8
+ import path from 'path';
9
+
10
+ import { glob } from 'glob';
11
+ import { logger } from '@eleventy-plugin-themer/core/logger';
12
+
13
+ /**
14
+ * Process files with consistent error handling and logging
15
+ *
16
+ * @param {Object} options - Processing options
17
+ * @param {string|string[]} options.pattern - Glob pattern(s) for files to process
18
+ * @param {string} options.outputDir - Output directory (for relative path logging)
19
+ * @param {Function} options.processor - Async function to process each file
20
+ * Signature: async (file) => { message?: string, stats?: object }
21
+ * @param {string} options.taskName - Name of the task (for logging)
22
+ * @param {Function} [options.calculateStats] - Optional function to calculate summary stats
23
+ * Signature: (results) => { metricName: value, ... }
24
+ * @param {string} [options.errorTip] - Optional tip to show when errors occur
25
+ * @returns {Promise<{success: boolean, processed: number, errors: Array}>}
26
+ *
27
+ * @example
28
+ * await processFiles({
29
+ * pattern: `./${outputDir}/assets/css/*.css`,
30
+ * outputDir,
31
+ * taskName: 'PurgeCSS',
32
+ * processor: async (file) => {
33
+ * // Process the file
34
+ * return { message: ' (50% smaller)' };
35
+ * },
36
+ * calculateStats: (results) => ({
37
+ * 'total reduction': '45%'
38
+ * }),
39
+ * });
40
+ */
41
+ export async function processFiles({
42
+ pattern,
43
+ outputDir,
44
+ processor,
45
+ taskName = 'Processing',
46
+ calculateStats,
47
+ errorTip,
48
+ }) {
49
+ logger.info(`\n📦 ${taskName}...\n`);
50
+
51
+ // Find files to process
52
+ const patterns = Array.isArray(pattern) ? pattern : [pattern];
53
+ let files = [];
54
+ for (const p of patterns) {
55
+ const matches = await glob(p);
56
+ files = files.concat(matches);
57
+ }
58
+
59
+ // Remove duplicates
60
+ files = [...new Set(files)];
61
+
62
+ if (files.length === 0) {
63
+ logger.info(`⚠️ ${taskName}: No files found to process`);
64
+ return { success: true, processed: 0, errors: [] };
65
+ }
66
+
67
+ const results = [];
68
+ const errors = [];
69
+
70
+ // Process each file
71
+ for (const file of files) {
72
+ try {
73
+ const result = await processor(file);
74
+ results.push({ file, ...(result || {}) });
75
+
76
+ const relativePath = path.relative(outputDir, file);
77
+ const message = result?.message || '';
78
+ logger.info(`✓ ${relativePath}${message}`);
79
+ } catch (error) {
80
+ const relativePath = path.relative(outputDir, file);
81
+ errors.push({
82
+ file: relativePath,
83
+ error: error.message,
84
+ });
85
+ logger.error(`✗ ${relativePath}: ${error.message}`);
86
+ }
87
+ }
88
+
89
+ // Calculate summary statistics
90
+ let statsStr = '';
91
+ if (calculateStats && results.length > 0) {
92
+ const stats = calculateStats(results);
93
+ if (stats && Object.keys(stats).length > 0) {
94
+ statsStr =
95
+ ', ' +
96
+ Object.entries(stats)
97
+ .map(([k, v]) => `${k}: ${v}`)
98
+ .join(', ');
99
+ }
100
+ }
101
+
102
+ // Log summary
103
+ logger.info(
104
+ `\n✓ ${taskName} completed: ${results.length}/${files.length} files${statsStr}${errors.length > 0 ? `, ${errors.length} failed` : ''}\n`,
105
+ );
106
+
107
+ // Report errors
108
+ if (errors.length > 0) {
109
+ logger.error(`\n❌ ${taskName} Errors:`);
110
+ errors.forEach(({ file, error }) => {
111
+ logger.error(` ${file}: ${error}`);
112
+ });
113
+
114
+ if (errorTip) {
115
+ logger.error(`\n💡 Tip: ${errorTip}\n`);
116
+ }
117
+
118
+ throw new Error(`${taskName} failed for ${errors.length} file(s). See errors above.`);
119
+ }
120
+
121
+ return { success: true, processed: results.length, errors: [] };
122
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Lightweight integration sanity check for build-vite consumers.
3
+ *
4
+ * Validates that node, vite, and @11ty/eleventy-plugin-vite are within the
5
+ * versions declared in build-vite's peerDependencies + engines. Emits a single
6
+ * banner on success and warnings on mismatch. **Never throws** — every failure
7
+ * mode (corrupt manifest, unreadable peer package.json) is swallowed to a
8
+ * single debug-style log so the check can never take down a consumer's build.
9
+ *
10
+ * Runs at most once per Node process. The dedupe is intentional: re-init
11
+ * within the same process (e.g. Eleventy `--serve` config reload) is silent
12
+ * because the underlying environment hasn't changed. Tests use
13
+ * `_resetIntegrationCheck()` to opt back in.
14
+ *
15
+ * Opt-out at runtime via `options.skipIntegrationCheck = true` (advanced
16
+ * consumers running a custom build flow).
17
+ */
18
+
19
+ import { createRequire } from 'module';
20
+ import { fileURLToPath } from 'url';
21
+ import path from 'path';
22
+ import fs from 'fs';
23
+
24
+ import { logger } from '@eleventy-plugin-themer/core/logger';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = path.dirname(__filename);
29
+ const BUILD_VITE_MANIFEST_PATH = path.resolve(__dirname, '..', 'package.json');
30
+
31
+ function readBuildViteManifest() {
32
+ return JSON.parse(fs.readFileSync(BUILD_VITE_MANIFEST_PATH, 'utf8'));
33
+ }
34
+
35
+ function readPeerVersion(pkgName) {
36
+ try {
37
+ return require(`${pkgName}/package.json`).version;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @internal Parse a leading major version from a semver-ish string.
45
+ * Accepts `7.3.3`, `v7.3.3`, `7.3.3-beta.1`. Returns null on failure.
46
+ * Anchored regex — no ReDoS.
47
+ */
48
+ export function _parseMajor(version) {
49
+ if (typeof version !== 'string') return null;
50
+ const match = version.match(/^v?(\d+)\./);
51
+ return match ? Number(match[1]) : null;
52
+ }
53
+
54
+ /**
55
+ * @internal Extract major numbers from a peerDependencies range string.
56
+ * Handles `^M.x.x` and `^M.x.x || ^N.x.x` forms.
57
+ */
58
+ export function _parseAllowedMajors(range) {
59
+ if (typeof range !== 'string') return [];
60
+ return [...range.matchAll(/\^(\d+)\./g)].map((m) => Number(m[1]));
61
+ }
62
+
63
+ /** @internal */
64
+ export function _checkNode(manifest, nodeVersion = process.versions.node) {
65
+ const required = manifest.engines?.node;
66
+ const min = typeof required === 'string' ? Number(required.replace(/[^\d]/g, '')) : null;
67
+ const actual = _parseMajor(nodeVersion);
68
+ if (min === null || actual === null) return null;
69
+ if (actual < min) {
70
+ return `Node ${nodeVersion} is below the supported floor (>=${min}). Upgrade Node to avoid undefined behaviour.`;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /** @internal */
76
+ export function _checkPeer(pkgName, manifest, peerLookup = readPeerVersion) {
77
+ const range = manifest.peerDependencies?.[pkgName];
78
+ if (!range) return null;
79
+ const installed = peerLookup(pkgName);
80
+ if (!installed) {
81
+ return `Peer dependency \`${pkgName}\` is not installed. Required: ${range}.`;
82
+ }
83
+ const allowed = _parseAllowedMajors(range);
84
+ const actual = _parseMajor(installed);
85
+ if (allowed.length === 0 || actual === null) return null;
86
+ if (!allowed.includes(actual)) {
87
+ return `\`${pkgName}\` ${installed} is outside the supported range (${range}). Behaviour may be unstable.`;
88
+ }
89
+ return null;
90
+ }
91
+
92
+ let alreadyRan = false;
93
+
94
+ /**
95
+ * @internal Pure evaluator. Takes injected dependencies; returns
96
+ * `{ version, warnings }` or `null` if the manifest can't be read.
97
+ */
98
+ export function _evaluate({
99
+ manifestReader = readBuildViteManifest,
100
+ peerLookup = readPeerVersion,
101
+ nodeVersion = process.versions.node,
102
+ } = {}) {
103
+ let manifest;
104
+ try {
105
+ manifest = manifestReader();
106
+ } catch {
107
+ return null;
108
+ }
109
+ const warnings = [
110
+ _checkNode(manifest, nodeVersion),
111
+ _checkPeer('vite', manifest, peerLookup),
112
+ _checkPeer('@11ty/eleventy-plugin-vite', manifest, peerLookup),
113
+ ].filter(Boolean);
114
+ return { version: manifest.version, warnings };
115
+ }
116
+
117
+ export function runIntegrationCheck({ silent = false } = {}) {
118
+ if (alreadyRan) return;
119
+ try {
120
+ const result = _evaluate();
121
+ if (!result) {
122
+ // Manifest unreadable — dedupe so we don't spam, but don't claim OK.
123
+ alreadyRan = true;
124
+ return;
125
+ }
126
+ alreadyRan = true;
127
+ if (silent) return;
128
+
129
+ const { version, warnings } = result;
130
+ if (warnings.length === 0) {
131
+ logger.info(`[themer/build-vite ${version}] integration check: OK`);
132
+ return;
133
+ }
134
+ logger.warn(`[themer/build-vite ${version}] integration check: ${warnings.length} warning(s)`);
135
+ for (const w of warnings) logger.warn(` • ${w}`);
136
+ } catch {
137
+ // Should be unreachable — _evaluate handles its own errors. Belt-and-braces
138
+ // so the docstring's "never throws" promise is genuinely guaranteed.
139
+ alreadyRan = true;
140
+ }
141
+ }
142
+
143
+ /** @internal Test-only: reset the once-per-process dedupe flag. */
144
+ export function _resetIntegrationCheck() {
145
+ alreadyRan = false;
146
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tiny helper for merging string arrays from theme defaults and user values.
3
+ *
4
+ * Used by `mergeThemeBuildHints` to combine PurgeCSS safelist entries
5
+ * (`standard`, `deep`, `greedy`). Theme entries always come first so theme
6
+ * authors can rely on stable ordering for any greedy regex matchers; user
7
+ * entries are appended; duplicates are removed.
8
+ */
9
+
10
+ /**
11
+ * Concatenate two arrays, theme entries first, deduped (preserves first-seen
12
+ * order). Tolerates `undefined`/non-array inputs by treating them as empty.
13
+ *
14
+ * @param {Array<string>|undefined} themeArr
15
+ * @param {Array<string>|undefined} userArr
16
+ * @returns {Array<string>}
17
+ */
18
+ export function mergeStringArrays(themeArr, userArr) {
19
+ const theme = Array.isArray(themeArr) ? themeArr : [];
20
+ const user = Array.isArray(userArr) ? userArr : [];
21
+ const seen = new Set();
22
+ const result = [];
23
+ for (const item of theme) {
24
+ if (seen.has(item)) continue;
25
+ seen.add(item);
26
+ result.push(item);
27
+ }
28
+ for (const item of user) {
29
+ if (seen.has(item)) continue;
30
+ seen.add(item);
31
+ result.push(item);
32
+ }
33
+ return result;
34
+ }