@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Artis Lismanis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @eleventy-plugin-themer/build-vite
2
+
3
+ Vite integration with production optimizations for Eleventy themes built with `@eleventy-plugin-themer/core`.
4
+
5
+ ## Features
6
+
7
+ - **Auto-Import** - Automatically imports theme styles and scripts into user entry points
8
+ - **Feature Discovery** - Discovers and bundles theme features as Vite entry points
9
+ - **PurgeCSS** - Removes unused CSS from production builds
10
+ - **Critical CSS** - Inlines critical CSS and async loads the rest (via Critters)
11
+ - **HTML Minification** - Minifies HTML output
12
+ - **Link Validation** - Validates internal links and images after build
13
+ - **Non-HTML Preservation** - Preserves files like RSS feeds and sitemaps
14
+ - **Dev Server** - Serves feature scripts during development with HMR
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install -D @eleventy-plugin-themer/build-vite @11ty/eleventy-plugin-vite
20
+ ```
21
+
22
+ Optional peer dependencies (install based on optimizations you enable):
23
+
24
+ ```bash
25
+ npm install -D purgecss critters html-minifier-terser node-html-parser glob
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```js
31
+ // eleventy.config.mjs
32
+ import { eleventyPluginThemer } from '@eleventy-plugin-themer/core';
33
+ import { eleventyPluginThemerVite } from '@eleventy-plugin-themer/build-vite';
34
+
35
+ const THEME_NAME = '@eleventy-plugin-themer/theme-base';
36
+
37
+ export default async function (eleventyConfig) {
38
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
+
40
+ // Register core theme plugin (direct call so we can spread `dir` into the return value)
41
+ const { dir } = await eleventyPluginThemer(eleventyConfig, {
42
+ theme: THEME_NAME,
43
+ projectRoot: __dirname,
44
+ input: 'content',
45
+ output: '_site',
46
+ });
47
+
48
+ // Register Vite plugin with optimizations
49
+ await eleventyPluginThemerVite(eleventyConfig, {
50
+ theme: THEME_NAME,
51
+ projectRoot: __dirname,
52
+ optimizations: {
53
+ purgeCSS: true,
54
+ criticalCSS: true,
55
+ minifyHTML: true,
56
+ validateLinks: true,
57
+ preserveNonHtml: {
58
+ extensions: ['xml', 'txt', 'xsl'],
59
+ },
60
+ },
61
+ });
62
+
63
+ return { dir };
64
+ }
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### `eleventyPluginThemerVite(eleventyConfig, options)`
70
+
71
+ Eleventy plugin that wraps `@11ty/eleventy-plugin-vite` with theme-aware configuration.
72
+
73
+ **Options:**
74
+
75
+ - `theme` (string, required) - Theme package name
76
+ - `projectRoot` (string, required) - Project root path
77
+ - `scriptsEntry` (string) - Main scripts entry point (default: `'overrides/scripts/main.js'`)
78
+ - `tempFolderName` (string) - Vite temp folder name (default: `'.11ty-vite'`)
79
+ - `overridePaths` (Object) - Override paths configuration
80
+ - `viteOptions` (Object) - Additional Vite options to merge with theme defaults
81
+ - `optimizations` (Object) - Production optimization toggles:
82
+ - `purgeCSS` (boolean | Object) - Remove unused CSS
83
+ - `criticalCSS` (boolean | Object) - Inline critical CSS
84
+ - `minifyHTML` (boolean | Object) - Minify HTML output
85
+ - `validateLinks` (boolean | Object) - Validate internal links
86
+ - `preserveNonHtml` (Object) - Preserve non-HTML files. Provide `{ extensions: ['xml', 'txt'] }`
87
+
88
+ ### How `optimizations` merges with `theme.json#build`
89
+
90
+ A theme can declare build hints in its `theme.json` under `build.*` (currently `build.purgeCSS` and `build.postcss`). These are merged with the consumer's `optimizations` config at plugin init by `mergeThemeBuildHints`:
91
+
92
+ - **Arrays** (e.g. `purgeCSS.safelist.standard`, `safelist.deep`, `safelist.greedy`): theme entries come **first**, user entries **append** (deduped). Theme entries cannot be silently shadowed by a user typo, and greedy patterns the theme relies on stay at the head of the array.
93
+ - **Objects** (non-array): user values **win** (last-spread). Setting `purgeCSS: true` enables the optimisation with the theme's hints; passing `purgeCSS: { safelist: {...} }` extends them per the array rule above.
94
+ - **Booleans / primitives**: user value replaces.
95
+ - **PostCSS plugins** (`build.postcss.plugins`) follow the same rule: theme-declared plugins run first, user-supplied plugins append. Override a theme plugin by re-declaring an entry with the same `package` name in your project's `postcss.config.mjs`.
96
+
97
+ Disabling a theme-provided optimisation entirely: set the toggle to `false` in `optimizations` (e.g. `purgeCSS: false`).
98
+
99
+ ### `getFeatureEntries(projectRoot, themeMetadata, opts?)`
100
+
101
+ Returns Vite entry points for the main script and all discovered features. Used internally by `eleventyPluginThemerVite`, but available for advanced use cases.
102
+
103
+ ```js
104
+ import { getFeatureEntries } from '@eleventy-plugin-themer/build-vite';
105
+ import { metadata } from '@eleventy-plugin-themer/theme-base';
106
+
107
+ const input = getFeatureEntries(__dirname, metadata, {
108
+ resolvedOverridePaths, // optional; auto-resolved if absent
109
+ discoveredFeatures, // optional Map; avoids redundant FS scan
110
+ });
111
+ ```
112
+
113
+ If you've already called `getAvailableFeatures()` at plugin init, pass the resulting Map as `opts.discoveredFeatures` to skip a duplicate filesystem scan.
114
+
115
+ ### Individual Plugins
116
+
117
+ Optimization plugins can be imported individually for custom build pipelines:
118
+
119
+ ```js
120
+ import {
121
+ purgeCSSFiles,
122
+ generateCriticalCSS,
123
+ minifyHTML,
124
+ validateLinks,
125
+ preserveNonHtmlFiles,
126
+ } from '@eleventy-plugin-themer/build-vite';
127
+ ```
128
+
129
+ All follow the signature `(outputDir, options) => Promise<void>` and throw on failure.
130
+
131
+ ## Integration check
132
+
133
+ `eleventyPluginThemerVite` runs a one-shot sanity check at plugin init that compares your environment against the package's declared peer ranges:
134
+
135
+ - Node version vs `engines.node` (>=22)
136
+ - `vite` peer version vs the supported major(s)
137
+ - `@11ty/eleventy-plugin-vite` peer version vs the supported major(s)
138
+
139
+ On a healthy environment you'll see one line on startup:
140
+
141
+ ```text
142
+ [themer/build-vite 0.1.0] integration check: OK
143
+ ```
144
+
145
+ On mismatch you get actionable warnings. The check **never throws** — a corrupt manifest or unreadable peer is logged and skipped so it can't take down your build. Opt out with `skipIntegrationCheck: true` if you're running a custom build flow.
146
+
147
+ ## Logging
148
+
149
+ Set `THEME_LOG_LEVEL` environment variable to control output verbosity:
150
+
151
+ ```bash
152
+ THEME_LOG_LEVEL=silent npx eleventy # No theme output
153
+ THEME_LOG_LEVEL=error npx eleventy # Errors only
154
+ THEME_LOG_LEVEL=warn npx eleventy # Errors + warnings
155
+ THEME_LOG_LEVEL=info npx eleventy # Default
156
+ THEME_LOG_LEVEL=debug npx eleventy # Verbose
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT
package/index.mjs ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * @eleventy-plugin-themer/build-vite
3
+ *
4
+ * Opinionated Vite integration with production optimizations.
5
+ * Build what works for me, adaptable for your needs.
6
+ */
7
+
8
+ import path from 'path';
9
+
10
+ import { getThemerContext } from '@eleventy-plugin-themer/core/internal/api';
11
+
12
+ import { createThemeViteConfig } from './theme-config.mjs';
13
+ import { getFeatureEntries as _getFeatureEntries } from './utils/features.mjs';
14
+ import { ASSET_PATHS } from './utils/constants.mjs';
15
+ import { runIntegrationCheck } from './utils/integration-check.mjs';
16
+ import { KNOWN_OPTIMIZATIONS } from './utils/plugin-orchestrator.mjs';
17
+
18
+ /**
19
+ * @public
20
+ *
21
+ * PostCSS preset helper. Consumers call `createPostcssConfig` from their own
22
+ * `postcss.config.mjs` to defer to plugins declared in `theme.json#build.postcss`.
23
+ */
24
+ export { createPostcssConfig } from './postcss.mjs';
25
+
26
+ /**
27
+ * Default rollup output options for theme builds.
28
+ *
29
+ * Provides sensible defaults for asset naming and organization.
30
+ * Can be overridden via options.build.rollupOptions.output.
31
+ *
32
+ * @param {Object} options - Configuration options
33
+ * @returns {Object} Rollup output configuration
34
+ */
35
+ function createDefaultRollupOutput(_options = {}) {
36
+ return {
37
+ entryFileNames: (chunkInfo) => {
38
+ if (chunkInfo.name === 'main') {
39
+ return `${ASSET_PATHS.scripts}/[name].[hash].js`;
40
+ }
41
+ const cleanName = chunkInfo.name.replace(/^\//, '').replace(/\.js$/, '');
42
+ return `${ASSET_PATHS.scripts}/${cleanName}.[hash].js`;
43
+ },
44
+ chunkFileNames: (chunkInfo) => {
45
+ if (chunkInfo.name === 'main') {
46
+ return `${ASSET_PATHS.scripts}/[name].[hash].js`;
47
+ }
48
+ return `${ASSET_PATHS.scripts}/chunks/[name].[hash].js`;
49
+ },
50
+ assetFileNames: ({ name, type }) => {
51
+ if (type === 'asset' && name?.endsWith('.css')) {
52
+ return `${ASSET_PATHS.css}/[name].[hash][extname]`;
53
+ }
54
+ if (/\.(xml|txt|xsl)$/.test(name ?? '')) {
55
+ return '[name][extname]';
56
+ }
57
+ if (/\.(woff|woff2|eot|ttf|otf)$/.test(name ?? '')) {
58
+ return `${ASSET_PATHS.fonts}/[name].[hash][extname]`;
59
+ }
60
+ if (/\.(png|jpe?g|svg|gif|webp|avif)$/.test(name ?? '')) {
61
+ return `${ASSET_PATHS.images}/[name].[hash][extname]`;
62
+ }
63
+ return 'assets/[name].[hash][extname]';
64
+ },
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Eleventy plugin for Vite integration with theme support.
70
+ *
71
+ * This is the recommended way to use @eleventy-plugin-themer/build-vite.
72
+ * It wraps @11ty/eleventy-plugin-vite with theme-aware configuration:
73
+ * 1. Auto-imports theme styles and scripts
74
+ * 2. Discovers and bundles theme features
75
+ * 3. Sets up @theme aliases for imports
76
+ * 4. Applies production optimizations (PurgeCSS, Critical CSS, etc.)
77
+ * 5. Provides sensible rollup output defaults
78
+ *
79
+ * @param {Object} eleventyConfig - Eleventy configuration object (provided by Eleventy)
80
+ * @param {Object} options - Plugin options
81
+ * @param {string} options.theme - The theme package name (required)
82
+ * @param {string} options.projectRoot - Project root path (required)
83
+ * @param {string} [options.scriptsEntry] - Path to main scripts entry (default: 'overrides/scripts/main.js')
84
+ * @param {Object} [options.optimizations] - Production optimizations config
85
+ * @param {boolean} [options.optimizations.purgeCSS] - Enable PurgeCSS
86
+ * @param {boolean} [options.optimizations.criticalCSS] - Enable Critical CSS extraction
87
+ * @param {boolean} [options.optimizations.minifyHTML] - Enable HTML minification
88
+ * @param {boolean} [options.optimizations.validateLinks] - Enable link validation
89
+ * @param {Object} [options.optimizations.preserveNonHtml] - Preserve non-HTML files config
90
+ * @param {Object} [options.overridePaths] - Override paths configuration
91
+ * @param {Object} [options.viteOptions] - Additional Vite options to merge
92
+ * @param {string} [options.tempFolderName='.11ty-vite'] - Temp folder name for Vite
93
+ *
94
+ * @example
95
+ * // In eleventy.config.mjs
96
+ * import { eleventyPluginThemerVite } from '@eleventy-plugin-themer/build-vite';
97
+ *
98
+ * export default async function(eleventyConfig) {
99
+ * const __dirname = fileURLToPath(new URL('.', import.meta.url));
100
+ *
101
+ * eleventyConfig.addPlugin(eleventyPluginThemerVite, {
102
+ * theme: '@eleventy-plugin-themer/theme-base',
103
+ * projectRoot: __dirname,
104
+ * optimizations: {
105
+ * purgeCSS: true,
106
+ * criticalCSS: true,
107
+ * minifyHTML: true,
108
+ * validateLinks: true,
109
+ * },
110
+ * });
111
+ *
112
+ * return { dir: { input: 'content', output: '_site' } };
113
+ * }
114
+ */
115
+ function validatePluginOptions({ theme, projectRoot, optimizations }) {
116
+ if (!theme) {
117
+ throw new Error(
118
+ 'eleventyPluginThemerVite requires a `theme` option specifying the theme package name.',
119
+ );
120
+ }
121
+ if (!projectRoot) {
122
+ throw new Error('eleventyPluginThemerVite requires a `projectRoot` option.');
123
+ }
124
+ if (optimizations && typeof optimizations === 'object') {
125
+ const unknown = Object.keys(optimizations).filter((k) => !KNOWN_OPTIMIZATIONS.has(k));
126
+ if (unknown.length > 0) {
127
+ throw new Error(
128
+ `eleventyPluginThemerVite: unknown optimization key(s): ${unknown.join(', ')}. ` +
129
+ `Valid keys: ${[...KNOWN_OPTIMIZATIONS].join(', ')}.`,
130
+ );
131
+ }
132
+ }
133
+ }
134
+
135
+ async function loadEleventyVitePlugin() {
136
+ try {
137
+ const mod = await import('@11ty/eleventy-plugin-vite');
138
+ return mod.default;
139
+ } catch (cause) {
140
+ throw new Error(
141
+ 'eleventyPluginThemerVite requires @11ty/eleventy-plugin-vite to be installed.\n' +
142
+ 'Run: npm install @11ty/eleventy-plugin-vite',
143
+ { cause },
144
+ );
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Resolve theme metadata, override paths, and feature discovery for the
150
+ * Vite adapter from the cached themer context populated by
151
+ * `eleventyPluginThemer` (see core/lib/index.mjs).
152
+ *
153
+ * Throws if the core plugin wasn't registered first — registration order is
154
+ * required for correctness (the adapter relies on the core plugin's resolved
155
+ * metadata, override paths, and feature discovery), so a silent fallback
156
+ * masks a real misconfiguration.
157
+ */
158
+ function resolveBuildContext({ eleventyConfig }) {
159
+ const cached = getThemerContext(eleventyConfig);
160
+ if (!cached) {
161
+ throw new Error(
162
+ 'eleventyPluginThemerVite: no themer context found on eleventyConfig. ' +
163
+ 'Register `eleventyPluginThemer` (from @eleventy-plugin-themer/core) before ' +
164
+ 'this plugin so it can share resolved metadata, override paths, and discovered features.',
165
+ );
166
+ }
167
+ return {
168
+ themeMetadata: cached.themeMetadata,
169
+ mergedThemeConfig: cached.mergedThemeConfig,
170
+ resolvedOverridePaths: cached.resolvedOverridePaths,
171
+ discoveredFeatures: cached.discoveredFeatures,
172
+ };
173
+ }
174
+
175
+ function buildViteOptions(ctx, opts) {
176
+ const {
177
+ themeMetadata,
178
+ mergedThemeConfig,
179
+ resolvedOverridePaths,
180
+ discoveredFeatures,
181
+ featureEntries,
182
+ } = ctx;
183
+ const { projectRoot, scriptsEntry, optimizations, viteOptions, tempFolderName } = opts;
184
+
185
+ return createThemeViteConfig(themeMetadata, {
186
+ projectRoot,
187
+ mergedConfig: mergedThemeConfig,
188
+ resolvedOverridePaths,
189
+ optimizations,
190
+ discoveredFeatures,
191
+ dirs: { temp: tempFolderName },
192
+ assetsInclude: ['**/*.xml', '**/*.txt', '**/*.xsl'],
193
+ publicDir: 'public',
194
+ server: {
195
+ mode: 'development',
196
+ middlewareMode: true,
197
+ watch: {
198
+ usePolling: true,
199
+ interval: 100,
200
+ ignored: ['**/_site/**', '**/node_modules/**'],
201
+ },
202
+ hmr: { overlay: true },
203
+ },
204
+ appType: 'custom',
205
+ resolve: {
206
+ alias: {
207
+ '/assets/scripts/main.js': path.resolve(projectRoot, scriptsEntry),
208
+ '/assets/scripts/features': path.resolve(projectRoot, resolvedOverridePaths.features),
209
+ },
210
+ },
211
+ build: {
212
+ mode: 'production',
213
+ sourcemap: 'hidden',
214
+ manifest: true,
215
+ emptyOutDir: false,
216
+ rollupOptions: {
217
+ input: {
218
+ main: path.resolve(projectRoot, scriptsEntry),
219
+ ...featureEntries,
220
+ },
221
+ output: createDefaultRollupOutput(),
222
+ },
223
+ cssCodeSplit: true,
224
+ },
225
+ ...viteOptions,
226
+ });
227
+ }
228
+
229
+ export async function eleventyPluginThemerVite(eleventyConfig, options = {}) {
230
+ const opts = {
231
+ scriptsEntry: 'overrides/scripts/main.js',
232
+ optimizations: {},
233
+ overridePaths: {},
234
+ viteOptions: {},
235
+ tempFolderName: '.11ty-vite',
236
+ ...options,
237
+ };
238
+
239
+ validatePluginOptions(opts);
240
+ runIntegrationCheck({ silent: opts.skipIntegrationCheck });
241
+ const EleventyVitePlugin = await loadEleventyVitePlugin();
242
+
243
+ const ctx = resolveBuildContext({ eleventyConfig });
244
+ const featureEntries = _getFeatureEntries(opts.projectRoot, ctx.themeMetadata, {
245
+ resolvedOverridePaths: ctx.resolvedOverridePaths,
246
+ discoveredFeatures: ctx.discoveredFeatures,
247
+ });
248
+
249
+ const themeViteConfig = buildViteOptions({ ...ctx, featureEntries }, opts);
250
+
251
+ eleventyConfig.addPlugin(EleventyVitePlugin, {
252
+ tempFolderName: opts.tempFolderName,
253
+ viteOptions: themeViteConfig,
254
+ });
255
+
256
+ return {
257
+ themeMetadata: ctx.themeMetadata,
258
+ featureEntries,
259
+ };
260
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@eleventy-plugin-themer/build-vite",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated Vite integration with production optimizations for Eleventy themes",
5
+ "type": "module",
6
+ "main": "index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs",
9
+ "./postcss": "./postcss.mjs"
10
+ },
11
+ "scripts": {
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "lint": "eslint ."
15
+ },
16
+ "files": [
17
+ "plugins/",
18
+ "utils/",
19
+ "index.mjs",
20
+ "theme-config.mjs",
21
+ "postcss.mjs",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "eleventy",
27
+ "eleventy-theme",
28
+ "vite",
29
+ "production-optimization",
30
+ "purgecss",
31
+ "critical-css",
32
+ "html-minification"
33
+ ],
34
+ "author": "Artis Lismanis",
35
+ "license": "MIT",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/artislismanis/eleventy-plugin-themer.git",
42
+ "directory": "packages/build/vite"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/artislismanis/eleventy-plugin-themer/issues"
46
+ },
47
+ "homepage": "https://github.com/artislismanis/eleventy-plugin-themer/tree/main/packages/build/vite#readme",
48
+ "dependencies": {
49
+ "@eleventy-plugin-themer/core": "^0.1.0",
50
+ "glob": "^13.0.0",
51
+ "purgecss": "^6.0.0",
52
+ "critters": "^0.0.24",
53
+ "html-minifier-terser": "^7.2.0",
54
+ "node-html-parser": "^7.0.1"
55
+ },
56
+ "peerDependencies": {
57
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
58
+ "@11ty/eleventy-plugin-vite": "^7.0.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=22"
62
+ }
63
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Vite plugin that auto-imports theme assets into user entry points
3
+ *
4
+ * This eliminates the need for users to manually import theme styles/scripts.
5
+ * The plugin prepends theme imports to the user's main entry file.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ import { resolveResource, getThemeRoot } from '@eleventy-plugin-themer/core/internal/api';
12
+
13
+ /**
14
+ * @param {Object} options - Plugin options
15
+ * @param {string} options.projectRoot - Project root path
16
+ * @param {string} options.themeName - Theme package name
17
+ * @param {string} options.stylesEntry - Theme styles entry filename (e.g., 'main.scss')
18
+ * @param {string} options.scriptsEntry - Theme scripts entry filename (e.g., 'main.js')
19
+ * @param {Object} options.resolvedOverridePaths - Resolved override paths object
20
+ * @returns {Object} Vite plugin
21
+ */
22
+ export function themeAutoImportPlugin(options = {}) {
23
+ const { projectRoot, themeName, stylesEntry, scriptsEntry, resolvedOverridePaths } = options;
24
+
25
+ if (!projectRoot || !themeName || !resolvedOverridePaths || !stylesEntry || !scriptsEntry) {
26
+ throw new Error(
27
+ 'themeAutoImportPlugin requires projectRoot, themeName, resolvedOverridePaths, stylesEntry, and scriptsEntry',
28
+ );
29
+ }
30
+
31
+ // Get theme root for direct theme imports (not cascade-resolved)
32
+ const themeRoot = getThemeRoot(projectRoot, themeName);
33
+
34
+ return {
35
+ name: 'theme-auto-import',
36
+
37
+ transform(code, id) {
38
+ // Resolve the path to the user's main script entry point
39
+ // Use basename because entry paths like 'scripts/main.js' are relative to theme root,
40
+ // but resolveResource already uses resourceType to determine the base directory
41
+ const userMainScript = resolveResource({
42
+ projectRoot,
43
+ themeName,
44
+ resolvedOverridePaths,
45
+ resourceType: 'scripts',
46
+ filename: path.basename(scriptsEntry), // Extract 'main.js' from 'scripts/main.js'
47
+ });
48
+
49
+ // Only transform if:
50
+ // 1. User has their own main script (we need to inject theme imports into it)
51
+ // 2. The file being transformed matches the user's main entry point
52
+ if (!userMainScript || userMainScript.source !== 'user' || id !== userMainScript.path) {
53
+ return null;
54
+ }
55
+
56
+ // Build direct paths to theme assets (always from theme, not cascade-resolved)
57
+ // This ensures theme scripts/styles are imported even when user has overrides
58
+ // Use the entry paths directly (e.g., 'styles/main.scss') which encode the directory
59
+ const themeStylesPath = path.join(themeRoot, stylesEntry);
60
+ const themeScriptsPath = path.join(themeRoot, scriptsEntry);
61
+
62
+ // Check if theme files exist
63
+ const hasThemeStyles = fs.existsSync(themeStylesPath);
64
+ const hasThemeScripts = fs.existsSync(themeScriptsPath);
65
+
66
+ // Prepend theme imports (always from theme package, not user overrides)
67
+ const themeImports = [
68
+ `// Auto-imported by theme (${themeName})`,
69
+ hasThemeStyles ? `import '${themeStylesPath}';` : '',
70
+ hasThemeScripts ? `import '${themeScriptsPath}';` : '',
71
+ '',
72
+ ]
73
+ .filter(Boolean)
74
+ .join('\n');
75
+
76
+ return {
77
+ code: themeImports + code,
78
+ map: null,
79
+ };
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'fs/promises';
2
+
3
+ import Critters from 'critters';
4
+
5
+ import { processFiles } from '../utils/file-processor.mjs';
6
+ import { GLOB_PATTERNS } from '../utils/constants.mjs';
7
+
8
+ export async function generateCriticalCSS(outputDir, options = {}) {
9
+ const critters = new Critters({
10
+ path: outputDir,
11
+ publicPath: '/',
12
+ inlineFonts: true,
13
+ pruneSource: true,
14
+ mergeStylesheets: true,
15
+ compress: true,
16
+ logLevel: 'warn', // Only show warnings/errors from Critters
17
+ ...options,
18
+ });
19
+
20
+ return processFiles({
21
+ pattern: GLOB_PATTERNS.html(outputDir),
22
+ outputDir,
23
+ taskName: 'Critical CSS',
24
+ errorTip: 'Check if CSS files exist and are properly linked in HTML',
25
+ processor: async (file) => {
26
+ const html = await fs.readFile(file, 'utf-8');
27
+ const inlined = await critters.process(html);
28
+ await fs.writeFile(file, inlined);
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Vite plugin to serve feature scripts in development mode
3
+ *
4
+ * In production, Vite bundles features via rollup entry points.
5
+ * In development, this plugin intercepts /{feature-name}.js requests
6
+ * and serves the corresponding feature file through Vite's transform pipeline.
7
+ */
8
+
9
+ import fs from 'fs';
10
+
11
+ import { getFeaturePathsForBuild } from '../utils/features.mjs';
12
+
13
+ /**
14
+ * @param {Object} options - Plugin options
15
+ * @param {Map<string, {path: string}>} options.discoveredFeatures - Pre-discovered features
16
+ * from `getAvailableFeatures()`. Required.
17
+ * @returns {Object} Vite plugin
18
+ */
19
+ export function featureServePlugin({ discoveredFeatures } = {}) {
20
+ // getFeaturePathsForBuild throws TypeError if discoveredFeatures is missing —
21
+ // surfacing the same contract violation here would just duplicate that.
22
+ const featurePaths = getFeaturePathsForBuild(discoveredFeatures);
23
+
24
+ return {
25
+ name: 'feature-serve',
26
+ apply: 'serve', // Only apply in dev mode
27
+
28
+ configureServer(server) {
29
+ // Add middleware to intercept feature requests
30
+ server.middlewares.use((req, res, next) => {
31
+ // Match /{feature-name}.js pattern
32
+ const match = req.url?.match(/^\/([a-z0-9-]+)\.js$/i);
33
+ if (!match) {
34
+ return next();
35
+ }
36
+
37
+ const featureName = match[1];
38
+ const featurePath = featurePaths.get(featureName);
39
+
40
+ if (!featurePath || !fs.existsSync(featurePath)) {
41
+ return next();
42
+ }
43
+
44
+ // Transform the request to use Vite's module resolution
45
+ // Prefix with /@fs/ to use Vite's file system serving
46
+ req.url = `/@fs${featurePath}`;
47
+ next();
48
+ });
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Vite plugins for Eleventy theme development and production
3
+ *
4
+ * All plugins use dynamic imports for optional dependencies.
5
+ * If a dependency is not installed, the plugin skips gracefully.
6
+ */
7
+
8
+ export { themeAutoImportPlugin } from './auto-import.mjs';
9
+ export { featureServePlugin } from './feature-serve.mjs';
10
+ export { purgeCSSFiles } from './purge-css.mjs';
11
+ export { generateCriticalCSS } from './critical-css.mjs';
12
+ export { minifyHTML } from './minify-html.mjs';
13
+ export { validateLinks } from './validate-links.mjs';
14
+ export { preserveNonHtmlFiles } from './preserve-non-html.mjs';
15
+ export { prismThemePlugin } from './prism-theme.mjs';