@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 +21 -0
- package/README.md +161 -0
- package/index.mjs +260 -0
- package/package.json +63 -0
- package/plugins/auto-import.mjs +82 -0
- package/plugins/critical-css.mjs +31 -0
- package/plugins/feature-serve.mjs +51 -0
- package/plugins/index.mjs +15 -0
- package/plugins/minify-html.mjs +66 -0
- package/plugins/preserve-non-html.mjs +55 -0
- package/plugins/prism-theme.mjs +61 -0
- package/plugins/purge-css.mjs +64 -0
- package/plugins/validate-links.mjs +164 -0
- package/postcss.mjs +113 -0
- package/theme-config.mjs +245 -0
- package/utils/constants.mjs +23 -0
- package/utils/features.mjs +98 -0
- package/utils/file-processor.mjs +122 -0
- package/utils/integration-check.mjs +146 -0
- package/utils/merge-arrays.mjs +34 -0
- package/utils/merge-config.mjs +99 -0
- package/utils/plugin-orchestrator.mjs +74 -0
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';
|