@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.
Files changed (54) hide show
  1. package/README.md +21 -23
  2. package/core/back-compat/options.js +69 -0
  3. package/core/content-graph/backlinks.js +65 -0
  4. package/core/content-graph/extractors.js +140 -0
  5. package/core/content-graph/graph.js +118 -0
  6. package/core/content-graph/index.js +2 -0
  7. package/core/content-graph/prepass.js +121 -0
  8. package/core/logging/banner.js +49 -0
  9. package/core/logging/index.js +80 -0
  10. package/core/logging/quips.js +30 -0
  11. package/core/markdown/auto-heading-ids.js +86 -0
  12. package/core/markdown/index.js +5 -0
  13. package/core/markdown/safe-use.js +42 -0
  14. package/core/{wikilinks.js → markdown/wikilinks.js} +6 -6
  15. package/core/page-context/build.js +239 -0
  16. package/core/page-context/index.js +1 -0
  17. package/core/page-context/register.js +73 -0
  18. package/core/page-context/seo-helpers.js +56 -0
  19. package/core/schema.js +52 -1
  20. package/core/slug-index.js +2 -2
  21. package/core/state.js +73 -0
  22. package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
  23. package/core/surface/index.js +22 -0
  24. package/core/types.js +1 -1
  25. package/core/utils/add-trailing-slash.js +11 -0
  26. package/core/utils/ensure-dot-slash-dir.js +13 -0
  27. package/core/utils/normalize-languages.js +28 -0
  28. package/core/utils/resolve-field.js +9 -0
  29. package/core/utils/resolve-subdir.js +20 -0
  30. package/core/utils/slugify.js +15 -0
  31. package/core/utils/unique-by.js +25 -0
  32. package/core/virtual-dir.js +11 -10
  33. package/index.js +152 -115
  34. package/modules/assets/index.js +4 -2
  35. package/modules/assets/processors/esbuild-process.js +35 -2
  36. package/modules/assets/processors/postcss-process.js +36 -2
  37. package/modules/head/drivers/capo-adapter.js +26 -4
  38. package/modules/head/drivers/posthtml-head-elements.js +2 -4
  39. package/modules/head/index.js +7 -10
  40. package/modules/multilang/index.js +4 -2
  41. package/modules/navigator/index.js +33 -20
  42. package/modules/navigator/templates/navigator-core.html +1 -1
  43. package/modules/sitemap/index.js +7 -3
  44. package/package.json +4 -2
  45. package/core/filters/index.js +0 -4
  46. package/core/global-functions/index.js +0 -6
  47. package/core/logging.js +0 -32
  48. package/core/page-context.js +0 -310
  49. package/core/shortcodes/index.js +0 -2
  50. package/core/utils/helpers.js +0 -75
  51. /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
  52. /package/core/{filters → surface/filters}/isString.js +0 -0
  53. /package/core/{filters → surface/filters}/related-posts.js +0 -0
  54. /package/core/{global-functions/date.js → surface/global-date-function.js} +0 -0
package/core/schema.js CHANGED
@@ -1,5 +1,38 @@
1
1
  import * as z from 'zod';
2
2
 
3
+ /**
4
+ * Schemas (runtime substrate)
5
+ *
6
+ * Zod schemas for the two user-facing inputs Baseline validates: the
7
+ * directory `config` export and the `settings` argument. Structural only.
8
+ * Value-level preferences stay permissive.
9
+ *
10
+ * Architecture layer:
11
+ * runtime substrate
12
+ *
13
+ * System role:
14
+ * Validation seam at the public boundary. The composition root parses
15
+ * `settings` non-fatally at init; the directory `config` is checked in
16
+ * the test suite, not at runtime.
17
+ *
18
+ * Lifecycle:
19
+ * build-time → composition root calls `settingsSchema.safeParse(settings)`
20
+ * and logs structural mismatches under `info`
21
+ *
22
+ * Why this exists:
23
+ * Eleventy accepts almost anything users pass through `addPlugin`. A
24
+ * structural gate catches typos and shape drift early without forcing
25
+ * a hard failure on imperfect input.
26
+ *
27
+ * Scope:
28
+ * Owns the structural shape of `settings` and `config`. Does not own
29
+ * defaults, value semantics, or required-field policy; those live in
30
+ * the composition root and individual modules.
31
+ *
32
+ * Data flow:
33
+ * user input → safeParse → issues logged or accepted
34
+ */
35
+
3
36
  export const configSchema = z.object({
4
37
  dir: z.object({
5
38
  input: z.string().min(1),
@@ -25,7 +58,25 @@ export const settingsSchema = z.object({
25
58
  url: z.string().optional(),
26
59
  noindex: z.boolean().optional(),
27
60
  defaultLanguage: z.string().optional(),
28
- languages: z.record(z.string(), z.looseObject({})).optional(),
61
+ languages: z
62
+ .unknown()
63
+ .optional()
64
+ .superRefine((value, ctx) => {
65
+ if (value === undefined) return;
66
+
67
+ if (Array.isArray(value)) {
68
+ const arrayResult = z.array(z.string().min(1)).safeParse(value);
69
+ if (!arrayResult.success) {
70
+ for (const issue of arrayResult.error.issues) ctx.addIssue(issue);
71
+ }
72
+ return;
73
+ }
74
+
75
+ const recordResult = z.record(z.string(), z.looseObject({})).safeParse(value);
76
+ if (!recordResult.success) {
77
+ for (const issue of recordResult.error.issues) ctx.addIssue(issue);
78
+ }
79
+ }),
29
80
  head: z
30
81
  .object({
31
82
  link: z.array(z.looseObject({})).optional(),
@@ -35,7 +35,7 @@ const SCOPE_NAME = 'core:slug-index';
35
35
  * page-context.buildPageContext → set() → registry scope → wikilinks getBySlug()
36
36
  *
37
37
  * @param {import('@11ty/eleventy').UserConfig} eleventyConfig
38
- * @returns {{set: (slug: string, url: string, inputPath?: string) => void, getBySlug: (slug: string) => string | null, snapshot: () => Record<string, {url: string, inputPath?: string}>}}
38
+ * @returns {{set: (slug: string, url: string, inputPath?: string) => void, getBySlug: (slug: string) => string | undefined, snapshot: () => Record<string, {url: string, inputPath?: string}>}}
39
39
  */
40
40
  export function createSlugIndex(eleventyConfig) {
41
41
  const scope = getScope(eleventyConfig, SCOPE_NAME);
@@ -52,7 +52,7 @@ export function createSlugIndex(eleventyConfig) {
52
52
  setEntry(scope, slug, { url, inputPath });
53
53
  },
54
54
  getBySlug(slug) {
55
- return getEntry(scope, slug)?.url ?? null;
55
+ return getEntry(scope, slug)?.url;
56
56
  },
57
57
  snapshot() {
58
58
  return Object.fromEntries(scope.values);
package/core/state.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * State derivation (composition root helper)
3
+ *
4
+ * Pure normalisation of user-supplied `settings` and `options` into the
5
+ * resolved `state` shape modules read from. No eleventyConfig, no
6
+ * environment reads beyond the `mode` argument, no side effects.
7
+ *
8
+ * Architecture layer:
9
+ * composition root (pure helper)
10
+ *
11
+ * System role:
12
+ * The single place that applies defaults, fallbacks, and feature
13
+ * inference. Extracted from the entry point so it can be reasoned
14
+ * about — and tested — without booting Eleventy.
15
+ *
16
+ * Why this exists:
17
+ * Keeping defaults and feature derivation tangled with eleventyConfig
18
+ * wiring made the entry point hard to scan. Pulling the pure half out
19
+ * leaves the composition root as a list of registration steps.
20
+ *
21
+ * Scope:
22
+ * Owns settings/options normalisation and the derived `features` map.
23
+ * Does not own validation (see core/schema.js) or any runtime wiring.
24
+ *
25
+ * Data flow:
26
+ * settings + options + { mode } → { settings, options, features }
27
+ *
28
+ * @param {import('./types.js').BaselineSettings} settings
29
+ * @param {import('./types.js').BaselineOptions} options
30
+ * @param {{ mode?: string }} [env]
31
+ * @returns {import('./types.js').BaselineState & { features: Readonly<Record<string, boolean>> }}
32
+ */
33
+ export function deriveBaselineState(settings, options, { mode } = {}) {
34
+ const isDev = mode === 'development';
35
+
36
+ const resolvedSettings = {
37
+ title: settings.title,
38
+ tagline: settings.tagline,
39
+ url: settings.url,
40
+ noindex: settings.noindex ?? false,
41
+ defaultLanguage: settings.defaultLanguage,
42
+ languages: settings.languages,
43
+ head: settings.head
44
+ };
45
+
46
+ const resolvedOptions = {
47
+ verbose: options.verbose ?? true,
48
+ multilang: options.multilingual ?? false,
49
+ sitemap: options.sitemap ?? options.enableSitemapTemplate ?? true,
50
+ navigator: options.navigator ?? options.enableNavigatorTemplate ?? isDev,
51
+ head: {
52
+ titleSeparator: options.head?.titleSeparator,
53
+ showGenerator: options.head?.showGenerator
54
+ },
55
+ assets: {
56
+ esbuild: options.assets?.esbuild ?? options.assetsESBuild ?? {}
57
+ }
58
+ };
59
+
60
+ const features = Object.freeze({
61
+ multilang: Boolean(resolvedOptions.multilang),
62
+ sitemap: Boolean(resolvedOptions.sitemap),
63
+ navigator: Boolean(resolvedOptions.navigator),
64
+ head: true,
65
+ assets: true
66
+ });
67
+
68
+ return Object.freeze({
69
+ settings: Object.freeze(resolvedSettings),
70
+ options: Object.freeze(resolvedOptions),
71
+ features
72
+ });
73
+ }
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import Image from '@11ty/eleventy-img';
3
- import { createLogger } from '../logging.js';
3
+ import { createLogger } from '../logging/index.js';
4
4
 
5
5
  // Module-level logger. Image shortcode only uses `.warn`, which emits regardless
6
6
  // of verbose, so we don't thread verbose through the shortcode signature.
7
- const log = createLogger('image');
7
+ const log = createLogger('image-shortcode');
8
8
 
9
9
  const DEFAULT_WIDTHS = [320, 640, 960, 1280, 1920, 'auto'];
10
10
  const DEFAULT_FORMATS = ['avif', 'webp'];
@@ -68,7 +68,7 @@ export async function imageShortcode(options = {}) {
68
68
 
69
69
  // --- Validation and normalization ---
70
70
 
71
- if (!src) throw new Error(`imageShortcode: src is required (received ${JSON.stringify(src)})`);
71
+ if (!src) throw new Error(`[baseline/image-shortcode] src is required (received ${JSON.stringify(src)})`);
72
72
  if (alt == null) {
73
73
  log.warn('alt is required (use empty string for decorative images)');
74
74
  }
@@ -106,7 +106,7 @@ export async function imageShortcode(options = {}) {
106
106
  });
107
107
  } catch (error) {
108
108
  if (process.env.ELEVENTY_RUN_MODE === 'serve') {
109
- log.warn(`transformOnRequest failed for ${src}, retrying.\n > ${error?.message || error}`);
109
+ log.warn(`transformOnRequest failed for ${src}, retrying. ${error?.message || error}`);
110
110
  metadata = await Image(resolvedSrc, imageOptions);
111
111
  } else {
112
112
  throw error;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Surface barrel
3
+ *
4
+ * Single entry point for everything Baseline registers against Eleventy that
5
+ * user templates can reach: filters, global functions, shortcodes.
6
+ */
7
+
8
+ import { registerDateGlobal } from './global-date-function.js';
9
+
10
+ // --- Filters ---
11
+ export { markdownFilter } from '../markdown/markdownify.js';
12
+ export { relatedPostsFilter } from './filters/related-posts.js';
13
+ export { isStringFilter } from './filters/isString.js';
14
+
15
+ // --- Shortcodes ---
16
+ export { imageShortcode } from './image-shortcode.js';
17
+
18
+ // --- Global functions (aggregator) ---
19
+ /** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
20
+ export function registerGlobals(eleventyConfig) {
21
+ registerDateGlobal(eleventyConfig);
22
+ }
package/core/types.js CHANGED
@@ -43,7 +43,7 @@
43
43
  * Head module options.
44
44
  *
45
45
  * @property {{ esbuild?: { minify?: boolean, target?: string } }} [assets]
46
- * Assets module options. The esbuild slice is permissive any esbuild
46
+ * Assets module options. The esbuild slice is permissive: any esbuild
47
47
  * option is accepted; only `minify` and `target` are typed.
48
48
  */
49
49
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Helper function to add trailing slash to a path
3
+ * @param {string} path
4
+ * @returns {string}
5
+ */
6
+ export function addTrailingSlash(path) {
7
+ if (path.slice(-1) === '/') {
8
+ return path;
9
+ }
10
+ return path + '/';
11
+ }
@@ -0,0 +1,13 @@
1
+ import { TemplatePath } from '@11ty/eleventy-utils';
2
+
3
+ /**
4
+ * Normalise a directory path to a `./`-prefixed form, defaulting empty/missing
5
+ * input to the current directory. Thin wrapper over
6
+ * `TemplatePath.addLeadingDotSlash` that bakes in the empty-string fallback.
7
+ *
8
+ * @param {string | undefined} dir
9
+ * @returns {string}
10
+ */
11
+ export function ensureDotSlashDir(dir) {
12
+ return TemplatePath.addLeadingDotSlash(dir || './');
13
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Normalize language input to an object map.
3
+ * Accepts an array of language codes or an object keyed by language code.
4
+ * Returns undefined if input is invalid or empty.
5
+ *
6
+ * @param {Object} settings - Options object containing languages.
7
+ * @param {import('../logging/index.js').BaselineLogger} [logger] - Logger for dropped-entry notice.
8
+ * @returns {Record<string, Object>|undefined} Normalized language map, or undefined.
9
+ */
10
+ export function normalizeLanguages(settings, logger) {
11
+ const normalizedLanguages = Array.isArray(settings.languages)
12
+ ? Object.fromEntries(
13
+ settings.languages
14
+ .filter((lang) => typeof lang === 'string' && lang.trim())
15
+ .map((lang) => [lang.toLowerCase().trim(), {}])
16
+ )
17
+ : settings.languages && typeof settings.languages === 'object'
18
+ ? settings.languages
19
+ : undefined;
20
+
21
+ if (logger && Array.isArray(settings.languages)) {
22
+ const normalizedCount = normalizedLanguages ? Object.keys(normalizedLanguages).length : 0;
23
+ if (normalizedCount !== settings.languages.length) {
24
+ logger.info('Some languages entries were invalid and were dropped.');
25
+ }
26
+ }
27
+ return normalizedLanguages;
28
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolve a field with a page → site → fallback precedence chain.
3
+ *
4
+ * @param {{ pageValue?: any, siteValue?: any, fallbackValue?: any, isHome?: boolean }} args
5
+ * @returns {any}
6
+ */
7
+ export function resolveField({ pageValue, siteValue, fallbackValue }) {
8
+ return pageValue ?? siteValue ?? fallbackValue;
9
+ }
@@ -0,0 +1,20 @@
1
+ import { TemplatePath } from '@11ty/eleventy-utils';
2
+ import { addTrailingSlash } from './add-trailing-slash.js';
3
+
4
+ /**
5
+ * Resolve a subdirectory under input and output.
6
+ * Joins inputDir/outputDir with rawDir, normalises, and adds trailing slashes.
7
+ * @param {string} inputDir - The input directory (e.g., "./src/").
8
+ * @param {string} outputDir - The output directory (e.g., "./dist/").
9
+ * @param {string} rawDir - Raw subdirectory value (e.g., "assets", "static").
10
+ * @returns {{input: string, output: string}}
11
+ */
12
+ export function resolveSubdir(inputDir, outputDir, rawDir) {
13
+ const joinedInput = TemplatePath.join(inputDir, rawDir || '');
14
+ const joinedOutput = TemplatePath.join(outputDir, rawDir || '');
15
+
16
+ return {
17
+ input: addTrailingSlash(TemplatePath.standardizeFilePath(joinedInput)),
18
+ output: addTrailingSlash(TemplatePath.standardizeFilePath(joinedOutput))
19
+ };
20
+ }
@@ -0,0 +1,15 @@
1
+ import slugifyLib from 'slugify';
2
+
3
+ /**
4
+ * Slugify a string into a wikilink-friendly key.
5
+ * Lowercases, strips diacritics, replaces non-alphanumerics with hyphens,
6
+ * trims leading/trailing hyphens. Returns undefined for empty input.
7
+ *
8
+ * @param {string|null|undefined} input
9
+ * @returns {string|undefined}
10
+ */
11
+ export function slugify(input) {
12
+ if (input == null) return;
13
+ const slug = slugifyLib(String(input), { lower: true, strict: true, trim: true });
14
+ return slug || undefined;
15
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Deduplicate an array by a key (string property name or selector function).
3
+ * Items without a derivable key are kept via their JSON-stringified shape.
4
+ *
5
+ * @template T
6
+ * @param {T[]} arr
7
+ * @param {string | ((item: T) => string | undefined)} keyFn
8
+ * @returns {T[]}
9
+ */
10
+ export const uniqueBy = (arr, keyFn) =>
11
+ Object.values(
12
+ (arr ?? []).reduce((acc, item) => {
13
+ if (!item) return acc;
14
+
15
+ const id = typeof keyFn === 'function' ? keyFn(item) : item?.[keyFn];
16
+
17
+ if (!id) {
18
+ acc[JSON.stringify(item)] = item;
19
+ return acc;
20
+ }
21
+
22
+ acc[id] = item;
23
+ return acc;
24
+ }, {})
25
+ );
@@ -1,8 +1,11 @@
1
- import { TemplatePath } from '@11ty/eleventy-utils';
2
- import { resolveSubdir } from './utils/helpers.js';
3
- import { createLogger } from './logging.js';
1
+ import { ensureDotSlashDir } from './utils/ensure-dot-slash-dir.js';
2
+ import { resolveSubdir } from './utils/resolve-subdir.js';
3
+ import { createLogger } from './logging/index.js';
4
4
  import { getScope, addScopeListener, setEntry } from './registry.js';
5
5
 
6
+ const SCOPE_NAME = 'core:virtual-dir';
7
+ const LOG_NAME = 'virtual-dir';
8
+
6
9
  /**
7
10
  * Virtual directories (runtime substrate)
8
11
  *
@@ -40,8 +43,6 @@ import { getScope, addScopeListener, setEntry } from './registry.js';
40
43
  * live { input, output } cache → consumers
41
44
  */
42
45
 
43
- const SCOPE_NAME = 'core:virtual-dir';
44
-
45
46
  /**
46
47
  * Register a virtual directory on eleventyConfig.directories.
47
48
  *
@@ -56,10 +57,10 @@ const SCOPE_NAME = 'core:virtual-dir';
56
57
  */
57
58
  export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
58
59
  if (!key) {
59
- throw new Error('registerVirtualDir: `name` is required');
60
+ throw new Error('[baseline/virtual-dir] `name` is required');
60
61
  }
61
62
 
62
- const log = createLogger(SCOPE_NAME);
63
+ const log = createLogger(LOG_NAME);
63
64
  const scope = getScope(eleventyConfig, SCOPE_NAME);
64
65
  const rawDir = eleventyConfig.dir?.[key] || key;
65
66
  const rawOutputDir = outputDir ?? rawDir;
@@ -75,7 +76,7 @@ export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
75
76
  // shared listener below refreshes when Eleventy emits its final directories.
76
77
  const existing = Object.getOwnPropertyDescriptor(eleventyConfig.directories, key);
77
78
  if (existing && existing.configurable === false) {
78
- log.info(`directories[${key}] already defined; skipping`);
79
+ log.info(`directories.${key} already defined, skipping`);
79
80
  } else {
80
81
  Object.defineProperty(eleventyConfig.directories, key, {
81
82
  get() {
@@ -101,8 +102,8 @@ export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
101
102
  }
102
103
 
103
104
  function syncCache(cache, dirs, rawDir, rawOutputDir) {
104
- const inputDir = TemplatePath.addLeadingDotSlash(dirs.input || './');
105
- const outputDir = TemplatePath.addLeadingDotSlash(dirs.output || './');
105
+ const inputDir = ensureDotSlashDir(dirs.input);
106
+ const outputDir = ensureDotSlashDir(dirs.output);
106
107
 
107
108
  // resolveSubdir symmetrically resolves against input and output; call twice
108
109
  // so input and output subdirs can differ (e.g. `public` copies to root).