@apleasantview/eleventy-plugin-baseline 0.1.0-next.33 → 0.1.0-next.39
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/README.md +48 -23
- package/core/content-map-store.js +51 -0
- package/core/filters/index.js +4 -0
- package/core/filters/isString.js +1 -1
- package/core/filters/related-posts.js +1 -1
- package/core/global-functions/index.js +6 -0
- package/core/logging.js +25 -25
- package/core/page-context.js +310 -0
- package/core/registry.js +110 -0
- package/core/schema.js +37 -0
- package/core/shortcodes/image.js +8 -3
- package/core/shortcodes/index.js +2 -0
- package/core/slug-index.js +61 -0
- package/core/translation-map-store.js +46 -0
- package/core/types.js +73 -0
- package/core/utils/helpers.js +75 -0
- package/core/utils/pick.js +7 -0
- package/core/virtual-dir.js +111 -0
- package/core/wikilinks.js +152 -0
- package/index.js +364 -0
- package/modules/assets/index.js +162 -0
- package/modules/{assets-esbuild/process.js → assets/processors/esbuild-process.js} +3 -1
- package/modules/{assets-postcss/process.js → assets/processors/postcss-process.js} +5 -2
- package/modules/assets/schema.js +14 -0
- package/modules/head/drivers/capo-adapter.js +72 -0
- package/modules/head/drivers/posthtml-head-elements.js +140 -0
- package/modules/head/index.js +106 -0
- package/modules/head/schema.js +42 -0
- package/modules/head/utils/alternates.js +11 -0
- package/modules/head/utils/dedupe.js +47 -0
- package/modules/multilang/index.js +149 -0
- package/modules/navigator/index.js +140 -0
- package/modules/navigator/schema.js +13 -0
- package/modules/{navigator-core → navigator}/templates/navigator-core.html +10 -4
- package/{core → modules/navigator/utils}/debug.js +7 -1
- package/modules/sitemap/index.js +121 -0
- package/modules/{sitemap-core → sitemap}/templates/sitemap-core.html +2 -2
- package/modules/{sitemap-core → sitemap}/templates/sitemap-index.html +2 -2
- package/modules.js +6 -0
- package/package.json +15 -6
- package/core/filters.js +0 -9
- package/core/globals.js +0 -6
- package/core/helpers.js +0 -36
- package/core/modules.js +0 -18
- package/core/shortcodes.js +0 -3
- package/eleventy.config.js +0 -169
- package/modules/assets-core/plugins/assets-core.js +0 -197
- package/modules/head-core/drivers/posthtml-head-elements.js +0 -127
- package/modules/head-core/plugins/head-core.js +0 -75
- package/modules/head-core/utils/head-utils.js +0 -249
- package/modules/multilang-core/plugins/multilang-core.js +0 -118
- package/modules/navigator-core/plugins/navigator-core.js +0 -57
- package/modules/sitemap-core/plugins/sitemap-core.js +0 -88
- /package/core/{globals → global-functions}/date.js +0 -0
- /package/modules/{assets-postcss/fallback → assets/configs}/postcss.config.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-default-translation.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-translation-in.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-translations-for.js +0 -0
package/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { HtmlBasePlugin } from '@11ty/eleventy';
|
|
5
|
+
import { eleventyImageOnRequestDuringServePlugin } from '@11ty/eleventy-img';
|
|
6
|
+
|
|
7
|
+
import { createLogger } from './core/logging.js';
|
|
8
|
+
import { createContentMapStore } from './core/content-map-store.js';
|
|
9
|
+
import { createTranslationMapStore } from './core/translation-map-store.js';
|
|
10
|
+
import { createSlugIndex } from './core/slug-index.js';
|
|
11
|
+
import { registerVirtualDir } from './core/virtual-dir.js';
|
|
12
|
+
import { registerPageContext } from './core/page-context.js';
|
|
13
|
+
import { wikilinks } from './core/wikilinks.js';
|
|
14
|
+
import { settingsSchema } from './core/schema.js';
|
|
15
|
+
|
|
16
|
+
import { registerGlobals } from './core/global-functions/index.js';
|
|
17
|
+
import { markdownFilter, relatedPostsFilter, isStringFilter } from './core/filters/index.js';
|
|
18
|
+
import { imageShortcode } from './core/shortcodes/index.js';
|
|
19
|
+
import { assetsCore, headCore, multilangCore, navigatorCore, sitemapCore } from './modules.js';
|
|
20
|
+
|
|
21
|
+
const __require = createRequire(import.meta.url);
|
|
22
|
+
const { name, version } = __require('./package.json');
|
|
23
|
+
|
|
24
|
+
const mode = process.env.ELEVENTY_ENV;
|
|
25
|
+
const isDev = mode === 'development';
|
|
26
|
+
const isProd = mode === 'production';
|
|
27
|
+
|
|
28
|
+
const LEGACY_OPTION_KEYS = [
|
|
29
|
+
'verbose',
|
|
30
|
+
'enableNavigatorTemplate',
|
|
31
|
+
'enableSitemapTemplate',
|
|
32
|
+
'assetsESBuild',
|
|
33
|
+
'multilingual'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Whitelist of reserved global data keys used internally across the plugin.
|
|
37
|
+
// Positive side effect is they all get listed in order and merge data to the same key.
|
|
38
|
+
// Also prevents name collision with filters.
|
|
39
|
+
const INTERNAL_KEYS = [
|
|
40
|
+
'_baseline',
|
|
41
|
+
'_assets',
|
|
42
|
+
'_head',
|
|
43
|
+
'_multilang',
|
|
44
|
+
'_navigator',
|
|
45
|
+
'_sitemap',
|
|
46
|
+
'_snapshot',
|
|
47
|
+
'_pageContext'
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect legacy single-object plugin invocation.
|
|
52
|
+
*
|
|
53
|
+
* The original plugin API accepted a single merged configuration object.
|
|
54
|
+
* This helper detects that shape and enables safe normalization into
|
|
55
|
+
* the current (settings, options) contract.
|
|
56
|
+
*
|
|
57
|
+
* NOTE: arguments.length is required because default parameters mask arity.
|
|
58
|
+
*/
|
|
59
|
+
function looksLikeLegacyOptions(firstArg, argsLength) {
|
|
60
|
+
if (argsLength >= 2) return false;
|
|
61
|
+
if (!firstArg || typeof firstArg !== 'object') return false;
|
|
62
|
+
return LEGACY_OPTION_KEYS.some((key) => key in firstArg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize legacy plugin input into the current structured contract.
|
|
67
|
+
*
|
|
68
|
+
* - settings → site identity (content + SEO concerns)
|
|
69
|
+
* - options → runtime behavior flags
|
|
70
|
+
*/
|
|
71
|
+
function splitLegacyOptions(legacy) {
|
|
72
|
+
const { defaultLanguage, languages, ...rest } = legacy;
|
|
73
|
+
return {
|
|
74
|
+
settings: { defaultLanguage, languages },
|
|
75
|
+
options: rest
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Baseline (composition root)
|
|
81
|
+
*
|
|
82
|
+
* Eleventy plugin entry point. Normalises user input, builds the runtime
|
|
83
|
+
* substrate (stores + page-context registry), and registers feature modules
|
|
84
|
+
* in deterministic order.
|
|
85
|
+
*
|
|
86
|
+
* Architecture layer:
|
|
87
|
+
* composition root
|
|
88
|
+
*
|
|
89
|
+
* System role:
|
|
90
|
+
* The single place that wires settings + options into state, attaches
|
|
91
|
+
* lifecycle stores, registers the page-context registry, and hands a
|
|
92
|
+
* uniform module context to each module. No feature behaviour lives here.
|
|
93
|
+
*
|
|
94
|
+
* Lifecycle:
|
|
95
|
+
* build-time → legacy-shape detection, state computation, virtual dir
|
|
96
|
+
* registration, store and page-context registration, module
|
|
97
|
+
* wiring
|
|
98
|
+
*
|
|
99
|
+
* Why this exists:
|
|
100
|
+
* Modules need a stable, normalised input contract and a shared runtime
|
|
101
|
+
* surface. Centralising the wiring keeps activation rules, option
|
|
102
|
+
* inference, and registration order in one auditable place.
|
|
103
|
+
*
|
|
104
|
+
* Scope:
|
|
105
|
+
* Owns the legacy-shape compatibility shim, state computation, runtime
|
|
106
|
+
* store creation, page-context registration, and the module registry.
|
|
107
|
+
* Does not own any feature behaviour; modules implement that.
|
|
108
|
+
*
|
|
109
|
+
* Data flow:
|
|
110
|
+
* settings + options → state → runtime stores + page-context registry →
|
|
111
|
+
* modules
|
|
112
|
+
*
|
|
113
|
+
* Typedefs (BaselineSettings, BaselineOptions, BaselineState, BaselineContext)
|
|
114
|
+
* live in core/types.js.
|
|
115
|
+
*
|
|
116
|
+
* @param {import('./core/types.js').BaselineSettings} [settings]
|
|
117
|
+
* @param {import('./core/types.js').BaselineOptions} [options]
|
|
118
|
+
*/
|
|
119
|
+
export default function baseline(settings = {}, options = {}) {
|
|
120
|
+
// --- Legacy compatibility layer ---
|
|
121
|
+
const argsLength = arguments.length;
|
|
122
|
+
const wasLegacy = looksLikeLegacyOptions(settings, argsLength);
|
|
123
|
+
|
|
124
|
+
if (wasLegacy) {
|
|
125
|
+
const split = splitLegacyOptions(settings);
|
|
126
|
+
settings = split.settings;
|
|
127
|
+
options = split.options;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Base logger outputs regardless of options.
|
|
131
|
+
const baseLog = createLogger(null, { verbose: true });
|
|
132
|
+
|
|
133
|
+
// Scoped logging.
|
|
134
|
+
function scopedLog(name) {
|
|
135
|
+
return createLogger(name, { verbose: options.verbose });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (wasLegacy) {
|
|
139
|
+
baseLog.info('DEPRECATED: single-object plugin arg. Use baseline(settings, options) instead.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate configuration shape (non-fatal).
|
|
143
|
+
const parsed = settingsSchema.safeParse(settings);
|
|
144
|
+
if (!parsed.success) {
|
|
145
|
+
for (const issue of parsed.error.issues) {
|
|
146
|
+
baseLog.info('settings:', `${issue.path.join('.')} — ${issue.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
baseLog.info('Eleventy Baseline', version);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Eleventy plugin initializer.
|
|
154
|
+
*
|
|
155
|
+
* This function is executed during Eleventy configuration time and
|
|
156
|
+
* composes global APIs, filters, shortcodes, and feature modules.
|
|
157
|
+
*/
|
|
158
|
+
const plugin = async function (eleventyConfig) {
|
|
159
|
+
// --- Eleventy compatibility check ---
|
|
160
|
+
try {
|
|
161
|
+
eleventyConfig.versionCheck('>=3.0');
|
|
162
|
+
} catch (e) {
|
|
163
|
+
baseLog.error('Eleventy version mismatch:', e.message);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
INTERNAL_KEYS.forEach((key) => {
|
|
167
|
+
eleventyConfig.addGlobalData(key, {});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const env = {
|
|
171
|
+
name: 'Eleventy Baseline',
|
|
172
|
+
package: name,
|
|
173
|
+
version,
|
|
174
|
+
mode
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
eleventyConfig.addGlobalData('_baseline', {
|
|
178
|
+
env
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!settings.url) {
|
|
182
|
+
baseLog.warn('settings.url missing — canonical URLs will be relative');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
registerGlobals(eleventyConfig);
|
|
186
|
+
|
|
187
|
+
eleventyConfig.addPlugin(HtmlBasePlugin, {
|
|
188
|
+
baseHref: process.env.URL || eleventyConfig.pathPrefix
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- State layer (authoritative configuration) ---
|
|
192
|
+
const hasImageTransformPlugin = eleventyConfig.hasPlugin('eleventyImageTransformPlugin');
|
|
193
|
+
|
|
194
|
+
const state = {
|
|
195
|
+
settings: {
|
|
196
|
+
title: settings.title,
|
|
197
|
+
tagline: settings.tagline,
|
|
198
|
+
url: settings.url,
|
|
199
|
+
noindex: settings.noindex ?? false,
|
|
200
|
+
defaultLanguage: settings.defaultLanguage,
|
|
201
|
+
languages: settings.languages,
|
|
202
|
+
head: settings.head
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
options: {
|
|
206
|
+
verbose: options.verbose ?? false,
|
|
207
|
+
multilang: options.multilingual ?? false,
|
|
208
|
+
sitemap: options.sitemap ?? options.enableSitemapTemplate ?? true,
|
|
209
|
+
navigator: options.navigator ?? options.enableNavigatorTemplate ?? isDev,
|
|
210
|
+
head: {
|
|
211
|
+
titleSeparator: options.head?.titleSeparator,
|
|
212
|
+
showGenerator: options.head?.showGenerator
|
|
213
|
+
},
|
|
214
|
+
assets: {
|
|
215
|
+
esbuild: options.assets?.esbuild ?? options.assetsESBuild ?? {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
eleventyConfig.addGlobalData('_baseline', {
|
|
221
|
+
features: {
|
|
222
|
+
...state.options,
|
|
223
|
+
hasImageTransformPlugin
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- Virtual directories ---
|
|
228
|
+
registerVirtualDir(eleventyConfig, {
|
|
229
|
+
key: 'assets'
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const publicDir = registerVirtualDir(eleventyConfig, {
|
|
233
|
+
key: 'public',
|
|
234
|
+
outputDir: ''
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const directories = {
|
|
238
|
+
input: eleventyConfig.directories?.input,
|
|
239
|
+
output: eleventyConfig.directories?.output,
|
|
240
|
+
includes: eleventyConfig.directories?.includes,
|
|
241
|
+
data: eleventyConfig.directories?.data,
|
|
242
|
+
assets: eleventyConfig.directories?.assets,
|
|
243
|
+
public: eleventyConfig.directories?.public
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
eleventyConfig.addPassthroughCopy({ [publicDir.input]: '/' });
|
|
247
|
+
|
|
248
|
+
// Add paths to global.
|
|
249
|
+
eleventyConfig.addGlobalData('_baseline', {
|
|
250
|
+
paths: {
|
|
251
|
+
...directories
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// --- Draft filtering (build-time concern) ---
|
|
256
|
+
if (!eleventyConfig.preprocessors.drafts) {
|
|
257
|
+
eleventyConfig.addPreprocessor('drafts', '*', (data) => {
|
|
258
|
+
if (data.draft && process.env.ELEVENTY_RUN_MODE === 'build') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Runtime stores (self-attach their lifecycle listeners) ---
|
|
265
|
+
const contentMapStore = createContentMapStore(eleventyConfig);
|
|
266
|
+
const translationMapStore = createTranslationMapStore(eleventyConfig);
|
|
267
|
+
const slugIndex = createSlugIndex(eleventyConfig);
|
|
268
|
+
|
|
269
|
+
// --- Module helpers (derived state) ---
|
|
270
|
+
const helpers = {};
|
|
271
|
+
|
|
272
|
+
// --- Core context (lazy access layer) ---
|
|
273
|
+
const coreContext = {
|
|
274
|
+
env,
|
|
275
|
+
state,
|
|
276
|
+
runtime: {
|
|
277
|
+
get contentMap() {
|
|
278
|
+
return contentMapStore.get();
|
|
279
|
+
},
|
|
280
|
+
translationMap: translationMapStore,
|
|
281
|
+
slugIndex
|
|
282
|
+
},
|
|
283
|
+
directories,
|
|
284
|
+
helpers
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Page context registry
|
|
288
|
+
const pageContextRegistry = registerPageContext(eleventyConfig, coreContext);
|
|
289
|
+
|
|
290
|
+
// Wikilinks: [[slug]] / [[slug | lang]] in body markdown.
|
|
291
|
+
eleventyConfig.amendLibrary('md', (md) => {
|
|
292
|
+
md.use(wikilinks, { slugIndex, pageContextRegistry, translationMapStore });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
coreContext.snapshots = {
|
|
296
|
+
contentMap: () => contentMapStore.snapshot(),
|
|
297
|
+
pageContext: () => pageContextRegistry.snapshot()
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// --- Module activation ---
|
|
301
|
+
const features = {
|
|
302
|
+
multilang: Boolean(state.options.multilang),
|
|
303
|
+
sitemap: Boolean(state.options.sitemap),
|
|
304
|
+
navigator: Boolean(state.options.navigator),
|
|
305
|
+
head: true,
|
|
306
|
+
assets: true
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// --- Module registry ---
|
|
310
|
+
const moduleRegistry = [
|
|
311
|
+
{ when: features.multilang, name: 'multilang', plugin: multilangCore },
|
|
312
|
+
{ when: features.sitemap, name: 'sitemap', plugin: sitemapCore },
|
|
313
|
+
{ name: 'navigator', plugin: navigatorCore },
|
|
314
|
+
{ when: features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
|
|
315
|
+
{ when: features.assets, name: 'assets', plugin: assetsCore }
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
for (const entry of moduleRegistry) {
|
|
319
|
+
const { when = true, name, plugin, consumes = {} } = entry;
|
|
320
|
+
if (!when) continue;
|
|
321
|
+
const moduleContext = {
|
|
322
|
+
...coreContext,
|
|
323
|
+
log: scopedLog(name),
|
|
324
|
+
resolvePageContext: consumes.pageContext ? pageContextRegistry : null
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
eleventyConfig.addPlugin(plugin, moduleContext);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Filters ---
|
|
331
|
+
eleventyConfig.addFilter('markdownify', markdownFilter);
|
|
332
|
+
eleventyConfig.addFilter('relatedPosts', relatedPostsFilter);
|
|
333
|
+
eleventyConfig.addFilter('isString', isStringFilter);
|
|
334
|
+
|
|
335
|
+
// --- Shortcodes ---
|
|
336
|
+
eleventyConfig.addShortcode('image', imageShortcode);
|
|
337
|
+
|
|
338
|
+
// --- Dev image pipeline ---
|
|
339
|
+
eleventyConfig.addPlugin(eleventyImageOnRequestDuringServePlugin);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Set a named function identity so eleventyConfig.hasPlugin() can detect this plugin.
|
|
343
|
+
Object.defineProperty(plugin, 'name', { value: name });
|
|
344
|
+
return plugin;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Eleventy directory configuration (external contract)
|
|
349
|
+
*
|
|
350
|
+
* Defines input/output structure for the build system.
|
|
351
|
+
*/
|
|
352
|
+
export const config = {
|
|
353
|
+
dir: {
|
|
354
|
+
input: 'src',
|
|
355
|
+
output: 'dist',
|
|
356
|
+
data: '_data',
|
|
357
|
+
includes: '_includes',
|
|
358
|
+
assets: 'assets',
|
|
359
|
+
public: 'static'
|
|
360
|
+
},
|
|
361
|
+
htmlTemplateEngine: 'njk',
|
|
362
|
+
markdownTemplateEngine: 'njk',
|
|
363
|
+
templateFormats: ['html', 'njk', 'md']
|
|
364
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { TemplatePath } from '@11ty/eleventy-utils';
|
|
3
|
+
|
|
4
|
+
import { optionsSchema } from './schema.js';
|
|
5
|
+
import assetsESbuild from './processors/esbuild-process.js';
|
|
6
|
+
import assetsPostCSS from './processors/postcss-process.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Assets (module)
|
|
10
|
+
*
|
|
11
|
+
* Asset pipeline integration. Wires Eleventy’s template formats to esbuild
|
|
12
|
+
* and PostCSS through compile guards that allow only declared entrypoints,
|
|
13
|
+
* and exposes inline filters for critical-path assets.
|
|
14
|
+
*
|
|
15
|
+
* Architecture layer:
|
|
16
|
+
* module
|
|
17
|
+
*
|
|
18
|
+
* System role:
|
|
19
|
+
* Bridge between Eleventy’s template system and the external asset
|
|
20
|
+
* processors. Reads `directories.assets` from the virtual-dir substrate.
|
|
21
|
+
*
|
|
22
|
+
* Lifecycle:
|
|
23
|
+
* build-time → register js/css formats, compile guards, watch target, and
|
|
24
|
+
* inline filters; guards run per-entrypoint during compile
|
|
25
|
+
*
|
|
26
|
+
* Why this exists:
|
|
27
|
+
* Eleventy treats every .js and .css file as a template. Without compile
|
|
28
|
+
* guards, 11tydata.js files and non-entry assets would either pollute the
|
|
29
|
+
* template graph or trigger the wrong processor.
|
|
30
|
+
*
|
|
31
|
+
* Scope:
|
|
32
|
+
* Owns template format registration, compile guards, watch wiring, and the
|
|
33
|
+
* inline filters (inlinePostCSS, inlineESbuild).
|
|
34
|
+
* Does not own the processors themselves (assets/processors/) or
|
|
35
|
+
* `directories.assets` resolution (core/virtual-dir.js).
|
|
36
|
+
*
|
|
37
|
+
* Data flow:
|
|
38
|
+
* assets/{js,css}/index.{js,css} entrypoints → compile guard →
|
|
39
|
+
* esbuild/PostCSS processor → output
|
|
40
|
+
*
|
|
41
|
+
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
|
|
42
|
+
* @param {Object} moduleContext
|
|
43
|
+
*/
|
|
44
|
+
export function assetsCore(eleventyConfig, moduleContext) {
|
|
45
|
+
const { state, directories, log } = moduleContext;
|
|
46
|
+
const { settings, options } = state;
|
|
47
|
+
|
|
48
|
+
// Structural-only options check: log on mismatch, do not throw.
|
|
49
|
+
const parsed = optionsSchema.safeParse(options.assets);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
for (const issue of parsed.error.issues) {
|
|
52
|
+
log.info('options:', `${issue.path.join('.')} — ${issue.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inputDirectory = directories.input;
|
|
57
|
+
const assetsDirectory = directories.assets;
|
|
58
|
+
const jsDirectory = `${assetsDirectory}js/`;
|
|
59
|
+
const cssDirectory = `${assetsDirectory}css/`;
|
|
60
|
+
|
|
61
|
+
const esbuildOptions = options.assets.esbuild || {};
|
|
62
|
+
const dataFiles = `${inputDirectory}**/*.11tydata.js`;
|
|
63
|
+
const watchGlob = TemplatePath.join(assetsDirectory, '**/*.{css,js,svg,png,jpeg,jpg,webp,gif,avif}');
|
|
64
|
+
|
|
65
|
+
if (!assetsDirectory) {
|
|
66
|
+
log.warn('eleventyConfig.directories.assets is unset; registerVirtualDir must run before this plugin.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Watch common asset formats so edits trigger reloads during --serve.
|
|
71
|
+
eleventyConfig.addWatchTarget(watchGlob);
|
|
72
|
+
|
|
73
|
+
// --- JS (esbuild) ---
|
|
74
|
+
// Register js as a template format. Only index.js files under assets/js/
|
|
75
|
+
// are compiled; everything else (11tydata.js, non-entry scripts) is skipped
|
|
76
|
+
// by the compile guard. The inline filter wraps the same process function.
|
|
77
|
+
// Defaults (minify, target) live in assets-esbuild/process.js.
|
|
78
|
+
|
|
79
|
+
eleventyConfig.addTemplateFormats('js');
|
|
80
|
+
|
|
81
|
+
// Prevent Eleventy from processing 11tydata.js files as templates.
|
|
82
|
+
// The compile guard below also filters these, but without this ignore
|
|
83
|
+
// Eleventy still enters them into the template graph (data cascade,
|
|
84
|
+
// permalink computation) before compile gets a chance to reject them.
|
|
85
|
+
eleventyConfig.ignores.add(dataFiles);
|
|
86
|
+
|
|
87
|
+
eleventyConfig.addExtension('js', {
|
|
88
|
+
outputFileExtension: 'js',
|
|
89
|
+
useLayouts: false,
|
|
90
|
+
read: false,
|
|
91
|
+
compileOptions: {
|
|
92
|
+
permalink: true,
|
|
93
|
+
cache: true
|
|
94
|
+
},
|
|
95
|
+
// Compile guard: only process index.js files under the assets js directory.
|
|
96
|
+
// Returning undefined skips the file without error.
|
|
97
|
+
compile: async function (_inputContent, inputPath) {
|
|
98
|
+
if (
|
|
99
|
+
inputPath.includes('11tydata.js') ||
|
|
100
|
+
!inputPath.startsWith(jsDirectory) ||
|
|
101
|
+
path.basename(inputPath) !== 'index.js'
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return async () => assetsESbuild(inputPath, esbuildOptions);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Inline filter: bundle a JS file and wrap in <script> tags.
|
|
111
|
+
// Accepts per-call esbuild options (merged with defaults in process.js).
|
|
112
|
+
// Eleventy's addAsyncFilter handles the Nunjucks callback bridge,
|
|
113
|
+
// so this is a plain async function.
|
|
114
|
+
eleventyConfig.addAsyncFilter('inlineESbuild', async function (inputPath, opts = {}) {
|
|
115
|
+
try {
|
|
116
|
+
const js = await assetsESbuild(inputPath, opts);
|
|
117
|
+
return `<script>${js}</script>`;
|
|
118
|
+
} catch {
|
|
119
|
+
// Non-fatal: return an error comment so the build doesn't break.
|
|
120
|
+
return `<script>/* Error processing JS */</script>`;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// --- CSS (PostCSS) ---
|
|
125
|
+
// Register css as a template format. Only index.css files under assets/css/
|
|
126
|
+
// are compiled; non-entry CSS is skipped. Reads from disk (read: false) —
|
|
127
|
+
// the process function owns its own I/O. Config loading and caching live
|
|
128
|
+
// in assets-postcss/process.js.
|
|
129
|
+
|
|
130
|
+
eleventyConfig.addTemplateFormats('css');
|
|
131
|
+
|
|
132
|
+
eleventyConfig.addExtension('css', {
|
|
133
|
+
outputFileExtension: 'css',
|
|
134
|
+
useLayouts: false,
|
|
135
|
+
read: false,
|
|
136
|
+
compileOptions: {
|
|
137
|
+
permalink: true,
|
|
138
|
+
cache: true
|
|
139
|
+
},
|
|
140
|
+
// Compile guard: only process index.css files under the assets css directory.
|
|
141
|
+
compile: async function (_inputContent, inputPath) {
|
|
142
|
+
if (!inputPath.startsWith(cssDirectory) || path.basename(inputPath) !== 'index.css') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return async () => assetsPostCSS(inputPath);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Inline filter: process a CSS file through PostCSS and wrap in <style> tags.
|
|
151
|
+
// Eleventy's addAsyncFilter handles the Nunjucks callback bridge,
|
|
152
|
+
// so this is a plain async function.
|
|
153
|
+
eleventyConfig.addAsyncFilter('inlinePostCSS', async function (inputPath) {
|
|
154
|
+
try {
|
|
155
|
+
const css = await assetsPostCSS(inputPath);
|
|
156
|
+
return `<style>${css}</style>`;
|
|
157
|
+
} catch {
|
|
158
|
+
// Non-fatal: return an error comment so the build doesn't break.
|
|
159
|
+
return `<style>/* Error processing CSS */</style>`;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as esbuild from 'esbuild';
|
|
2
|
+
import { createLogger } from '../../../core/logging.js';
|
|
2
3
|
|
|
4
|
+
const log = createLogger('assets-esbuild');
|
|
3
5
|
const defaultOptions = { minify: true, target: 'es2020' };
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -26,7 +28,7 @@ export default async function assetsESbuild(jsFilePath, options = {}) {
|
|
|
26
28
|
// Return raw JS; markup wrapping is handled by the plugin registration.
|
|
27
29
|
return result.outputFiles[0].text;
|
|
28
30
|
} catch (error) {
|
|
29
|
-
|
|
31
|
+
log.error('esbuild failed:', error);
|
|
30
32
|
// Surface a safe JS comment so the caller can decide how to wrap it.
|
|
31
33
|
return '/* Error processing JS */';
|
|
32
34
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import postcss from 'postcss';
|
|
3
3
|
import loadPostCSSConfig from 'postcss-load-config';
|
|
4
|
-
import fallbackPostCSSConfig from '
|
|
4
|
+
import fallbackPostCSSConfig from '../configs/postcss.config.js';
|
|
5
|
+
import { createLogger } from '../../../core/logging.js';
|
|
6
|
+
|
|
7
|
+
const log = createLogger('assets-postcss');
|
|
5
8
|
|
|
6
9
|
// Resolve user PostCSS config from the project root (cwd), not the Eleventy input dir.
|
|
7
10
|
const configRoot = process.cwd();
|
|
@@ -42,7 +45,7 @@ export default async function assetsPostCSS(cssFilePath) {
|
|
|
42
45
|
// Return raw CSS; markup wrapping is handled in the plugin registration.
|
|
43
46
|
return result.css;
|
|
44
47
|
} catch (error) {
|
|
45
|
-
|
|
48
|
+
log.error('PostCSS failed:', error);
|
|
46
49
|
// Surface a safe CSS string so the caller can decide how to wrap it.
|
|
47
50
|
return '/* Error processing CSS */';
|
|
48
51
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
// Structural schema for the `options.assets` slice. Permissive on unknown
|
|
4
|
+
// keys (esbuild accepts many options we don't touch); strict on the keys the
|
|
5
|
+
// plugin itself reads.
|
|
6
|
+
|
|
7
|
+
export const esbuildOptionsSchema = z.looseObject({
|
|
8
|
+
minify: z.boolean().optional(),
|
|
9
|
+
target: z.string().optional()
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const optionsSchema = z.looseObject({
|
|
13
|
+
esbuild: esbuildOptionsSchema.optional()
|
|
14
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capo.js adapter for PostHTML AST nodes.
|
|
3
|
+
*
|
|
4
|
+
* Implements the HTMLAdapter interface capo.js v2 uses to compute element
|
|
5
|
+
* weights (src/adapters/adapter.js in @rviscomi/capo.js). Only getWeight is
|
|
6
|
+
* consumed downstream; the rest are shimmed to satisfy the shape.
|
|
7
|
+
*
|
|
8
|
+
* A PostHTML element node looks like `{ tag, attrs, content }` where attrs
|
|
9
|
+
* is either undefined or a plain object. Boolean attributes appear with an
|
|
10
|
+
* empty-string value (`{ async: '' }`) or as `true`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const hasAttr = (node, name) => {
|
|
14
|
+
if (!node || !node.attrs) return false;
|
|
15
|
+
const key = name.toLowerCase();
|
|
16
|
+
for (const k of Object.keys(node.attrs)) {
|
|
17
|
+
if (k.toLowerCase() === key) return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getAttr = (node, name) => {
|
|
23
|
+
if (!node || !node.attrs) return null;
|
|
24
|
+
const key = name.toLowerCase();
|
|
25
|
+
for (const k of Object.keys(node.attrs)) {
|
|
26
|
+
if (k.toLowerCase() === key) {
|
|
27
|
+
const v = node.attrs[k];
|
|
28
|
+
return v === true ? '' : v == null ? null : String(v);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getText = (node) => {
|
|
35
|
+
if (!node || node.content == null) return '';
|
|
36
|
+
if (typeof node.content === 'string') return node.content;
|
|
37
|
+
if (Array.isArray(node.content)) return node.content.filter((c) => typeof c === 'string').join('');
|
|
38
|
+
return '';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const capoPosthtmlAdapter = {
|
|
42
|
+
isElement(node) {
|
|
43
|
+
return !!(node && typeof node === 'object' && typeof node.tag === 'string');
|
|
44
|
+
},
|
|
45
|
+
getTagName(node) {
|
|
46
|
+
return node && typeof node.tag === 'string' ? node.tag.toLowerCase() : '';
|
|
47
|
+
},
|
|
48
|
+
getAttribute(node, name) {
|
|
49
|
+
return getAttr(node, name);
|
|
50
|
+
},
|
|
51
|
+
hasAttribute(node, name) {
|
|
52
|
+
return hasAttr(node, name);
|
|
53
|
+
},
|
|
54
|
+
getAttributeNames(node) {
|
|
55
|
+
return node && node.attrs ? Object.keys(node.attrs) : [];
|
|
56
|
+
},
|
|
57
|
+
getTextContent(node) {
|
|
58
|
+
return getText(node);
|
|
59
|
+
},
|
|
60
|
+
getChildren() {
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
getParent() {
|
|
64
|
+
return null;
|
|
65
|
+
},
|
|
66
|
+
getSiblings() {
|
|
67
|
+
return [];
|
|
68
|
+
},
|
|
69
|
+
stringify(node) {
|
|
70
|
+
return node && node.tag ? `<${node.tag}>` : '';
|
|
71
|
+
}
|
|
72
|
+
};
|