@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.
@@ -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
- };