@cloudcannon/editable-regions 0.0.17 → 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/package.json +18 -7
- 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
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import esbuild from "esbuild";
|
|
4
|
-
import { createBindIncludeTag } from "./liquid/index.mjs";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {Object} ComponentRegistration
|
|
8
|
-
* @property {string} name - Component name
|
|
9
|
-
* @property {string} file - Path to component file
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {Object} LiquidOptions
|
|
14
|
-
* @property {string[]} [componentDirs] - Defaults to Eleventy's configured directories.includes
|
|
15
|
-
* @property {string[]} [extensions] - Defaults to [".liquid", ".html"]
|
|
16
|
-
* @property {string[]} [ignoreDirectories] - Directory names to skip (e.g., ["_drafts", "node_modules"])
|
|
17
|
-
* @property {ComponentRegistration[]} [components] - Registered components
|
|
18
|
-
* @property {ComponentRegistration[]} [filters] - Custom Liquid filters
|
|
19
|
-
* @property {ComponentRegistration[]} [shortcodes] - Custom shortcodes
|
|
20
|
-
* @property {ComponentRegistration[]} [pairedShortcodes] - Custom paired shortcodes
|
|
21
|
-
* @property {ComponentRegistration[]} [tags] - Custom tags
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {Object} PluginOptions
|
|
26
|
-
* @property {string} [output] - Output path for live-editing.js
|
|
27
|
-
* @property {boolean} [verbose] - Enable verbose browser logging
|
|
28
|
-
* @property {LiquidOptions} [liquid] - Liquid template options
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @typedef {Object} EleventyDirectories
|
|
33
|
-
* @property {string} input - Input directory
|
|
34
|
-
* @property {string} includes - Includes directory (normalized, relative to project root)
|
|
35
|
-
* @property {string} data - Data directory
|
|
36
|
-
* @property {string} output - Output directory
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {Object} EleventyConfig
|
|
41
|
-
* @property {function(string, function): void} addLiquidTag - Register a custom Liquid tag
|
|
42
|
-
* @property {function(string, function({ directories: EleventyDirectories }): Promise<void>): void} on - Register an event handler
|
|
43
|
-
* @property {EleventyDirectories} dir - Directory configuration
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Eleventy plugin for CloudCannon editable regions.
|
|
48
|
-
* Registers Liquid tags and builds live-editing client bundle.
|
|
49
|
-
*
|
|
50
|
-
* @param {EleventyConfig} eleventyConfig - Eleventy configuration object
|
|
51
|
-
* @param {PluginOptions} pluginOptions - Plugin configuration options
|
|
52
|
-
* @returns {void}
|
|
53
|
-
*/
|
|
54
|
-
export default function (eleventyConfig, pluginOptions) {
|
|
55
|
-
if (pluginOptions.liquid) {
|
|
56
|
-
eleventyConfig.addLiquidTag("bind_include", createBindIncludeTag);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
eleventyConfig.on("eleventy.before", async ({ directories }) => {
|
|
60
|
-
const liveEditingSource = createLiveEditingSource(
|
|
61
|
-
pluginOptions,
|
|
62
|
-
directories,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Build dynamic loader config from extensions
|
|
66
|
-
// esbuild only looks at the final extension, so .bookshop.liquid -> .liquid
|
|
67
|
-
/** @type {Record<string, import('esbuild').Loader>} */
|
|
68
|
-
const loader = {};
|
|
69
|
-
const extensions = pluginOptions.liquid?.extensions ?? [".liquid", ".html"];
|
|
70
|
-
extensions.forEach((ext) => {
|
|
71
|
-
const normalized = ext.startsWith(".") ? ext : `.${ext}`;
|
|
72
|
-
// Extract the final extension (e.g., ".bookshop.liquid" -> ".liquid")
|
|
73
|
-
const lastDotIndex = normalized.lastIndexOf(".");
|
|
74
|
-
loader[normalized.slice(lastDotIndex)] = "text";
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
await esbuild.build({
|
|
78
|
-
stdin: {
|
|
79
|
-
contents: await liveEditingSource,
|
|
80
|
-
resolveDir: process.cwd(),
|
|
81
|
-
},
|
|
82
|
-
loader,
|
|
83
|
-
bundle: true,
|
|
84
|
-
outfile: pluginOptions.output ?? `${directories.output}/live-editing.js`,
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Creates the JavaScript source code for the live-editing client bundle.
|
|
91
|
-
* Generates imports for components, filters, shortcodes, and tags.
|
|
92
|
-
*
|
|
93
|
-
* @param {PluginOptions} pluginOptions - Plugin configuration options
|
|
94
|
-
* @param {EleventyDirectories} directories - Eleventy directory configuration
|
|
95
|
-
* @returns {Promise<string>} Generated JavaScript source code
|
|
96
|
-
*/
|
|
97
|
-
const createLiveEditingSource = async (pluginOptions, directories) => {
|
|
98
|
-
let source = "";
|
|
99
|
-
|
|
100
|
-
if (pluginOptions.liquid) {
|
|
101
|
-
const componentDirs = pluginOptions.liquid.componentDirs ?? [
|
|
102
|
-
directories.includes,
|
|
103
|
-
directories.input,
|
|
104
|
-
];
|
|
105
|
-
const extensions = pluginOptions.liquid.extensions ?? [".liquid", ".html"];
|
|
106
|
-
const ignoreDirectories = pluginOptions.liquid.ignoreDirectories ?? [
|
|
107
|
-
directories.output,
|
|
108
|
-
"node_modules",
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
const normalizedExtensions = extensions.map((ext) =>
|
|
112
|
-
ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`,
|
|
113
|
-
);
|
|
114
|
-
const normalizedIgnoreDirs = ignoreDirectories.map((dir) =>
|
|
115
|
-
dir.toLowerCase(),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
source += `
|
|
119
|
-
import { createSharedLiquidEngine, registerLiquidComponent, registerCustomFilter, registerCustomShortcode, registerCustomPairedShortcode, registerCustomTag, setVerbose } from '@cloudcannon/editable-regions/liquid';
|
|
120
|
-
|
|
121
|
-
setVerbose(${Boolean(pluginOptions.verbose)});
|
|
122
|
-
|
|
123
|
-
// Configure the Liquid engine with component directories
|
|
124
|
-
createSharedLiquidEngine({
|
|
125
|
-
root: ${JSON.stringify(componentDirs)},
|
|
126
|
-
extname: ".liquid",
|
|
127
|
-
strictFilters: true,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
window.cc_files = {};
|
|
131
|
-
`;
|
|
132
|
-
|
|
133
|
-
// Add files we'll need to window.cc_files -
|
|
134
|
-
// Then in our liquid file system we can grab them from window.cc_files during readFile
|
|
135
|
-
let i = 0;
|
|
136
|
-
const allLiquidFiles = await findAllLiquidFiles(
|
|
137
|
-
componentDirs,
|
|
138
|
-
normalizedExtensions,
|
|
139
|
-
normalizedIgnoreDirs,
|
|
140
|
-
);
|
|
141
|
-
allLiquidFiles.forEach((path) => {
|
|
142
|
-
const id = `liquidFile_${i++}`;
|
|
143
|
-
source += `import ${id} from "./${path}";
|
|
144
|
-
|
|
145
|
-
window.cc_files["${path}"] = ${id};
|
|
146
|
-
`;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Register custom filters
|
|
150
|
-
const customFilters = pluginOptions.liquid?.filters;
|
|
151
|
-
if (customFilters?.length) {
|
|
152
|
-
let filterIdx = 0;
|
|
153
|
-
for (const { name, file } of customFilters) {
|
|
154
|
-
const filterName = `customFilter_${filterIdx++}`;
|
|
155
|
-
source += `
|
|
156
|
-
import ${filterName} from "./${file}";
|
|
157
|
-
registerCustomFilter("${name}", ${filterName});
|
|
158
|
-
`;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Register custom shortcodes
|
|
163
|
-
const customShortcodes = pluginOptions.liquid?.shortcodes;
|
|
164
|
-
if (customShortcodes?.length) {
|
|
165
|
-
let shortcodeIdx = 0;
|
|
166
|
-
for (const { name, file } of customShortcodes) {
|
|
167
|
-
const shortcodeName = `customShortcode_${shortcodeIdx++}`;
|
|
168
|
-
source += `
|
|
169
|
-
import ${shortcodeName} from "./${file}";
|
|
170
|
-
registerCustomShortcode("${name}", ${shortcodeName});
|
|
171
|
-
`;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Register custom paired shortcodes
|
|
176
|
-
const customPairedShortcodes = pluginOptions.liquid?.pairedShortcodes;
|
|
177
|
-
if (customPairedShortcodes?.length) {
|
|
178
|
-
let pairedIdx = 0;
|
|
179
|
-
for (const { name, file } of customPairedShortcodes) {
|
|
180
|
-
const pairedShortcodeName = `customPairedShortcode_${pairedIdx++}`;
|
|
181
|
-
source += `
|
|
182
|
-
import ${pairedShortcodeName} from "./${file}";
|
|
183
|
-
registerCustomPairedShortcode("${name}", ${pairedShortcodeName});
|
|
184
|
-
`;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Register custom tags
|
|
189
|
-
const tags = pluginOptions.liquid?.tags;
|
|
190
|
-
if (tags?.length) {
|
|
191
|
-
let tagIdx = 0;
|
|
192
|
-
for (const { name, file } of tags) {
|
|
193
|
-
const tagName = `tag_${tagIdx++}`;
|
|
194
|
-
source += `
|
|
195
|
-
import ${tagName} from "./${file}";
|
|
196
|
-
registerCustomTag("${name}", ${tagName});
|
|
197
|
-
`;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Register components
|
|
202
|
-
let componentIdx = 0;
|
|
203
|
-
pluginOptions.liquid?.components?.forEach(({ name, file }) => {
|
|
204
|
-
const componentName = `customComponent_${componentIdx++}`;
|
|
205
|
-
|
|
206
|
-
source += `
|
|
207
|
-
import ${componentName} from "./${file}";
|
|
208
|
-
registerLiquidComponent("${name}", ${componentName});
|
|
209
|
-
`;
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
return source;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Find all component files across multiple directories.
|
|
217
|
-
*
|
|
218
|
-
* @param {string[]} componentDirs - Directories to search
|
|
219
|
-
* @param {string[]} extensions - File extensions to match
|
|
220
|
-
* @param {string[]} ignoreDirectories - Directory names to skip
|
|
221
|
-
* @returns {Promise<string[]>} Array of file paths
|
|
222
|
-
*/
|
|
223
|
-
async function findAllLiquidFiles(
|
|
224
|
-
componentDirs,
|
|
225
|
-
extensions,
|
|
226
|
-
ignoreDirectories,
|
|
227
|
-
) {
|
|
228
|
-
const allFiles = [];
|
|
229
|
-
|
|
230
|
-
for (const dir of componentDirs) {
|
|
231
|
-
const files = await findFilesInDirectory({
|
|
232
|
-
directory: dir,
|
|
233
|
-
extensions,
|
|
234
|
-
ignoreDirectories,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
allFiles.push(...files);
|
|
238
|
-
}
|
|
239
|
-
return allFiles;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Recursively find all matching files in a single directory.
|
|
244
|
-
*
|
|
245
|
-
* @param {Object} options - Search options
|
|
246
|
-
* @param {string} options.directory - Directory to search
|
|
247
|
-
* @param {string[]} [options.extensions] - File extensions to match
|
|
248
|
-
* @param {string[]} [options.ignoreDirectories] - Directory names to skip
|
|
249
|
-
* @returns {Promise<string[]>} Array of file paths
|
|
250
|
-
*/
|
|
251
|
-
async function findFilesInDirectory({
|
|
252
|
-
directory,
|
|
253
|
-
extensions = [".html", ".liquid"],
|
|
254
|
-
ignoreDirectories = [],
|
|
255
|
-
}) {
|
|
256
|
-
const files = [];
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
const entries = await fs.promises.readdir(directory, {
|
|
260
|
-
withFileTypes: true,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
const fullPath = path.join(directory, entry.name);
|
|
265
|
-
|
|
266
|
-
if (entry.isDirectory()) {
|
|
267
|
-
if (ignoreDirectories.includes(entry.name.toLowerCase())) {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
const subFiles = await findFilesInDirectory({
|
|
271
|
-
directory: fullPath,
|
|
272
|
-
extensions,
|
|
273
|
-
ignoreDirectories,
|
|
274
|
-
});
|
|
275
|
-
files.push(...subFiles);
|
|
276
|
-
} else if (entry.isFile()) {
|
|
277
|
-
// Check if filename ends with any of the configured extensions
|
|
278
|
-
// This handles both simple (.liquid) and compound (.bookshop.liquid) extensions
|
|
279
|
-
const filenameLower = entry.name.toLowerCase();
|
|
280
|
-
const hasValidExtension = extensions.some((ext) =>
|
|
281
|
-
filenameLower.endsWith(ext),
|
|
282
|
-
);
|
|
283
|
-
if (hasValidExtension) {
|
|
284
|
-
files.push(fullPath);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
} catch (error) {
|
|
289
|
-
console.error("ERROR reading directory:", directory, error);
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return files;
|
|
294
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-compatible implementations of Eleventy's built-in filters.
|
|
3
|
-
* Some filters (get*CollectionItem, inputPathToUrl, renderTransforms) are not
|
|
4
|
-
* included as they require build-time context.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import slugify from "@sindresorhus/slugify";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Logs value to console (pass-through filter).
|
|
11
|
-
*
|
|
12
|
-
* @param {any} value - Value to log
|
|
13
|
-
* @param {string} [prefix] - Optional prefix for the log message
|
|
14
|
-
* @returns {any} The original value (for chaining)
|
|
15
|
-
*/
|
|
16
|
-
export function logFilter(value, prefix = "") {
|
|
17
|
-
if (prefix) {
|
|
18
|
-
console.log(`[${prefix}]`, value);
|
|
19
|
-
} else {
|
|
20
|
-
console.log(value);
|
|
21
|
-
}
|
|
22
|
-
// Return the original value so it can be chained or used in output
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Normalizes URL paths (simplified browser version of Eleventy's url filter).
|
|
28
|
-
*
|
|
29
|
-
* @param {string} url - URL to normalize
|
|
30
|
-
* @param {string} [pathPrefix] - Optional path prefix to prepend
|
|
31
|
-
* @returns {string} Normalized URL
|
|
32
|
-
*/
|
|
33
|
-
export function urlFilter(url, pathPrefix = "") {
|
|
34
|
-
if (!url) {
|
|
35
|
-
return "";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const urlString = String(url);
|
|
39
|
-
|
|
40
|
-
// If there's a pathPrefix, prepend it
|
|
41
|
-
if (pathPrefix) {
|
|
42
|
-
// Ensure pathPrefix starts with / and doesn't end with /
|
|
43
|
-
const normalizedPrefix = `/${pathPrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
44
|
-
|
|
45
|
-
// If url is absolute (starts with /), prepend pathPrefix
|
|
46
|
-
if (urlString.startsWith("/")) {
|
|
47
|
-
return normalizedPrefix + urlString;
|
|
48
|
-
}
|
|
49
|
-
// If url is relative, just return it
|
|
50
|
-
return urlString;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Basic normalization: ensure single slashes, remove trailing slash (except root)
|
|
54
|
-
const normalized = urlString.replace(/\/+/g, "/");
|
|
55
|
-
|
|
56
|
-
// Remove trailing slash unless it's the root path
|
|
57
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
58
|
-
return normalized.slice(0, -1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return normalized;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** @type {Record<string, any>} */
|
|
65
|
-
export const eleventyFilters = {
|
|
66
|
-
slugify,
|
|
67
|
-
log: logFilter,
|
|
68
|
-
url: urlFilter,
|
|
69
|
-
};
|