@apleasantview/eleventy-plugin-baseline 0.1.0-next.39 → 0.1.0-next.41
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 +21 -23
- package/core/back-compat/options.js +69 -0
- package/core/content-graph/backlinks.js +65 -0
- package/core/content-graph/extractors.js +140 -0
- package/core/content-graph/graph.js +118 -0
- package/core/content-graph/index.js +2 -0
- package/core/content-graph/prepass.js +121 -0
- package/core/logging/banner.js +49 -0
- package/core/logging/index.js +80 -0
- package/core/logging/quips.js +30 -0
- package/core/markdown/auto-heading-ids.js +86 -0
- package/core/markdown/index.js +5 -0
- package/core/markdown/safe-use.js +42 -0
- package/core/{wikilinks.js → markdown/wikilinks.js} +6 -6
- package/core/page-context/build.js +239 -0
- package/core/page-context/index.js +1 -0
- package/core/page-context/register.js +73 -0
- package/core/page-context/seo-helpers.js +56 -0
- package/core/schema.js +52 -1
- package/core/slug-index.js +2 -2
- package/core/state.js +73 -0
- package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
- package/core/surface/index.js +22 -0
- package/core/types.js +1 -1
- package/core/utils/add-trailing-slash.js +11 -0
- package/core/utils/ensure-dot-slash-dir.js +13 -0
- package/core/utils/normalize-languages.js +28 -0
- package/core/utils/resolve-field.js +9 -0
- package/core/utils/resolve-subdir.js +20 -0
- package/core/utils/slugify.js +15 -0
- package/core/utils/unique-by.js +25 -0
- package/core/virtual-dir.js +11 -10
- package/index.js +152 -115
- package/modules/assets/index.js +4 -2
- package/modules/assets/processors/esbuild-process.js +35 -2
- package/modules/assets/processors/postcss-process.js +36 -2
- package/modules/head/drivers/capo-adapter.js +26 -4
- package/modules/head/drivers/posthtml-head-elements.js +2 -4
- package/modules/head/index.js +7 -10
- package/modules/multilang/index.js +4 -2
- package/modules/navigator/index.js +33 -20
- package/modules/navigator/templates/navigator-core.html +1 -1
- package/modules/sitemap/index.js +7 -3
- package/package.json +4 -2
- package/core/filters/index.js +0 -4
- package/core/global-functions/index.js +0 -6
- package/core/logging.js +0 -32
- package/core/page-context.js +0 -310
- package/core/shortcodes/index.js +0 -2
- package/core/utils/helpers.js +0 -75
- /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
- /package/core/{filters → surface/filters}/isString.js +0 -0
- /package/core/{filters → surface/filters}/related-posts.js +0 -0
- /package/core/{global-functions/date.js → surface/global-date-function.js} +0 -0
package/index.js
CHANGED
|
@@ -3,36 +3,40 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
|
|
4
4
|
import { HtmlBasePlugin } from '@11ty/eleventy';
|
|
5
5
|
import { eleventyImageOnRequestDuringServePlugin } from '@11ty/eleventy-img';
|
|
6
|
+
import markdownItAttrs from 'markdown-it-attrs';
|
|
6
7
|
|
|
7
|
-
import { createLogger } from './core/logging.js';
|
|
8
|
+
import { createLogger, printBannerOnce } from './core/logging/index.js';
|
|
9
|
+
import { isLegacyShape, normalizeLegacyShape } from './core/back-compat/options.js';
|
|
10
|
+
import { settingsSchema } from './core/schema.js';
|
|
11
|
+
import { deriveBaselineState } from './core/state.js';
|
|
12
|
+
import { runPrepass, PREPASS_SENTINEL } from './core/content-graph/index.js';
|
|
13
|
+
import { registerVirtualDir } from './core/virtual-dir.js';
|
|
8
14
|
import { createContentMapStore } from './core/content-map-store.js';
|
|
9
15
|
import { createTranslationMapStore } from './core/translation-map-store.js';
|
|
10
16
|
import { createSlugIndex } from './core/slug-index.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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';
|
|
17
|
+
import { registerPageContext } from './core/page-context/index.js';
|
|
18
|
+
import { autoHeadingIds, safeUse, wikilinks } from './core/markdown/index.js';
|
|
19
|
+
import { slugify } from './core/utils/slugify.js';
|
|
19
20
|
import { assetsCore, headCore, multilangCore, navigatorCore, sitemapCore } from './modules.js';
|
|
21
|
+
import {
|
|
22
|
+
registerGlobals,
|
|
23
|
+
markdownFilter,
|
|
24
|
+
relatedPostsFilter,
|
|
25
|
+
isStringFilter,
|
|
26
|
+
imageShortcode
|
|
27
|
+
} from './core/surface/index.js';
|
|
20
28
|
|
|
21
29
|
const __require = createRequire(import.meta.url);
|
|
22
30
|
const { name, version } = __require('./package.json');
|
|
31
|
+
const eleventyVersion = process.env.ELEVENTY_VERSION;
|
|
32
|
+
// const absoluteRoot = process.env.ELEVENTY_ROOT; -> Safekeeping.
|
|
23
33
|
|
|
24
34
|
const mode = process.env.ELEVENTY_ENV;
|
|
35
|
+
// eslint-disable-next-line no-unused-vars
|
|
25
36
|
const isDev = mode === 'development';
|
|
37
|
+
// eslint-disable-next-line no-unused-vars
|
|
26
38
|
const isProd = mode === 'production';
|
|
27
39
|
|
|
28
|
-
const LEGACY_OPTION_KEYS = [
|
|
29
|
-
'verbose',
|
|
30
|
-
'enableNavigatorTemplate',
|
|
31
|
-
'enableSitemapTemplate',
|
|
32
|
-
'assetsESBuild',
|
|
33
|
-
'multilingual'
|
|
34
|
-
];
|
|
35
|
-
|
|
36
40
|
// Whitelist of reserved global data keys used internally across the plugin.
|
|
37
41
|
// Positive side effect is they all get listed in order and merge data to the same key.
|
|
38
42
|
// Also prevents name collision with filters.
|
|
@@ -44,37 +48,19 @@ const INTERNAL_KEYS = [
|
|
|
44
48
|
'_navigator',
|
|
45
49
|
'_sitemap',
|
|
46
50
|
'_snapshot',
|
|
47
|
-
'_pageContext'
|
|
51
|
+
'eleventyComputed._pageContext',
|
|
52
|
+
'eleventyComputed._node',
|
|
53
|
+
'eleventyComputed._edges',
|
|
54
|
+
'eleventyComputed._backlinks',
|
|
55
|
+
'eleventyComputed._outgoing'
|
|
48
56
|
];
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
}
|
|
58
|
+
// Base logger outputs regardless of options.
|
|
59
|
+
const baseLog = createLogger(null, { verbose: true });
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
61
|
+
printBannerOnce(baseLog, { version, eleventyVersion });
|
|
62
|
+
|
|
63
|
+
let contentGraph = null;
|
|
78
64
|
|
|
79
65
|
/**
|
|
80
66
|
* Baseline (composition root)
|
|
@@ -118,36 +104,29 @@ function splitLegacyOptions(legacy) {
|
|
|
118
104
|
*/
|
|
119
105
|
export default function baseline(settings = {}, options = {}) {
|
|
120
106
|
// --- Legacy compatibility layer ---
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.');
|
|
107
|
+
if (isLegacyShape(settings, arguments.length)) {
|
|
108
|
+
const normalized = normalizeLegacyShape(settings);
|
|
109
|
+
settings = normalized.settings;
|
|
110
|
+
options = normalized.options;
|
|
111
|
+
baseLog.info('Single-object plugin arg is deprecated. Use baseline(settings, options).');
|
|
140
112
|
}
|
|
141
113
|
|
|
142
114
|
// Validate configuration shape (non-fatal).
|
|
143
115
|
const parsed = settingsSchema.safeParse(settings);
|
|
144
116
|
if (!parsed.success) {
|
|
145
117
|
for (const issue of parsed.error.issues) {
|
|
146
|
-
baseLog.info('settings:', `${issue.path.join('.')}
|
|
118
|
+
baseLog.info('settings:', `${issue.path.join('.')}, ${issue.message}`);
|
|
147
119
|
}
|
|
148
120
|
}
|
|
149
121
|
|
|
150
|
-
|
|
122
|
+
// Resolve state once, above the closure. Pure; no eleventyConfig.
|
|
123
|
+
const state = deriveBaselineState(settings, options, { mode });
|
|
124
|
+
baseLog.info('Settings and options resolved, modules loaded');
|
|
125
|
+
|
|
126
|
+
// Scoped logging.
|
|
127
|
+
function scopedLog(name) {
|
|
128
|
+
return createLogger(name, { verbose: state.options.verbose });
|
|
129
|
+
}
|
|
151
130
|
|
|
152
131
|
/**
|
|
153
132
|
* Eleventy plugin initializer.
|
|
@@ -160,26 +139,69 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
160
139
|
try {
|
|
161
140
|
eleventyConfig.versionCheck('>=3.0');
|
|
162
141
|
} catch (e) {
|
|
163
|
-
baseLog.error('Eleventy version mismatch
|
|
142
|
+
baseLog.error('Eleventy version mismatch.', e.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Pre-pass wiring ---
|
|
146
|
+
// One mechanic: the pre-pass runs at the start of every Eleventy
|
|
147
|
+
// build cycle via `eleventy.before`. Initial build, watch rebuild,
|
|
148
|
+
// production build — all the same path. Templates always render
|
|
149
|
+
// against a graph rebuilt from current source. The sentinel keeps
|
|
150
|
+
// the inner Eleventy from re-attaching the hook on re-entry.
|
|
151
|
+
if (process.env[PREPASS_SENTINEL] !== '1') {
|
|
152
|
+
const prepassLog = scopedLog('pre-pass');
|
|
153
|
+
|
|
154
|
+
// Origins HtmlBasePlugin may have rewritten internal hrefs to.
|
|
155
|
+
// Stripped during link extraction so backlinks key on path-only.
|
|
156
|
+
const knownOrigins = new Set(['http://localhost:8080']);
|
|
157
|
+
for (const candidate of [settings.url, process.env.URL]) {
|
|
158
|
+
if (!candidate) continue;
|
|
159
|
+
try {
|
|
160
|
+
knownOrigins.add(new URL(candidate).origin);
|
|
161
|
+
} catch {
|
|
162
|
+
prepassLog.info('No known origins, using localhost only');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
eleventyConfig.on('eleventy.before', async () => {
|
|
167
|
+
contentGraph = await runPrepass(
|
|
168
|
+
eleventyConfig.directories?.input,
|
|
169
|
+
eleventyConfig.directories?.output,
|
|
170
|
+
scopedLog,
|
|
171
|
+
{ quietMode: true, knownOrigins }
|
|
172
|
+
);
|
|
173
|
+
});
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
INTERNAL_KEYS.forEach((key) => {
|
|
177
|
+
// We leave eleventyComputed callback kEys alone, the rest are reserved-empty.
|
|
178
|
+
if (
|
|
179
|
+
key === 'eleventyComputed._pageContext' ||
|
|
180
|
+
key === 'eleventyComputed._node' ||
|
|
181
|
+
key === 'eleventyComputed._edges' ||
|
|
182
|
+
key === 'eleventyComputed._backlinks' ||
|
|
183
|
+
key === 'eleventyComputed._outgoing'
|
|
184
|
+
)
|
|
185
|
+
return;
|
|
167
186
|
eleventyConfig.addGlobalData(key, {});
|
|
168
187
|
});
|
|
169
188
|
|
|
170
189
|
const env = {
|
|
171
|
-
name: 'Eleventy Baseline',
|
|
172
|
-
package: name,
|
|
173
190
|
version,
|
|
174
|
-
|
|
191
|
+
name: 'Eleventy Baseline',
|
|
192
|
+
env: {
|
|
193
|
+
mode,
|
|
194
|
+
package: name
|
|
195
|
+
}
|
|
175
196
|
};
|
|
176
197
|
|
|
177
198
|
eleventyConfig.addGlobalData('_baseline', {
|
|
178
|
-
env
|
|
199
|
+
...env,
|
|
200
|
+
options: state.options
|
|
179
201
|
});
|
|
180
202
|
|
|
181
203
|
if (!settings.url) {
|
|
182
|
-
baseLog.warn('settings.url missing
|
|
204
|
+
baseLog.warn('settings.url missing, canonical URLs will be relative');
|
|
183
205
|
}
|
|
184
206
|
|
|
185
207
|
registerGlobals(eleventyConfig);
|
|
@@ -188,38 +210,12 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
188
210
|
baseHref: process.env.URL || eleventyConfig.pathPrefix
|
|
189
211
|
});
|
|
190
212
|
|
|
191
|
-
// ---
|
|
213
|
+
// --- Feature exposure to templates ---
|
|
192
214
|
const hasImageTransformPlugin = eleventyConfig.hasPlugin('eleventyImageTransformPlugin');
|
|
193
215
|
|
|
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
216
|
eleventyConfig.addGlobalData('_baseline', {
|
|
221
217
|
features: {
|
|
222
|
-
...state.
|
|
218
|
+
...state.features,
|
|
223
219
|
hasImageTransformPlugin
|
|
224
220
|
}
|
|
225
221
|
});
|
|
@@ -234,6 +230,9 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
234
230
|
outputDir: ''
|
|
235
231
|
});
|
|
236
232
|
|
|
233
|
+
const virtualDirLog = scopedLog('virtual-dir');
|
|
234
|
+
virtualDirLog.info('Virtual directories mounted');
|
|
235
|
+
|
|
237
236
|
const directories = {
|
|
238
237
|
input: eleventyConfig.directories?.input,
|
|
239
238
|
output: eleventyConfig.directories?.output,
|
|
@@ -277,6 +276,9 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
277
276
|
get contentMap() {
|
|
278
277
|
return contentMapStore.get();
|
|
279
278
|
},
|
|
279
|
+
get contentGraph() {
|
|
280
|
+
return contentGraph;
|
|
281
|
+
},
|
|
280
282
|
translationMap: translationMapStore,
|
|
281
283
|
slugIndex
|
|
282
284
|
},
|
|
@@ -287,34 +289,68 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
287
289
|
// Page context registry
|
|
288
290
|
const pageContextRegistry = registerPageContext(eleventyConfig, coreContext);
|
|
289
291
|
|
|
290
|
-
//
|
|
292
|
+
// --- Content graph ---
|
|
293
|
+
// Cascade hookup for the content graph. Reads via the runtime getter so
|
|
294
|
+
// serve-mode rebuilds reassigning `contentGraph` are picked up.
|
|
295
|
+
function getNode(pageUrl) {
|
|
296
|
+
return coreContext.runtime.contentGraph?.nodes?.[pageUrl];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getEdges() {
|
|
300
|
+
return coreContext.runtime.contentGraph?.edges ?? [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
eleventyConfig.addGlobalData('eleventyComputed._node', () => (data) => {
|
|
304
|
+
const pageUrl = data.page?.url;
|
|
305
|
+
if (!pageUrl) return undefined;
|
|
306
|
+
|
|
307
|
+
return getNode(pageUrl);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
eleventyConfig.addGlobalData('eleventyComputed._backlinks', () => (data) => {
|
|
311
|
+
const edges = getEdges();
|
|
312
|
+
|
|
313
|
+
const pageUrl = data.page?.url;
|
|
314
|
+
if (!pageUrl) return [];
|
|
315
|
+
|
|
316
|
+
return edges.filter((edge) => edge.to === pageUrl);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
eleventyConfig.addGlobalData('eleventyComputed._outgoing', () => (data) => {
|
|
320
|
+
const edges = getEdges();
|
|
321
|
+
|
|
322
|
+
const pageUrl = data.page?.url;
|
|
323
|
+
if (!pageUrl) return [];
|
|
324
|
+
|
|
325
|
+
return edges.filter((edge) => edge.from === pageUrl);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// --- Markdown engine ---
|
|
329
|
+
// Order matters: attrs first so manual ids are visible to auto-heading-ids'
|
|
330
|
+
// seed pass; wikilinks last since it parses inline tokens independently.
|
|
331
|
+
const mdLog = scopedLog('markdown');
|
|
291
332
|
eleventyConfig.amendLibrary('md', (md) => {
|
|
292
|
-
md
|
|
333
|
+
safeUse(md, 'curly_attributes', markdownItAttrs, undefined, mdLog);
|
|
334
|
+
safeUse(md, 'baseline_auto_heading_ids', autoHeadingIds, { slugify }, mdLog);
|
|
335
|
+
safeUse(md, 'baseline_wikilinks', wikilinks, { slugIndex, pageContextRegistry, translationMapStore }, mdLog);
|
|
293
336
|
});
|
|
294
337
|
|
|
338
|
+
// --- Snapshots ---
|
|
295
339
|
coreContext.snapshots = {
|
|
296
340
|
contentMap: () => contentMapStore.snapshot(),
|
|
297
341
|
pageContext: () => pageContextRegistry.snapshot()
|
|
298
342
|
};
|
|
299
343
|
|
|
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
344
|
// --- Module registry ---
|
|
310
345
|
const moduleRegistry = [
|
|
311
|
-
{ when: features.multilang, name: 'multilang', plugin: multilangCore },
|
|
312
|
-
{ when: features.sitemap, name: 'sitemap', plugin: sitemapCore },
|
|
346
|
+
{ when: state.features.multilang, name: 'multilang', plugin: multilangCore },
|
|
347
|
+
{ when: state.features.sitemap, name: 'sitemap', plugin: sitemapCore },
|
|
313
348
|
{ name: 'navigator', plugin: navigatorCore },
|
|
314
|
-
{ when: features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
|
|
315
|
-
{ when: features.assets, name: 'assets', plugin: assetsCore }
|
|
349
|
+
{ when: state.features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
|
|
350
|
+
{ when: state.features.assets, name: 'assets', plugin: assetsCore }
|
|
316
351
|
];
|
|
317
352
|
|
|
353
|
+
const active = [];
|
|
318
354
|
for (const entry of moduleRegistry) {
|
|
319
355
|
const { when = true, name, plugin, consumes = {} } = entry;
|
|
320
356
|
if (!when) continue;
|
|
@@ -325,6 +361,7 @@ export default function baseline(settings = {}, options = {}) {
|
|
|
325
361
|
};
|
|
326
362
|
|
|
327
363
|
eleventyConfig.addPlugin(plugin, moduleContext);
|
|
364
|
+
active.push(name);
|
|
328
365
|
}
|
|
329
366
|
|
|
330
367
|
// --- Filters ---
|
package/modules/assets/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export function assetsCore(eleventyConfig, moduleContext) {
|
|
|
49
49
|
const parsed = optionsSchema.safeParse(options.assets);
|
|
50
50
|
if (!parsed.success) {
|
|
51
51
|
for (const issue of parsed.error.issues) {
|
|
52
|
-
log.info('options:', `${issue.path.join('.')}
|
|
52
|
+
log.info('options:', `${issue.path.join('.')}, ${issue.message}`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -63,13 +63,15 @@ export function assetsCore(eleventyConfig, moduleContext) {
|
|
|
63
63
|
const watchGlob = TemplatePath.join(assetsDirectory, '**/*.{css,js,svg,png,jpeg,jpg,webp,gif,avif}');
|
|
64
64
|
|
|
65
65
|
if (!assetsDirectory) {
|
|
66
|
-
log.warn('
|
|
66
|
+
log.warn('directories.assets is unset, registerVirtualDir must run before this plugin');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Watch common asset formats so edits trigger reloads during --serve.
|
|
71
71
|
eleventyConfig.addWatchTarget(watchGlob);
|
|
72
72
|
|
|
73
|
+
log.info('Assets pipeline registered');
|
|
74
|
+
|
|
73
75
|
// --- JS (esbuild) ---
|
|
74
76
|
// Register js as a template format. Only index.js files under assets/js/
|
|
75
77
|
// are compiled; everything else (11tydata.js, non-entry scripts) is skipped
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import * as esbuild from 'esbuild';
|
|
2
|
-
import { createLogger } from '../../../core/logging.js';
|
|
2
|
+
import { createLogger } from '../../../core/logging/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* esbuild processor (processor)
|
|
6
|
+
*
|
|
7
|
+
* Bundles a single JS entrypoint with esbuild and returns the text. Used
|
|
8
|
+
* both by the `js` template format compile guard and by the
|
|
9
|
+
* `inlineESbuild` filter in the assets module.
|
|
10
|
+
*
|
|
11
|
+
* Architecture layer:
|
|
12
|
+
* module
|
|
13
|
+
*
|
|
14
|
+
* System role:
|
|
15
|
+
* Stateless bundler called by `modules/assets/index.js`. The compile
|
|
16
|
+
* guard decides which files reach this processor; this file owns the
|
|
17
|
+
* esbuild call itself.
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle:
|
|
20
|
+
* build-time → invoked per matching entrypoint during template compile,
|
|
21
|
+
* or per-call from the inline filter
|
|
22
|
+
*
|
|
23
|
+
* Why this exists:
|
|
24
|
+
* Eleventy treats every `.js` file as a template. A dedicated processor
|
|
25
|
+
* keeps esbuild configuration out of the template format wiring and lets
|
|
26
|
+
* the inline filter reuse the same defaults.
|
|
27
|
+
*
|
|
28
|
+
* Scope:
|
|
29
|
+
* Owns esbuild option defaults and the bundle call. Does not own the
|
|
30
|
+
* compile guard, the watch target, or markup wrapping; the assets
|
|
31
|
+
* module owns those.
|
|
32
|
+
*
|
|
33
|
+
* Data flow:
|
|
34
|
+
* entrypoint path + options → esbuild.build → bundled JS text
|
|
35
|
+
*/
|
|
3
36
|
|
|
4
37
|
const log = createLogger('assets-esbuild');
|
|
5
38
|
const defaultOptions = { minify: true, target: 'es2020' };
|
|
@@ -28,7 +61,7 @@ export default async function assetsESbuild(jsFilePath, options = {}) {
|
|
|
28
61
|
// Return raw JS; markup wrapping is handled by the plugin registration.
|
|
29
62
|
return result.outputFiles[0].text;
|
|
30
63
|
} catch (error) {
|
|
31
|
-
log.error('esbuild failed
|
|
64
|
+
log.error('esbuild failed.', error);
|
|
32
65
|
// Surface a safe JS comment so the caller can decide how to wrap it.
|
|
33
66
|
return '/* Error processing JS */';
|
|
34
67
|
}
|
|
@@ -2,7 +2,41 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import postcss from 'postcss';
|
|
3
3
|
import loadPostCSSConfig from 'postcss-load-config';
|
|
4
4
|
import fallbackPostCSSConfig from '../configs/postcss.config.js';
|
|
5
|
-
import { createLogger } from '../../../core/logging.js';
|
|
5
|
+
import { createLogger } from '../../../core/logging/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PostCSS processor (processor)
|
|
9
|
+
*
|
|
10
|
+
* Processes a single CSS entrypoint through PostCSS and returns the text.
|
|
11
|
+
* Used both by the `css` template format compile guard and by the
|
|
12
|
+
* `inlinePostCSS` filter in the assets module.
|
|
13
|
+
*
|
|
14
|
+
* Architecture layer:
|
|
15
|
+
* module
|
|
16
|
+
*
|
|
17
|
+
* System role:
|
|
18
|
+
* Stateless processor called by `modules/assets/index.js`. Resolves the
|
|
19
|
+
* user's PostCSS config from the project root, falling back to the
|
|
20
|
+
* bundled Baseline config when none is found. Cached for the lifetime
|
|
21
|
+
* of the process.
|
|
22
|
+
*
|
|
23
|
+
* Lifecycle:
|
|
24
|
+
* build-time → invoked per matching entrypoint during template compile,
|
|
25
|
+
* or per-call from the inline filter
|
|
26
|
+
*
|
|
27
|
+
* Why this exists:
|
|
28
|
+
* Eleventy has no PostCSS hook of its own. A dedicated processor lets
|
|
29
|
+
* user configs win when present and keeps the bundled fallback out of
|
|
30
|
+
* the consumer's `node_modules` resolution path.
|
|
31
|
+
*
|
|
32
|
+
* Scope:
|
|
33
|
+
* Owns config resolution, caching, and the PostCSS call. Does not own
|
|
34
|
+
* the compile guard, the watch target, or markup wrapping; the assets
|
|
35
|
+
* module owns those.
|
|
36
|
+
*
|
|
37
|
+
* Data flow:
|
|
38
|
+
* entrypoint path → PostCSS pipeline → processed CSS text
|
|
39
|
+
*/
|
|
6
40
|
|
|
7
41
|
const log = createLogger('assets-postcss');
|
|
8
42
|
|
|
@@ -45,7 +79,7 @@ export default async function assetsPostCSS(cssFilePath) {
|
|
|
45
79
|
// Return raw CSS; markup wrapping is handled in the plugin registration.
|
|
46
80
|
return result.css;
|
|
47
81
|
} catch (error) {
|
|
48
|
-
log.error('PostCSS failed
|
|
82
|
+
log.error('PostCSS failed.', error);
|
|
49
83
|
// Surface a safe CSS string so the caller can decide how to wrap it.
|
|
50
84
|
return '/* Error processing CSS */';
|
|
51
85
|
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Capo PostHTML adapter (driver)
|
|
3
3
|
*
|
|
4
|
-
* Implements the HTMLAdapter interface capo.js v2
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Implements the `HTMLAdapter` interface capo.js v2 expects, against
|
|
5
|
+
* PostHTML's `{ tag, attrs, content }` node shape. Only `getWeight` is
|
|
6
|
+
* exercised downstream; the rest are shimmed to satisfy the contract.
|
|
7
|
+
*
|
|
8
|
+
* Architecture layer:
|
|
9
|
+
* module
|
|
10
|
+
*
|
|
11
|
+
* System role:
|
|
12
|
+
* Translation shim between the head driver's PostHTML tree and the
|
|
13
|
+
* capo.js sort. Used by `posthtml-head-elements.js` when ordering the
|
|
14
|
+
* composed `<head>` element list.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle:
|
|
17
|
+
* transform-time → invoked per node while capo.js scores element weights
|
|
18
|
+
*
|
|
19
|
+
* Why this exists:
|
|
20
|
+
* capo.js is DOM-shaped; PostHTML is not. Without an adapter the driver
|
|
21
|
+
* would have to walk the tree twice or hand-roll element weighting.
|
|
22
|
+
*
|
|
23
|
+
* Scope:
|
|
24
|
+
* Owns attribute lookup, tag name resolution, and text extraction over
|
|
25
|
+
* PostHTML nodes. Does not own weighting logic; capo.js owns that.
|
|
26
|
+
*
|
|
27
|
+
* Data flow:
|
|
28
|
+
* PostHTML node → adapter accessor → capo.js getWeight
|
|
7
29
|
*
|
|
8
30
|
* A PostHTML element node looks like `{ tag, attrs, content }` where attrs
|
|
9
31
|
* is either undefined or a plain object. Boolean attributes appear with an
|
|
@@ -10,7 +10,7 @@ import { dedupeMeta, dedupeLink } from '../utils/dedupe.js';
|
|
|
10
10
|
* <baseline-head> placeholder with the result.
|
|
11
11
|
*
|
|
12
12
|
* Architecture layer:
|
|
13
|
-
* module
|
|
13
|
+
* module
|
|
14
14
|
*
|
|
15
15
|
* System role:
|
|
16
16
|
* The seam between head's pipeline and the renderer choice. Alternate
|
|
@@ -41,10 +41,9 @@ import { dedupeMeta, dedupeLink } from '../utils/dedupe.js';
|
|
|
41
41
|
* @param {Object} args.options - Head options (titleSeparator, showGenerator).
|
|
42
42
|
* @param {string} args.placeholderTag - Placeholder element to replace.
|
|
43
43
|
* @param {string} args.eol - End-of-line separator interleaved between nodes.
|
|
44
|
-
* @param {Object} args.log - Scoped logger.
|
|
45
44
|
* @returns {(tree: Object) => Object} PostHTML plugin function.
|
|
46
45
|
*/
|
|
47
|
-
export function renderHead({ seeds, alternates, options, placeholderTag, eol
|
|
46
|
+
export function renderHead({ seeds, alternates, options, placeholderTag, eol }) {
|
|
48
47
|
const defaults = emitMeta(seeds.meta, seeds.render, options);
|
|
49
48
|
const extras = emitExtras(seeds.head, alternates);
|
|
50
49
|
|
|
@@ -52,7 +51,6 @@ export function renderHead({ seeds, alternates, options, placeholderTag, eol, lo
|
|
|
52
51
|
const sorted = capoSort(deduped);
|
|
53
52
|
|
|
54
53
|
return function rendererPlugin(tree) {
|
|
55
|
-
// log.info('injecting head for', seeds.page.inputPath || seeds.page.url);
|
|
56
54
|
tree.match({ tag: placeholderTag }, () => ({
|
|
57
55
|
tag: 'head',
|
|
58
56
|
content: interleaveEOL(sorted, eol)
|
package/modules/head/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { renderHead } from './drivers/posthtml-head-elements.js';
|
|
|
2
2
|
import { buildAlternates } from './utils/alternates.js';
|
|
3
3
|
import { optionsSchema } from './schema.js';
|
|
4
4
|
|
|
5
|
+
import chalk from 'kleur';
|
|
6
|
+
|
|
5
7
|
// Internal constants — not user-facing.
|
|
6
8
|
const PLACEHOLDER_TAG = 'baseline-head';
|
|
7
9
|
const EOL = '\n';
|
|
@@ -52,7 +54,7 @@ export function headCore(eleventyConfig, moduleContext) {
|
|
|
52
54
|
const parsed = optionsSchema.safeParse(options.head);
|
|
53
55
|
if (!parsed.success) {
|
|
54
56
|
for (const issue of parsed.error.issues) {
|
|
55
|
-
log.info('options:', `${issue.path.join('.')}
|
|
57
|
+
log.info('options:', `${issue.path.join('.')}, ${issue.message}`);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -68,23 +70,19 @@ export function headCore(eleventyConfig, moduleContext) {
|
|
|
68
70
|
const headStats = { pages: new Set() };
|
|
69
71
|
|
|
70
72
|
eleventyConfig.on('eleventy.after', () => {
|
|
71
|
-
log.info({
|
|
72
|
-
message: 'Head injection summary',
|
|
73
|
-
totalPages: headStats.pages.size,
|
|
74
|
-
sample: Array.from(headStats.pages).slice(0, 10)
|
|
75
|
-
});
|
|
73
|
+
log.info(chalk.green(`Head injected into ${headStats.pages.size} pages`));
|
|
76
74
|
headStats.pages.clear();
|
|
77
75
|
});
|
|
78
76
|
|
|
79
77
|
// --- Transform-time: compose and inject. ---
|
|
80
|
-
log.info('Injecting heads
|
|
78
|
+
log.info('Injecting heads');
|
|
81
79
|
eleventyConfig.htmlTransformer.addPosthtmlPlugin('html', function (context) {
|
|
82
80
|
headStats.pages.add(context?.page?.inputPath || context?.outputPath);
|
|
83
81
|
|
|
84
82
|
const key = context?.page?.url ?? context?.page?.inputPath;
|
|
85
83
|
const seeds = pageContextRegistry?.getByKey(key);
|
|
86
84
|
if (!seeds) {
|
|
87
|
-
log.warn('
|
|
85
|
+
log.warn('No head seeds for', context?.page?.inputPath || context?.outputPath);
|
|
88
86
|
return (tree) => tree;
|
|
89
87
|
}
|
|
90
88
|
|
|
@@ -99,8 +97,7 @@ export function headCore(eleventyConfig, moduleContext) {
|
|
|
99
97
|
alternates,
|
|
100
98
|
options: headOptions,
|
|
101
99
|
placeholderTag: PLACEHOLDER_TAG,
|
|
102
|
-
eol: EOL
|
|
103
|
-
log
|
|
100
|
+
eol: EOL
|
|
104
101
|
});
|
|
105
102
|
});
|
|
106
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { I18nPlugin } from '@11ty/eleventy';
|
|
2
2
|
import { DeepCopy } from '@11ty/eleventy-utils';
|
|
3
|
-
import { normalizeLanguages } from '../../core/utils/
|
|
3
|
+
import { normalizeLanguages } from '../../core/utils/normalize-languages.js';
|
|
4
4
|
import i18nTranslationsFor from './filters/i18n-translations-for.js';
|
|
5
5
|
import i18nTranslationIn from './filters/i18n-translation-in.js';
|
|
6
6
|
import i18nDefaultTranslation from './filters/i18n-default-translation.js';
|
|
@@ -63,10 +63,12 @@ export function multilangCore(eleventyConfig, moduleContext) {
|
|
|
63
63
|
const isMultilingual = options.multilang === true && defaultLanguage && hasLanguages;
|
|
64
64
|
|
|
65
65
|
if (!isMultilingual) {
|
|
66
|
-
log.info('inactive
|
|
66
|
+
log.info('Multilang inactive, needs options.multilang, settings.defaultLanguage, and languages');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
log.info(`Multilang active: ${Object.keys(languages).join('/')} (default: ${defaultLanguage})`);
|
|
71
|
+
|
|
70
72
|
// Register Eleventy's built-in I18nPlugin for locale-aware URL resolution.
|
|
71
73
|
eleventyConfig.addPlugin(I18nPlugin, {
|
|
72
74
|
defaultLanguage: defaultLanguage,
|