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