@apleasantview/eleventy-plugin-baseline 0.1.0-next.32 → 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 +6 -1
- package/core/filters/markdown.js +6 -0
- package/core/filters/related-posts.js +7 -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 +167 -144
- 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/processors/esbuild-process.js +35 -0
- package/modules/assets/processors/postcss-process.js +52 -0
- 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 +14 -8
- package/modules/navigator/utils/debug.js +41 -0
- package/modules/sitemap/index.js +121 -0
- package/modules/sitemap/templates/sitemap-core.html +34 -0
- package/modules/{sitemap-core → sitemap}/templates/sitemap-index.html +2 -2
- package/modules.js +6 -0
- package/package.json +15 -6
- package/core/debug.js +0 -20
- package/core/filters.js +0 -9
- package/core/globals.js +0 -6
- package/core/helpers.js +0 -127
- package/core/modules.js +0 -22
- package/core/shortcodes.js +0 -3
- package/eleventy.config.js +0 -157
- package/modules/assets-core/plugins/assets-core.js +0 -84
- package/modules/assets-esbuild/filters/inline-esbuild.js +0 -24
- package/modules/assets-esbuild/plugins/assets-esbuild.js +0 -71
- package/modules/assets-postcss/filters/inline-postcss.js +0 -38
- package/modules/assets-postcss/plugins/assets-postcss.js +0 -75
- package/modules/head-core/drivers/posthtml-head-elements.js +0 -132
- package/modules/head-core/plugins/head-core.js +0 -57
- package/modules/head-core/utils/head-utils.js +0 -183
- package/modules/multilang-core/plugins/multilang-core.js +0 -101
- package/modules/navigator-core/plugins/navigator-core.js +0 -39
- package/modules/sitemap-core/plugins/sitemap-core.js +0 -65
- package/modules/sitemap-core/templates/sitemap-core.html +0 -25
- /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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { TemplatePath } from '@11ty/eleventy-utils';
|
|
2
|
+
import { resolveSubdir } from './utils/helpers.js';
|
|
3
|
+
import { createLogger } from './logging.js';
|
|
4
|
+
import { getScope, addScopeListener, setEntry } from './registry.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Virtual directories (runtime substrate)
|
|
8
|
+
*
|
|
9
|
+
* Synthesises extra keys on eleventyConfig.directories (e.g. `assets`,
|
|
10
|
+
* `public`) that Eleventy itself won't accept, and keeps them in sync once
|
|
11
|
+
* Eleventy finalises its real directory map.
|
|
12
|
+
*
|
|
13
|
+
* Architecture layer:
|
|
14
|
+
* runtime substrate
|
|
15
|
+
*
|
|
16
|
+
* System role:
|
|
17
|
+
* Adds virtual dir keys consumed by modules (assets reads
|
|
18
|
+
* `directories.assets`) and by the composition root (passthrough copy from
|
|
19
|
+
* `directories.public`).
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle:
|
|
22
|
+
* build-time → synthesise key, pre-populate cache from eleventyConfig.dir
|
|
23
|
+
* build-time → on `eleventy.directories`, refresh the cache to final paths
|
|
24
|
+
*
|
|
25
|
+
* Why this exists:
|
|
26
|
+
* Eleventy's ProjectDirectories.setViaConfigObject() only honours input,
|
|
27
|
+
* output, data, includes, and layouts. Extra `dir.*` keys are silently
|
|
28
|
+
* ignored, and the `eleventy.directories` event exposes only the same set.
|
|
29
|
+
* Synthesis fills the gap so consumers can read additional dirs the same
|
|
30
|
+
* way they read the real ones.
|
|
31
|
+
*
|
|
32
|
+
* Scope:
|
|
33
|
+
* Owns synthesis of extra `eleventyConfig.directories` keys, the live
|
|
34
|
+
* cache, and a single shared listener for sync.
|
|
35
|
+
* Does not own passthrough copy or watch wiring (composition root and
|
|
36
|
+
* modules own those).
|
|
37
|
+
*
|
|
38
|
+
* Data flow:
|
|
39
|
+
* { name, outputDir } → eleventyConfig.directories[name] getter →
|
|
40
|
+
* live { input, output } cache → consumers
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const SCOPE_NAME = 'core:virtual-dir';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a virtual directory on eleventyConfig.directories.
|
|
47
|
+
*
|
|
48
|
+
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
|
|
49
|
+
* @param {Object} options
|
|
50
|
+
* @param {string} options.key - Key to synthesise (e.g. 'assets', 'public').
|
|
51
|
+
* @param {string} [options.outputDir] - Override the output subdirectory. Defaults
|
|
52
|
+
* to the raw dir value (symmetric with input). Pass `''` to resolve to the
|
|
53
|
+
* output root (used by `public`, which copies to `/`).
|
|
54
|
+
* @returns {{input: string, output: string}} Live cache; properties refresh when
|
|
55
|
+
* eleventy.directories fires. Safe to read at plugin-init time.
|
|
56
|
+
*/
|
|
57
|
+
export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
|
|
58
|
+
if (!key) {
|
|
59
|
+
throw new Error('registerVirtualDir: `name` is required');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const log = createLogger(SCOPE_NAME);
|
|
63
|
+
const scope = getScope(eleventyConfig, SCOPE_NAME);
|
|
64
|
+
const rawDir = eleventyConfig.dir?.[key] || key;
|
|
65
|
+
const rawOutputDir = outputDir ?? rawDir;
|
|
66
|
+
const cache = { input: null, output: null };
|
|
67
|
+
|
|
68
|
+
// Pre-populate from eleventyConfig.dir so synchronous readers at plugin-init
|
|
69
|
+
// time (watch globs, ignores, compile-guard prefixes) see a valid path.
|
|
70
|
+
syncCache(cache, eleventyConfig.dir || {}, rawDir, rawOutputDir);
|
|
71
|
+
|
|
72
|
+
setEntry(scope, key, { rawDir, rawOutputDir, cache });
|
|
73
|
+
|
|
74
|
+
// Define the virtual key once. The getter reads the live cache, which the
|
|
75
|
+
// shared listener below refreshes when Eleventy emits its final directories.
|
|
76
|
+
const existing = Object.getOwnPropertyDescriptor(eleventyConfig.directories, key);
|
|
77
|
+
if (existing && existing.configurable === false) {
|
|
78
|
+
log.info(`directories[${key}] already defined; skipping`);
|
|
79
|
+
} else {
|
|
80
|
+
Object.defineProperty(eleventyConfig.directories, key, {
|
|
81
|
+
get() {
|
|
82
|
+
return cache.input;
|
|
83
|
+
},
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: false
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// One listener services all virtual dirs registered on this config.
|
|
90
|
+
// addScopeListener dedupes on ('eleventy.directories', 'sync'), so
|
|
91
|
+
// subsequent registerVirtualDir calls don't stack handlers.
|
|
92
|
+
addScopeListener(eleventyConfig, SCOPE_NAME, 'eleventy.directories', 'sync', (scope, dirs) => {
|
|
93
|
+
for (const entry of scope.values.values()) {
|
|
94
|
+
syncCache(entry.cache, dirs, entry.rawDir, entry.rawOutputDir);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
log.info('Virtual directories mounted');
|
|
99
|
+
|
|
100
|
+
return cache;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function syncCache(cache, dirs, rawDir, rawOutputDir) {
|
|
104
|
+
const inputDir = TemplatePath.addLeadingDotSlash(dirs.input || './');
|
|
105
|
+
const outputDir = TemplatePath.addLeadingDotSlash(dirs.output || './');
|
|
106
|
+
|
|
107
|
+
// resolveSubdir symmetrically resolves against input and output; call twice
|
|
108
|
+
// so input and output subdirs can differ (e.g. `public` copies to root).
|
|
109
|
+
cache.input = resolveSubdir(inputDir, outputDir, rawDir).input;
|
|
110
|
+
cache.output = resolveSubdir(inputDir, outputDir, rawOutputDir).output;
|
|
111
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { slugify } from './utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WIKILINKS (filter)
|
|
5
|
+
*
|
|
6
|
+
* MediaWiki-style inline link syntax for body markdown. Recognises
|
|
7
|
+
* [[slug]], [[slug#anchor]], [[slug:lang]], [[slug|alias]], and any
|
|
8
|
+
* combination. Resolves slugs against the slug index and, when a lang
|
|
9
|
+
* suffix is given, hops to the requested translation. Misses render as
|
|
10
|
+
* the original literal text.
|
|
11
|
+
*
|
|
12
|
+
* Architecture layer:
|
|
13
|
+
* runtime substrate
|
|
14
|
+
*
|
|
15
|
+
* System role:
|
|
16
|
+
* Markdown-it plugin registered by the composition root via
|
|
17
|
+
* amendLibrary('md', ...). Reads the slug index, page-context registry,
|
|
18
|
+
* and translation-map store; writes nothing back.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle:
|
|
21
|
+
* transform-time → parses [[...]] in body markdown and emits link or
|
|
22
|
+
* text tokens
|
|
23
|
+
*
|
|
24
|
+
* Why this exists:
|
|
25
|
+
* Markdown-it has no wiki-link syntax. Eleventy resolves all
|
|
26
|
+
* eleventyComputed values before any body renders, so the slug index
|
|
27
|
+
* and translation map are complete by the time this rule fires —
|
|
28
|
+
* resolution is deterministic without a two-pass build.
|
|
29
|
+
*
|
|
30
|
+
* Scope:
|
|
31
|
+
* Owns syntax parsing, slug-to-href resolution, the lang hop, and link
|
|
32
|
+
* rendering with class/lang/hreflang attributes.
|
|
33
|
+
* Does not own slug derivation (page-context), index population, or the
|
|
34
|
+
* translation-map shape (multilang module).
|
|
35
|
+
*
|
|
36
|
+
* Data flow:
|
|
37
|
+
* markdown-it inline state → slug index + page context + translation map → link tokens
|
|
38
|
+
*
|
|
39
|
+
* @param {import('markdown-it').default} md
|
|
40
|
+
* @param {Object} deps
|
|
41
|
+
* @param {{getBySlug: (slug: string) => string | null}} deps.slugIndex
|
|
42
|
+
* @param {{getByKey: (url: string) => any}} deps.pageContextRegistry
|
|
43
|
+
* @param {{get: () => Record<string, Record<string, {url: string, title?: string}>> | null}} [deps.translationMapStore]
|
|
44
|
+
*/
|
|
45
|
+
export function wikilinks(md, { slugIndex, pageContextRegistry, translationMapStore } = {}) {
|
|
46
|
+
if (!slugIndex || !pageContextRegistry) {
|
|
47
|
+
throw new Error('wikilinks plugin requires { slugIndex, pageContextRegistry }');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parse(inner) {
|
|
51
|
+
// [[target|alias]]
|
|
52
|
+
const pipeIdx = inner.indexOf('|');
|
|
53
|
+
const target = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);
|
|
54
|
+
const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1).trim() || null;
|
|
55
|
+
|
|
56
|
+
// [[slug-or-langed#anchor]]
|
|
57
|
+
const hashIdx = target.indexOf('#');
|
|
58
|
+
const slugAndLang = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
59
|
+
const rawAnchor = hashIdx === -1 ? null : target.slice(hashIdx + 1).trim() || null;
|
|
60
|
+
const anchor = rawAnchor ? slugify(rawAnchor) : null;
|
|
61
|
+
|
|
62
|
+
// [[slug:lang]]
|
|
63
|
+
const colonIdx = slugAndLang.indexOf(':');
|
|
64
|
+
const rawSlug = colonIdx === -1 ? slugAndLang : slugAndLang.slice(0, colonIdx);
|
|
65
|
+
const rawLang = colonIdx === -1 ? null : slugAndLang.slice(colonIdx + 1).trim() || null;
|
|
66
|
+
|
|
67
|
+
return { rawSlug: rawSlug.trim(), rawLang, anchor, alias };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolve({ rawSlug, rawLang, anchor, alias }) {
|
|
71
|
+
const slug = slugify(rawSlug);
|
|
72
|
+
if (!slug) return null;
|
|
73
|
+
|
|
74
|
+
const url = slugIndex.getBySlug(slug);
|
|
75
|
+
if (!url) return null;
|
|
76
|
+
|
|
77
|
+
const ctx = pageContextRegistry.getByKey(url);
|
|
78
|
+
const defaultTitle = ctx?.entry?.title ?? rawSlug;
|
|
79
|
+
|
|
80
|
+
const withAnchor = (u) => (anchor ? `${u}#${anchor}` : u);
|
|
81
|
+
|
|
82
|
+
if (!rawLang) {
|
|
83
|
+
return {
|
|
84
|
+
href: withAnchor(url),
|
|
85
|
+
label: alias ?? defaultTitle,
|
|
86
|
+
lang: null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lang = rawLang.toLowerCase();
|
|
91
|
+
const translationKey = ctx?.page?.locale?.translationKey;
|
|
92
|
+
if (!translationKey) return null;
|
|
93
|
+
|
|
94
|
+
const map = translationMapStore?.get?.();
|
|
95
|
+
const entry = map?.[translationKey]?.[lang];
|
|
96
|
+
if (!entry) return null;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
href: withAnchor(entry.url),
|
|
100
|
+
label: alias ?? entry.title ?? defaultTitle,
|
|
101
|
+
lang
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tokenize(state, silent) {
|
|
106
|
+
const start = state.pos;
|
|
107
|
+
const src = state.src;
|
|
108
|
+
|
|
109
|
+
if (src.charCodeAt(start) !== 0x5b /* [ */) return false;
|
|
110
|
+
if (src.charCodeAt(start + 1) !== 0x5b) return false;
|
|
111
|
+
|
|
112
|
+
const end = src.indexOf(']]', start + 2);
|
|
113
|
+
if (end === -1) return false;
|
|
114
|
+
|
|
115
|
+
const inner = src.slice(start + 2, end);
|
|
116
|
+
// Reject nested brackets to keep the rule unambiguous.
|
|
117
|
+
if (inner.includes('[') || inner.includes(']')) return false;
|
|
118
|
+
|
|
119
|
+
if (silent) {
|
|
120
|
+
state.pos = end + 2;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const literal = src.slice(start, end + 2);
|
|
125
|
+
const parsed = parse(inner);
|
|
126
|
+
const result = resolve(parsed);
|
|
127
|
+
|
|
128
|
+
if (!result) {
|
|
129
|
+
const t = state.push('text', '', 0);
|
|
130
|
+
t.content = literal;
|
|
131
|
+
} else {
|
|
132
|
+
const open = state.push('link_open', 'a', 1);
|
|
133
|
+
const attrs = [
|
|
134
|
+
['href', result.href],
|
|
135
|
+
['class', 'wikilink']
|
|
136
|
+
];
|
|
137
|
+
if (result.lang) {
|
|
138
|
+
attrs.push(['lang', result.lang]);
|
|
139
|
+
attrs.push(['hreflang', result.lang]);
|
|
140
|
+
}
|
|
141
|
+
open.attrs = attrs;
|
|
142
|
+
const text = state.push('text', '', 0);
|
|
143
|
+
text.content = result.label;
|
|
144
|
+
state.push('link_close', 'a', -1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
state.pos = end + 2;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
md.inline.ruler.before('link', 'wikilink', tokenize);
|
|
152
|
+
}
|
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
|
+
};
|