@cloudcannon/editable-regions 0.0.16 → 0.0.18
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/helpers/cloudcannon.mjs +7 -22
- package/integrations/astro/astro-integration.mjs +1 -4
- package/integrations/astro/index.mjs +15 -41
- package/integrations/astro/modules/assets.js +1 -4
- package/integrations/astro/modules/content.js +2 -11
- package/integrations/astro/react-renderer.mjs +4 -5
- package/integrations/eleventy/browser/collect-config.mjs +188 -0
- package/integrations/eleventy/browser/index.mjs +9 -0
- package/integrations/eleventy/browser/liquid-builtins.mjs +306 -0
- package/integrations/eleventy/browser/liquid-render.mjs +216 -0
- package/integrations/eleventy/index.cjs +1 -0
- package/integrations/eleventy/index.mjs +582 -0
- package/integrations/liquid/README.md +588 -0
- package/integrations/liquid/errors.mjs +39 -0
- package/integrations/liquid/fs.mjs +25 -74
- package/integrations/liquid/globals.mjs +209 -0
- package/integrations/liquid/include-with-tag.mjs +84 -0
- package/integrations/liquid/index.mjs +163 -170
- package/integrations/liquid/logger.mjs +15 -78
- package/integrations/liquid/page-map.mjs +32 -0
- package/integrations/liquid/shortcodes.mjs +62 -99
- package/integrations/react.mjs +5 -9
- package/nodes/editable-component.ts +6 -6
- package/package.json +19 -8
- package/types/eleventy.d.cts +20 -0
- package/types/eleventy.d.ts +81 -0
- package/types/liquid.d.ts +15 -12
- package/integrations/eleventy.mjs +0 -294
- package/integrations/liquid/11ty-filters.mjs +0 -69
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { builtinModules, createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import esbuild from "esbuild";
|
|
5
|
+
import { createIncludeWithTag } from "../liquid/include-with-tag.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import("../../types/eleventy").LiquidOptions} LiquidOptions
|
|
9
|
+
* @typedef {import("../../types/eleventy").PluginOptions} PluginOptions
|
|
10
|
+
* @typedef {import("../../types/eleventy").NormalizedPluginOptions} NormalizedPluginOptions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} EleventyDirectories
|
|
15
|
+
* @property {string} input
|
|
16
|
+
* @property {string} includes - Normalized, relative to project root
|
|
17
|
+
* @property {string} data
|
|
18
|
+
* @property {string} output
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Payload Eleventy passes to `eleventy.after`. `directories` is the 3.x shape;
|
|
23
|
+
* `dir` the older fallback still passed in 3.x.
|
|
24
|
+
*
|
|
25
|
+
* @typedef {Object} EleventyEventPayload
|
|
26
|
+
* @property {EleventyDirectories} [directories]
|
|
27
|
+
* @property {EleventyDirectories} [dir]
|
|
28
|
+
* @property {Array<{inputPath?: string, outputPath?: string, url?: string}>} [results]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {(liquidEngine: import("liquidjs").Liquid) => { parse: (...args: any[]) => void, render: (...args: any[]) => unknown }} LiquidTagFactory
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {"eleventy.before" | "eleventy.after" | "eleventy.beforeWatch" | "eleventy.beforeConfig"} EleventyEventName
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The subset of Eleventy's (untyped) user config we touch.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {Object} EleventyConfig
|
|
43
|
+
* @property {(name: string, factory: LiquidTagFactory) => void} addLiquidTag
|
|
44
|
+
* @property {(event: EleventyEventName, handler: (payload: EleventyEventPayload) => Promise<void> | void) => void} on
|
|
45
|
+
* @property {EleventyDirectories} dir
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Eleventy plugin for CloudCannon editable regions. Registers Liquid tags
|
|
50
|
+
* and builds the live-editing client bundle.
|
|
51
|
+
*
|
|
52
|
+
* @param {EleventyConfig} eleventyConfig
|
|
53
|
+
* @param {PluginOptions} [pluginOptions]
|
|
54
|
+
*/
|
|
55
|
+
export default function editableRegionsPlugin(eleventyConfig, pluginOptions) {
|
|
56
|
+
const options = normalizePluginOptions(pluginOptions);
|
|
57
|
+
|
|
58
|
+
// No supported languages enabled — nothing to register or bundle.
|
|
59
|
+
if (!options.liquid) return;
|
|
60
|
+
const liquidOptions = options.liquid;
|
|
61
|
+
|
|
62
|
+
eleventyConfig.addLiquidTag("includeWith", createIncludeWithTag);
|
|
63
|
+
|
|
64
|
+
eleventyConfig.on("eleventy.after", async ({ directories, dir, results }) => {
|
|
65
|
+
// 3.x `directories`, legacy `dir`, then a closure fallback.
|
|
66
|
+
const dirs = directories ?? dir ?? eleventyConfig.dir;
|
|
67
|
+
|
|
68
|
+
const rawExtensions = liquidOptions.extensions ?? [".liquid", ".html"];
|
|
69
|
+
const normalizedExtensions = rawExtensions.map((ext) =>
|
|
70
|
+
ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const liveEditingSource = await generateLiveEditingSource(
|
|
74
|
+
options,
|
|
75
|
+
dirs,
|
|
76
|
+
normalizedExtensions,
|
|
77
|
+
results,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// esbuild only matches the final extension, so .bookshop.liquid -> .liquid
|
|
81
|
+
/** @type {Record<string, import('esbuild').Loader>} */
|
|
82
|
+
const loader = {};
|
|
83
|
+
for (const ext of normalizedExtensions) {
|
|
84
|
+
loader[ext.slice(ext.lastIndexOf("."))] = "text";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await esbuild.build({
|
|
88
|
+
stdin: {
|
|
89
|
+
contents: liveEditingSource,
|
|
90
|
+
resolveDir: process.cwd(),
|
|
91
|
+
},
|
|
92
|
+
loader,
|
|
93
|
+
bundle: true,
|
|
94
|
+
platform: "browser",
|
|
95
|
+
// The bundle imports the user's real Eleventy config (see
|
|
96
|
+
// `emitConfigMirror`), dragging in Node/build-time imports — stub them.
|
|
97
|
+
plugins: [createBrowserStubPlugin(liquidOptions.browserStub)],
|
|
98
|
+
outfile: options.output ?? `${dirs.output}/register-components.js`,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Liquid is enabled implicitly; only `liquid: false` opts out.
|
|
105
|
+
*
|
|
106
|
+
* @param {PluginOptions | undefined} pluginOptions
|
|
107
|
+
* @returns {NormalizedPluginOptions}
|
|
108
|
+
*/
|
|
109
|
+
function normalizePluginOptions(pluginOptions) {
|
|
110
|
+
const opts = pluginOptions ?? {};
|
|
111
|
+
return {
|
|
112
|
+
...opts,
|
|
113
|
+
liquid: normalizeLiquidOption(opts.liquid),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolves the `liquid` option to either an options object (enabled) or
|
|
119
|
+
* `false` (disabled). Liquid is on by default, so anything but an explicit
|
|
120
|
+
* `false` enables it; `true` and an omitted value mean "on with defaults".
|
|
121
|
+
*
|
|
122
|
+
* @param {LiquidOptions | boolean | undefined} liquid
|
|
123
|
+
* @returns {LiquidOptions | false}
|
|
124
|
+
*/
|
|
125
|
+
function normalizeLiquidOption(liquid) {
|
|
126
|
+
// Explicit opt-out is the only way to disable Liquid.
|
|
127
|
+
if (liquid === false) return false;
|
|
128
|
+
|
|
129
|
+
// An options object is used as-is.
|
|
130
|
+
if (liquid && typeof liquid === "object") return liquid;
|
|
131
|
+
|
|
132
|
+
// `true`, `undefined`, or `null` → enabled with default options.
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Specifiers stubbed by exact match (subpaths bundle normally — notably the
|
|
138
|
+
* Node plugin's own `/browser` and `/liquid` runtime). `@11ty/eleventy` is
|
|
139
|
+
* matched separately in `shouldStub` because its subpaths must be stubbed too.
|
|
140
|
+
*/
|
|
141
|
+
const ALWAYS_STUBBED = ["@cloudcannon/editable-regions/eleventy"];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* esbuild plugin resolving Node built-ins and build-time-only packages to a
|
|
145
|
+
* Proxy that survives `import` and property access but throws when called or
|
|
146
|
+
* constructed — so the user's config bundles, and only a helper that actually
|
|
147
|
+
* invokes a Node API at render time fails.
|
|
148
|
+
*
|
|
149
|
+
* @param {string[]} [extraStubs] - Extra specifiers to stub
|
|
150
|
+
* (`pluginOptions.liquid.browserStub`), e.g. native deps like `sharp`.
|
|
151
|
+
* @returns {import('esbuild').Plugin}
|
|
152
|
+
*/
|
|
153
|
+
function createBrowserStubPlugin(extraStubs = []) {
|
|
154
|
+
const nodeBuiltins = new Set([
|
|
155
|
+
...builtinModules,
|
|
156
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
157
|
+
]);
|
|
158
|
+
const exactStubs = new Set([...ALWAYS_STUBBED, ...extraStubs]);
|
|
159
|
+
|
|
160
|
+
const shouldStub = (/** @type {string} */ id) => {
|
|
161
|
+
if (nodeBuiltins.has(id)) return true;
|
|
162
|
+
|
|
163
|
+
if (id === "@11ty/eleventy" || id.startsWith("@11ty/eleventy/")) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return exactStubs.has(id);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
name: "editable-regions-browser-stub",
|
|
172
|
+
setup(build) {
|
|
173
|
+
build.onResolve({ filter: /.*/ }, (args) =>
|
|
174
|
+
shouldStub(args.path)
|
|
175
|
+
? { path: args.path, namespace: "er-stub" }
|
|
176
|
+
: null,
|
|
177
|
+
);
|
|
178
|
+
build.onLoad({ filter: /.*/, namespace: "er-stub" }, () => ({
|
|
179
|
+
contents: `
|
|
180
|
+
const handler = {
|
|
181
|
+
get: () => new Proxy(function () {}, handler),
|
|
182
|
+
apply: () => {
|
|
183
|
+
throw new Error("editable-regions: a Node/build-time API was called in the browser live-editing bundle. Provide a browser-friendly override via pluginOptions.liquid.<kind>.");
|
|
184
|
+
},
|
|
185
|
+
construct: () => {
|
|
186
|
+
throw new Error("editable-regions: a Node/build-time API was constructed in the browser live-editing bundle. Provide a browser-friendly override via pluginOptions.liquid.<kind>.");
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
module.exports = new Proxy(function () {}, handler);
|
|
190
|
+
`,
|
|
191
|
+
loader: "js",
|
|
192
|
+
}));
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolves the user's Eleventy config path (11ty doesn't expose it): an
|
|
199
|
+
* explicit `configPath`, else the first default config filename in the root.
|
|
200
|
+
*
|
|
201
|
+
* @param {LiquidOptions} liquidOptions
|
|
202
|
+
* @returns {string | null} Absolute path, or `null` if none found
|
|
203
|
+
*/
|
|
204
|
+
function resolveEleventyConfigPath(liquidOptions) {
|
|
205
|
+
if (liquidOptions.configPath) {
|
|
206
|
+
return path.resolve(process.cwd(), liquidOptions.configPath);
|
|
207
|
+
}
|
|
208
|
+
// Matches 11ty's default resolution order (TemplateConfig.js).
|
|
209
|
+
const defaults = [
|
|
210
|
+
".eleventy.js",
|
|
211
|
+
"eleventy.config.js",
|
|
212
|
+
"eleventy.config.mjs",
|
|
213
|
+
"eleventy.config.cjs",
|
|
214
|
+
];
|
|
215
|
+
for (const name of defaults) {
|
|
216
|
+
const candidate = path.resolve(process.cwd(), name);
|
|
217
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Emits the import of the user's Eleventy config plus the call that replays it
|
|
224
|
+
* in the browser to auto-mirror its helpers (see
|
|
225
|
+
* `collectAndRegisterEleventyHelpers`). Passes the per-kind override names to
|
|
226
|
+
* skip; those are registered separately by `emitImportRegistrations` so the
|
|
227
|
+
* override wins.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} configPath - Absolute path to the Eleventy config
|
|
230
|
+
* @param {LiquidOptions | undefined} liquidOptions
|
|
231
|
+
* @returns {string} JS source
|
|
232
|
+
*/
|
|
233
|
+
function emitConfigMirror(configPath, liquidOptions) {
|
|
234
|
+
const skip = {
|
|
235
|
+
filters: Object.keys(liquidOptions?.filters ?? {}),
|
|
236
|
+
shortcodes: Object.keys(liquidOptions?.shortcodes ?? {}),
|
|
237
|
+
pairedShortcodes: Object.keys(liquidOptions?.pairedShortcodes ?? {}),
|
|
238
|
+
tags: Object.keys(liquidOptions?.tags ?? {}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
`\nimport userEleventyConfig from ${JSON.stringify(configPath)};\n` +
|
|
243
|
+
`collectAndRegisterEleventyHelpers(userEleventyConfig, ${JSON.stringify({ skip })});\n`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Builds the JS source for the live-editing client bundle: imports and
|
|
249
|
+
* `register*` calls for components, filters, shortcodes, and tags.
|
|
250
|
+
*
|
|
251
|
+
* @param {NormalizedPluginOptions} options
|
|
252
|
+
* @param {EleventyDirectories} directories
|
|
253
|
+
* @param {string[]} normalizedExtensions - Lowercase, leading-dot
|
|
254
|
+
* @param {Array<{inputPath?: string, outputPath?: string, url?: string}> | undefined} results - From `eleventy.after`
|
|
255
|
+
* @returns {Promise<string>}
|
|
256
|
+
*/
|
|
257
|
+
async function generateLiveEditingSource(
|
|
258
|
+
options,
|
|
259
|
+
directories,
|
|
260
|
+
normalizedExtensions,
|
|
261
|
+
results,
|
|
262
|
+
) {
|
|
263
|
+
let source = "";
|
|
264
|
+
|
|
265
|
+
if (options.liquid) {
|
|
266
|
+
const liquidOptions = options.liquid;
|
|
267
|
+
// `input` alongside `includes` so `{% include %}` reaches sibling files.
|
|
268
|
+
const componentDirs = liquidOptions.componentDirs ?? [
|
|
269
|
+
directories.includes,
|
|
270
|
+
directories.input,
|
|
271
|
+
];
|
|
272
|
+
const ignoreDirectories = liquidOptions.ignoreDirectories ?? [
|
|
273
|
+
directories.output,
|
|
274
|
+
"node_modules",
|
|
275
|
+
];
|
|
276
|
+
const normalizedIgnoreDirs = ignoreDirectories.map((dir) =>
|
|
277
|
+
dir.toLowerCase(),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
source += `
|
|
281
|
+
import { createSharedLiquidEngine, registerLiquidComponent, registerFilter, registerShortcode, registerPairedShortcode, registerCustomTag, registerGlobals, registerEleventyData, registerPkg, registerPageMap, initComponentProxy, setVerbose } from '@cloudcannon/editable-regions/liquid';
|
|
282
|
+
import { registerEleventyBuiltins, collectAndRegisterEleventyHelpers } from '@cloudcannon/editable-regions/eleventy/browser';
|
|
283
|
+
|
|
284
|
+
setVerbose(${Boolean(options.verbose)});
|
|
285
|
+
|
|
286
|
+
const liquidEngine = createSharedLiquidEngine({
|
|
287
|
+
root: ${JSON.stringify(componentDirs)},
|
|
288
|
+
extname: ".liquid",
|
|
289
|
+
strictFilters: true,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Wires on Eleventy's built-in filters/shortcodes (browser ports)
|
|
293
|
+
// and RenderPlugin shims onto the host-agnostic engine.
|
|
294
|
+
registerEleventyBuiltins(liquidEngine);
|
|
295
|
+
|
|
296
|
+
window.cc_liquid_files = {};
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
// User-supplied globals, embedded as a literal so editor-rendered
|
|
300
|
+
// templates read the same values the build exposes.
|
|
301
|
+
if (options.globals && Object.keys(options.globals).length > 0) {
|
|
302
|
+
source += `\nregisterGlobals(${JSON.stringify(options.globals)});\n`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Static `eleventy` global so templates branching on `eleventy.version` /
|
|
306
|
+
// `eleventy.env.runMode` see something sensible.
|
|
307
|
+
const eleventyData = buildEleventyData(directories);
|
|
308
|
+
source += `\nregisterEleventyData(${JSON.stringify(eleventyData)});\n`;
|
|
309
|
+
|
|
310
|
+
// 11ty exposes the project's package.json as the `pkg` global by default.
|
|
311
|
+
const pkg = buildPkg();
|
|
312
|
+
if (pkg) {
|
|
313
|
+
source += `\nregisterPkg(${JSON.stringify(pkg)});\n`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Build-time page map from 11ty's `results`, so the page/collections
|
|
317
|
+
// proxies and `inputPathToUrl` resolve computed/templated permalinks.
|
|
318
|
+
const pageMap = buildPageMap(results);
|
|
319
|
+
if (Object.keys(pageMap).length > 0) {
|
|
320
|
+
source += `\nregisterPageMap(${JSON.stringify(pageMap)});\n`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Pre-populate `window.cc_liquid_files` with every includable template,
|
|
324
|
+
// the map LiquidJS's in-memory fs (`liquid/fs.mjs`) resolves against.
|
|
325
|
+
const allLiquidFiles = await findAllLiquidFiles(
|
|
326
|
+
componentDirs,
|
|
327
|
+
normalizedExtensions,
|
|
328
|
+
normalizedIgnoreDirs,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
for (const [i, filePath] of allLiquidFiles.entries()) {
|
|
332
|
+
const id = `liquidFile_${i}`;
|
|
333
|
+
source += `import ${id} from "./${filePath}";
|
|
334
|
+
|
|
335
|
+
window.cc_liquid_files["${filePath}"] = ${id};
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Auto-mirror the user's config helpers by importing and replaying the
|
|
340
|
+
// real config in the browser. See `emitConfigMirror`.
|
|
341
|
+
const configPath = resolveEleventyConfigPath(liquidOptions);
|
|
342
|
+
if (configPath) {
|
|
343
|
+
source += emitConfigMirror(configPath, liquidOptions);
|
|
344
|
+
} else {
|
|
345
|
+
console.warn(
|
|
346
|
+
"[editable-regions] Could not locate an Eleventy config file to " +
|
|
347
|
+
"auto-mirror helpers from. Set `pluginOptions.liquid.configPath` " +
|
|
348
|
+
"if your config isn't at a default location. Filters/shortcodes " +
|
|
349
|
+
"defined in the config won't be available in live editing " +
|
|
350
|
+
"(overrides still work).",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Register browser-side overrides and pinned components. Override names
|
|
355
|
+
// are excluded from the mirror, so each is its name's sole registration.
|
|
356
|
+
source += emitImportRegistrations(liquidOptions);
|
|
357
|
+
|
|
358
|
+
source += `
|
|
359
|
+
initComponentProxy();
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
return source;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds the static `eleventy` global, mirroring the browser-applicable parts
|
|
367
|
+
* of https://www.11ty.dev/docs/data-eleventy-supplied/. `env.config`/`env.root`
|
|
368
|
+
* (absolute paths) and `serverless` are omitted; `env.runMode`/`env.source` are
|
|
369
|
+
* hardcoded to `"serve"`/`"cli"` so branching templates take a sane path.
|
|
370
|
+
*
|
|
371
|
+
* @param {EleventyDirectories} directories
|
|
372
|
+
*/
|
|
373
|
+
function buildEleventyData(directories) {
|
|
374
|
+
const version = readEleventyVersion();
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
version,
|
|
378
|
+
generator: `Eleventy v${version}`,
|
|
379
|
+
env: {
|
|
380
|
+
runMode: "serve",
|
|
381
|
+
source: "cli",
|
|
382
|
+
},
|
|
383
|
+
directories: {
|
|
384
|
+
input: directories.input,
|
|
385
|
+
includes: directories.includes,
|
|
386
|
+
data: directories.data,
|
|
387
|
+
output: directories.output,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Reads the project `package.json` for the `pkg` global. Returns `null` on
|
|
394
|
+
* missing/malformed input so the bundle still builds.
|
|
395
|
+
*/
|
|
396
|
+
function buildPkg() {
|
|
397
|
+
try {
|
|
398
|
+
const contents = fs.readFileSync(
|
|
399
|
+
path.join(process.cwd(), "package.json"),
|
|
400
|
+
"utf8",
|
|
401
|
+
);
|
|
402
|
+
return JSON.parse(contents);
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Reads the installed Eleventy version, or `"unknown"` if unresolvable.
|
|
410
|
+
* `@11ty/eleventy` doesn't export `./package.json`, so we resolve its main
|
|
411
|
+
* entry and walk up to the package root instead.
|
|
412
|
+
*/
|
|
413
|
+
function readEleventyVersion() {
|
|
414
|
+
try {
|
|
415
|
+
const require = createRequire(import.meta.url);
|
|
416
|
+
const entryPath = require.resolve("@11ty/eleventy");
|
|
417
|
+
let dir = path.dirname(entryPath);
|
|
418
|
+
while (true) {
|
|
419
|
+
const pkgPath = path.join(dir, "package.json");
|
|
420
|
+
if (fs.existsSync(pkgPath)) {
|
|
421
|
+
/** @type {{name?: string, version?: string}} */
|
|
422
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
423
|
+
if (pkg.name === "@11ty/eleventy" && pkg.version) return pkg.version;
|
|
424
|
+
}
|
|
425
|
+
const parent = path.dirname(dir);
|
|
426
|
+
if (parent === dir) break;
|
|
427
|
+
dir = parent;
|
|
428
|
+
}
|
|
429
|
+
return "unknown";
|
|
430
|
+
} catch {
|
|
431
|
+
return "unknown";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Compacts 11ty's `results` into the page map, keyed by normalized input path
|
|
437
|
+
* (matching `normalizeInputPath` in `liquid/page-map.mjs`). Pagination yields
|
|
438
|
+
* duplicate `inputPath`s — we keep the first, since cursors aren't modelled in
|
|
439
|
+
* the editor. Returns `{}` if `results` is absent or malformed.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array<{inputPath?: string, outputPath?: string, url?: string}> | undefined} results
|
|
442
|
+
*/
|
|
443
|
+
function buildPageMap(results) {
|
|
444
|
+
if (!Array.isArray(results)) return {};
|
|
445
|
+
|
|
446
|
+
/** @type {Record<string, { url?: string, outputPath?: string }>} */
|
|
447
|
+
const map = {};
|
|
448
|
+
|
|
449
|
+
for (const entry of results) {
|
|
450
|
+
if (!entry || typeof entry.inputPath !== "string") continue;
|
|
451
|
+
|
|
452
|
+
const key = entry.inputPath.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
453
|
+
if (!key || key in map) continue;
|
|
454
|
+
|
|
455
|
+
map[key] = {
|
|
456
|
+
url: typeof entry.url === "string" ? entry.url : undefined,
|
|
457
|
+
outputPath:
|
|
458
|
+
typeof entry.outputPath === "string" ? entry.outputPath : undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return map;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {string[]} componentDirs
|
|
467
|
+
* @param {string[]} extensions
|
|
468
|
+
* @param {string[]} ignoreDirectories
|
|
469
|
+
*/
|
|
470
|
+
async function findAllLiquidFiles(
|
|
471
|
+
componentDirs,
|
|
472
|
+
extensions,
|
|
473
|
+
ignoreDirectories,
|
|
474
|
+
) {
|
|
475
|
+
const allFiles = [];
|
|
476
|
+
|
|
477
|
+
for (const dir of componentDirs) {
|
|
478
|
+
const files = await findFilesInDirectory({
|
|
479
|
+
directory: dir,
|
|
480
|
+
extensions,
|
|
481
|
+
ignoreDirectories,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
allFiles.push(...files);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return allFiles;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @param {Object} options
|
|
492
|
+
* @param {string} options.directory
|
|
493
|
+
* @param {string[]} [options.extensions]
|
|
494
|
+
* @param {string[]} [options.ignoreDirectories]
|
|
495
|
+
* @returns {Promise<string[]>}
|
|
496
|
+
*/
|
|
497
|
+
async function findFilesInDirectory({
|
|
498
|
+
directory,
|
|
499
|
+
extensions = [".html", ".liquid"],
|
|
500
|
+
ignoreDirectories = [],
|
|
501
|
+
}) {
|
|
502
|
+
const files = [];
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const entries = await fs.promises.readdir(directory, {
|
|
506
|
+
withFileTypes: true,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
for (const entry of entries) {
|
|
510
|
+
const fullPath = path.join(directory, entry.name);
|
|
511
|
+
|
|
512
|
+
if (entry.isDirectory()) {
|
|
513
|
+
if (ignoreDirectories.includes(entry.name.toLowerCase())) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const subFiles = await findFilesInDirectory({
|
|
518
|
+
directory: fullPath,
|
|
519
|
+
extensions,
|
|
520
|
+
ignoreDirectories,
|
|
521
|
+
});
|
|
522
|
+
files.push(...subFiles);
|
|
523
|
+
} else if (entry.isFile()) {
|
|
524
|
+
// Handles compound extensions too (e.g. `.bookshop.liquid`).
|
|
525
|
+
const filenameLower = entry.name.toLowerCase();
|
|
526
|
+
const hasValidExtension = extensions.some((ext) =>
|
|
527
|
+
filenameLower.endsWith(ext),
|
|
528
|
+
);
|
|
529
|
+
if (hasValidExtension) {
|
|
530
|
+
files.push(fullPath);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch (error) {
|
|
535
|
+
console.error("ERROR reading directory:", directory, error);
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return files;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Maps each `{ name: modulePath }` `pluginOptions.liquid` field to its runtime
|
|
544
|
+
* registration function. `components` pins a module ahead of the
|
|
545
|
+
* filesystem-resolution proxy via the same import-and-register shape.
|
|
546
|
+
*/
|
|
547
|
+
const IMPORT_REGISTER_FNS = {
|
|
548
|
+
filters: "registerFilter",
|
|
549
|
+
shortcodes: "registerShortcode",
|
|
550
|
+
pairedShortcodes: "registerPairedShortcode",
|
|
551
|
+
tags: "registerCustomTag",
|
|
552
|
+
components: "registerLiquidComponent",
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Emits an `import` + register-call pair for every `{ name: modulePath }` entry
|
|
557
|
+
* across the `IMPORT_REGISTER_FNS` maps, e.g.:
|
|
558
|
+
*
|
|
559
|
+
* import filters_0 from "./path/to/file";
|
|
560
|
+
* registerFilter("name", filters_0);
|
|
561
|
+
*
|
|
562
|
+
* @param {LiquidOptions | undefined} liquidOptions
|
|
563
|
+
* @returns {string} JS source
|
|
564
|
+
*/
|
|
565
|
+
function emitImportRegistrations(liquidOptions) {
|
|
566
|
+
let out = "";
|
|
567
|
+
|
|
568
|
+
for (const optionKey of /** @type {Array<keyof typeof IMPORT_REGISTER_FNS>} */ (
|
|
569
|
+
Object.keys(IMPORT_REGISTER_FNS)
|
|
570
|
+
)) {
|
|
571
|
+
const registerFn = IMPORT_REGISTER_FNS[optionKey];
|
|
572
|
+
|
|
573
|
+
for (const [i, [name, file]] of Object.entries(
|
|
574
|
+
liquidOptions?.[optionKey] ?? {},
|
|
575
|
+
).entries()) {
|
|
576
|
+
const id = `${optionKey}_${i}`;
|
|
577
|
+
out += `\nimport ${id} from "./${file}";\n${registerFn}(${JSON.stringify(name)}, ${id});\n`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return out;
|
|
582
|
+
}
|