🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@11ty/eleventy

Package Overview
Dependencies
Maintainers
2
Versions
218
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@11ty/eleventy - npm Package Compare versions

Comparing version
4.0.0-alpha.4
to
4.0.0-alpha.5
+70
src/TemplatePreprocessors.js
export class TemplatePreprocessors {
constructor(preprocessors) {
this.preprocessors = preprocessors || [];
}
async runAll(template, data) {
let { inputPath } = template;
let content = await template.getPreRender();
let skippedVia = false;
for (let [name, preprocessor] of Object.entries(this.preprocessors)) {
let { filter, callback } = preprocessor;
let filters;
if (Array.isArray(filter)) {
filters = filter;
} else if (typeof filter === "string") {
filters = filter.split(",");
} else {
throw new Error(
`Expected file extensions passed to "${name}" content preprocessor to be a string or array. Received: ${filter}`,
);
}
filters = filters.map((extension) => {
if (extension.startsWith(".") || extension === "*") {
return extension;
}
return `.${extension}`;
});
if (!filters.some((extension) => extension === "*" || inputPath.endsWith(extension))) {
// skip
continue;
}
try {
let ret = await callback.call(
{
inputPath,
},
data,
content,
);
// Returning explicit false is the same as ignoring the template
if (ret === false) {
skippedVia = name;
continue;
}
// Different from transforms: returning falsy (not false) here does nothing (skips the preprocessor)
if (ret) {
content = ret;
}
} catch (e) {
throw new Error(
`Preprocessor \`${name}\` encountered an error when transforming ${inputPath}.`,
{ cause: e },
);
}
}
return {
skippedVia,
content,
};
}
}
import { spawnAsync } from "./spawn.js";
import memoize from "./MemoizeFunction.js";
const getCreatedTimestamp = memoize(async function (filePath) {
try {
let timestamp = await spawnAsync(
"git",
// Formats https://www.git-scm.com/docs/git-log#_pretty_formats
// %at author date, UNIX timestamp
["log", "--diff-filter=A", "--follow", "-1", "--format=%at", filePath],
);
// parseInt removes trailing \n
return parseInt(timestamp, 10) * 1000;
} catch (e) {
// do nothing
}
});
const getUpdatedTimestamp = memoize(async function (filePath) {
try {
let timestamp = await spawnAsync(
"git",
// Formats https://www.git-scm.com/docs/git-log#_pretty_formats
// %at author date, UNIX timestamp
["log", "-1", "--format=%at", filePath],
);
return parseInt(timestamp, 10) * 1000;
} catch (e) {
// do nothing
}
});
export { getCreatedTimestamp, getUpdatedTimestamp };
export function union(...sets) {
let root = new Set();
for (let set of sets) {
for (let entry of set) {
root.add(entry);
}
}
return root;
}
export function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (e) {
// invalid url OR local path
return false;
}
}
export function getDirectoryFromUrl(url) {
if (url === false) {
return false;
}
// returns a url
if (url.endsWith("/")) {
return url;
}
let parts = url.split("/");
parts.pop();
return parts.join("/") + "/";
}
+46
-42
{
"name": "@11ty/eleventy",
"version": "4.0.0-alpha.4",
"version": "4.0.0-alpha.5",
"description": "A simpler static site generator.",

@@ -18,3 +18,4 @@ "publishConfig": {

"types": "./src/UserConfig.js"
}
},
"./utils/git": "./src/Util/Git.js"
},

@@ -26,3 +27,3 @@ "bin": {

"engines": {
"node": ">=20"
"node": ">=20.19"
},

@@ -71,3 +72,3 @@ "workspaces": [

"type": "git",
"url": "git://github.com/11ty/eleventy.git"
"url": "git+https://github.com/11ty/eleventy.git"
},

@@ -103,19 +104,19 @@ "bugs": "https://github.com/11ty/eleventy/issues",

"@11ty/eleventy-plugin-rss": "^2.0.4",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.1",
"@11ty/eleventy-plugin-webc": "^0.12.0-beta.3",
"@eslint/js": "^9.32.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"@11ty/eleventy-plugin-webc": "^0.12.0-beta.7",
"@eslint/js": "^9.39.1",
"@iarna/toml": "^2.2.5",
"@mdx-js/node-loader": "^3.1.0",
"@mdx-js/node-loader": "^3.1.1",
"@stylistic/eslint-plugin-js": "^4.4.1",
"@types/node": "^22.17.0",
"@vue/server-renderer": "^3.5.18",
"@types/node": "^22.19.1",
"@vue/server-renderer": "^3.5.25",
"@zachleat/noop": "^1.0.6",
"ava": "^6.4.1",
"c8": "^10.1.3",
"cross-env": "^7.0.3",
"eslint": "^9.32.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.3.0",
"jsx-async-runtime": "^1.0.3",
"luxon": "^3.7.1",
"globals": "^16.5.0",
"jsx-async-runtime": "^1.0.4",
"luxon": "^3.7.2",
"markdown-it-abbr": "^2.0.0",

@@ -125,36 +126,36 @@ "markdown-it-emoji": "^3.0.0",

"nano-staged": "^0.8.0",
"prettier": "^3.6.2",
"prettier": "^3.7.1",
"pretty": "^2.0.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"rimraf": "^6.0.1",
"sass": "^1.89.2",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"rimraf": "^6.1.2",
"sass": "^1.94.2",
"simple-git-hooks": "^2.13.1",
"tsx": "^4.20.3",
"typescript": "^5.9.2",
"vue": "^3.5.18",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"vue": "^3.5.25",
"zod": "^3.25.76",
"zod-validation-error": "^3.5.3"
"zod-validation-error": "^3.5.4"
},
"dependencies": {
"@11ty/dependency-tree": "^4.0.0",
"@11ty/dependency-tree-esm": "^2.0.0",
"@11ty/eleventy-dev-server": "3.0.0-alpha.3",
"@11ty/eleventy-plugin-bundle": "^3.0.6",
"@11ty/dependency-tree": "^4.0.1",
"@11ty/dependency-tree-esm": "^2.0.3",
"@11ty/eleventy-dev-server": "^3.0.0-alpha.5",
"@11ty/eleventy-plugin-bundle": "^3.0.7",
"@11ty/eleventy-utils": "^2.0.7",
"@11ty/gray-matter": "^2.0.0",
"@11ty/gray-matter": "^2.0.1",
"@11ty/lodash-custom": "^4.17.21",
"@11ty/parse-date-strings": "^2.0.5",
"@11ty/posthtml-urls": "^1.0.1",
"@11ty/recursive-copy": "^4.0.2",
"@sindresorhus/slugify": "^2.2.1",
"@11ty/parse-date-strings": "^2.0.6",
"@11ty/posthtml-urls": "^1.0.2",
"@11ty/recursive-copy": "^4.0.3",
"@sindresorhus/slugify": "^3.0.0",
"bcp-47-normalize": "^2.3.0",
"chokidar": "^4.0.3",
"debug": "^4.4.1",
"chokidar": "^5.0.0",
"debug": "^4.4.3",
"dependency-graph": "^1.0.0",
"entities": "^6.0.1",
"import-module-string": "^2.0.1",
"entities": "^7.0.0",
"import-module-string": "^2.0.3",
"iso-639-1": "^3.1.5",
"kleur": "^4.1.5",
"liquidjs": "^10.21.1",
"liquidjs": "^10.24.0",
"markdown-it": "^14.1.0",

@@ -167,10 +168,13 @@ "minimist": "^1.2.8",

"please-upgrade-node": "^3.2.0",
"posthtml": "^0.16.6",
"posthtml": "^0.16.7",
"posthtml-match-helper": "^2.0.3",
"semver": "^7.7.2",
"tinyglobby": "^0.2.14"
"semver": "^7.7.3",
"tinyglobby": "^0.2.15"
},
"overrides": {
"chokidar": "$chokidar"
"commander": "^14",
"chokidar": "$chokidar",
"fdir": "6.4.6",
"posthtml-parser": "^0.12"
}
}

@@ -48,3 +48,3 @@ <p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="eleventy Logo"></p>

- [Top Feature Requests](https://github.com/11ty/eleventy/issues?q=label%3Aneeds-votes+sort%3Areactions-%2B1-desc+label%3Aenhancement) (Add your own votes using the 👍 reaction)
- [Top Feature Requests](https://github.com/11ty/eleventy/discussions/categories/enhancement-queue?discussions_q=is%3Aopen+category%3A%22Enhancement+Queue%22+sort%3Atop) (Vote for your favorites!)
- [Top Bugs 😱](https://github.com/11ty/eleventy/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions) (Add your own votes using the 👍 reaction)

@@ -51,0 +51,0 @@

@@ -308,3 +308,3 @@ import path from "node:path";

let oldData = lodashGet(globalData, objectPathTarget);
data = TemplateData.mergeDeep(this.config.dataDeepMerge, oldData, data);
data = Merge(oldData, data);
}

@@ -426,3 +426,3 @@

}
TemplateData.mergeDeep(this.config.dataDeepMerge, localData, cleanedDataForPath);
Merge(localData, cleanedDataForPath);
}

@@ -637,14 +637,2 @@ }

static mergeDeep(deepMerge, target, ...source) {
if (!deepMerge && deepMerge !== undefined) {
return Object.assign(target, ...source);
} else {
return TemplateData.merge(target, ...source);
}
}
static merge(target, ...source) {
return Merge(target, ...source);
}
/* Like cleanupData() but does not mutate */

@@ -651,0 +639,0 @@ static getCleanedTagsImmutable(data, options = {}) {

@@ -129,2 +129,3 @@ import { relative } from "node:path";

shouldTriggerConfigReset(changedFiles) {
// looks for all eligible config files (not just the active one, handles config file rename)
let configFilePaths = new Set(this.eleventyConfig.getLocalProjectConfigFiles());

@@ -146,3 +147,3 @@

for (const configFilePath of configFilePaths) {
for (let configFilePath of configFilePaths) {
// Any dependencies of the config file changed

@@ -302,3 +303,3 @@ let configFileDependencies = new Set(this.watchTargets.getDependenciesOf(configFilePath));

// Watch the local project config file
this.watchTargets.add(this.eleventyConfig.getLocalProjectConfigFiles());
this.watchTargets.add(this.eleventyConfig.getActiveConfigPath());

@@ -407,8 +408,2 @@ // Template and Directory Data Files

// TODO use DirContains
let dataDir = TemplatePath.stripLeadingDotSlash(this.templateData.getDataDir());
function filterOutGlobalDataFiles(path) {
return !dataDir || !TemplatePath.stripLeadingDotSlash(path).startsWith(dataDir);
}
// Lazy resolve isEsm only for --watch

@@ -421,5 +416,11 @@ this.watchTargets.setProjectUsingEsm(this.isEsm);

// TODO use DirContains
let dataDir = TemplatePath.stripLeadingDotSlash(this.templateData.getDataDir());
function filterOutGlobalDataFiles(path) {
return !dataDir || !TemplatePath.stripLeadingDotSlash(path).startsWith(dataDir);
}
// Config file dependencies
await this.watchTargets.addDependencies(
this.eleventyConfig.getLocalProjectConfigFiles(),
this.eleventyConfig.getActiveConfigPath(),
filterOutGlobalDataFiles,

@@ -426,0 +427,0 @@ );

@@ -151,6 +151,2 @@ import { existsSync, statSync, readFileSync } from "node:fs";

get templateData() {
if (!this._templateData) {
this._templateData = new TemplateData(this.templateConfig);
}
return this._templateData;

@@ -157,0 +153,0 @@ }

@@ -10,3 +10,5 @@ import { RetrieveGlobals } from "../../Util/RetrieveGlobals.js";

if (trimmed.startsWith("{")) {
return RetrieveGlobals(`export default ${trimmed}`, filePath).then((res) => {
return RetrieveGlobals(`export default ${trimmed}`, filePath, {
isJavaScriptFrontMatterCompat: true, // turns off implicit exports
}).then((res) => {
return res.default;

@@ -13,0 +15,0 @@ });

@@ -52,4 +52,4 @@ import moo from "moo";

strictFilters: true,
// TODO?
// cache: true,
jsTruthy: true, // Breaking in v4 #3507
};

@@ -329,2 +329,8 @@

options.globals = {
page: data?.page,
eleventy: data?.eleventy,
collections: data?.collections,
};
return engine.render(tmpl, data, options);

@@ -331,0 +337,0 @@ };

@@ -117,6 +117,11 @@ import debugUtil from "debug";

// TODO(zachleat): variableName should work with quotes or without quotes (same as {% set %})
this.addPairedShortcode("setAsync", function (content, variableName) {
this.ctx[variableName] = content;
return "";
});
// This was changed to be an async function in v4 but notably previous versions of synchronous paired shortcodes used CallExtensionAsync
this.addPairedShortcode(
"setAsync",
async function (content, variableName) {
this.ctx[variableName] = content;
return "";
},
true,
);

@@ -287,30 +292,30 @@ this.addCustomTags(this.config.nunjucksTags);

this.parse = function (parser, nodes) {
var tok = parser.nextToken();
if (isAsync) {
this.parse = function (parser, nodes) {
var tok = parser.nextToken();
var args = parser.parseSignature(true, true);
parser.advanceAfterBlockEnd(tok.value);
var args = parser.parseSignature(true, true);
parser.advanceAfterBlockEnd(tok.value);
var body = parser.parseUntilBlocks("end" + shortcodeName);
parser.advanceAfterBlockEnd();
var body = parser.parseUntilBlocks("end" + shortcodeName);
parser.advanceAfterBlockEnd();
return new nodes.CallExtensionAsync(this, "run", args, [body]);
};
return new nodes.CallExtensionAsync(this, "run", args, [body]);
};
this.run = function (...args) {
let resolve = args.pop();
let body = args.pop();
let [context, ...argArray] = args;
this.run = function (...args) {
let resolve = args.pop();
let body = args.pop();
let [context, ...argArray] = args;
body(function (e, bodyContent) {
if (e) {
resolve(
new EleventyNunjucksError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\``,
e,
),
);
}
body(function (e, bodyContent) {
if (e) {
resolve(
new EleventyNunjucksError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\``,
e,
),
);
}
if (isAsync) {
let ret = shortcodeFn.call(

@@ -342,21 +347,34 @@ Nunjucks.normalizeContext(context),

);
} else {
try {
resolve(
null,
new NunjucksLib.runtime.SafeString(
shortcodeFn.call(Nunjucks.normalizeContext(context), bodyContent, ...argArray),
),
);
} catch (e) {
resolve(
new EleventyNunjucksError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\``,
e,
),
);
}
});
};
} else {
this.parse = function (parser, nodes) {
var tok = parser.nextToken();
var args = parser.parseSignature(true, true);
parser.advanceAfterBlockEnd(tok.value);
var body = parser.parseUntilBlocks("end" + shortcodeName);
parser.advanceAfterBlockEnd();
return new nodes.CallExtension(this, "run", args, [body]);
};
this.run = function (...args) {
let body = args.pop();
let [context, ...argArray] = args;
let bodyContent = body();
try {
return new NunjucksLib.runtime.SafeString(
shortcodeFn.call(Nunjucks.normalizeContext(context), bodyContent, ...argArray),
);
} catch (e) {
throw new EleventyNunjucksError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\``,
e,
);
}
});
};
};
}
};

@@ -363,0 +381,0 @@ }

@@ -34,3 +34,5 @@ import { glob } from "tinyglobby";

// Strip leading slashes from everything!
globs = globs.map((entry) => TemplatePath.stripLeadingDotSlash(entry));
globs = globs.map((entry) => {
return TemplatePath.stripLeadingDotSlash(entry);
});

@@ -37,0 +39,0 @@ let cwd = GlobRemap.getCwd(globs);

import { TemplatePath } from "@11ty/eleventy-utils";
import isValidUrl from "../Util/ValidUrl.js";
import { isValidUrl } from "../Util/UrlUtil.js";

@@ -5,0 +5,0 @@ // Note: This filter is used in the Eleventy Navigation plugin in versions prior to 0.3.4

@@ -5,3 +5,3 @@ import { DeepCopy } from "@11ty/eleventy-utils";

import { HtmlTransformer } from "../Util/HtmlTransformer.js";
import isValidUrl from "../Util/ValidUrl.js";
import { isValidUrl } from "../Util/UrlUtil.js";

@@ -8,0 +8,0 @@ function addPathPrefixToUrl(url, pathPrefix, base) {

import path from "node:path";
import { TemplatePath } from "@11ty/eleventy-utils";
import isValidUrl from "../Util/ValidUrl.js";
import { isValidUrl } from "../Util/UrlUtil.js";

@@ -5,0 +5,0 @@ function getValidPath(contentMap, testPath) {

@@ -340,3 +340,4 @@ import { isPlainObject } from "@11ty/eleventy-utils";

let { /*linkInstance,*/ rawPath, path, href } = await cloned.getOutputLocations(clonedData);
let { /*linkInstance,*/ rawPath, path, href, dir } =
await cloned.getOutputLocations(clonedData);
// TODO subdirectory to links if the site doesn’t live at /

@@ -352,2 +353,3 @@ if (rawPath) {

clonedData.page.outputPath = path;
clonedData.page.dir = dir;

@@ -354,0 +356,0 @@ entries.push({

@@ -1,6 +0,6 @@

import path from "node:path";
import { parse } from "node:path";
import { statSync } from "node:fs";
import lodash from "@11ty/lodash-custom";
import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
import debugUtil from "debug";

@@ -10,5 +10,3 @@

import ConsoleLogger from "./Util/ConsoleLogger.js";
import getDateFromGitLastUpdated from "./Util/DateGitLastUpdated.js";
import getDateFromGitFirstAdded from "./Util/DateGitFirstAdded.js";
import TemplateData from "./Data/TemplateData.js";
import { getCreatedTimestamp, getUpdatedTimestamp } from "./Util/Git.js";
import TemplateContent from "./TemplateContent.js";

@@ -28,2 +26,5 @@ import TemplatePermalink from "./TemplatePermalink.js";

import { FileSystemManager } from "./Util/FileSystemManager.js";
import { TemplatePreprocessors } from "./TemplatePreprocessors.js";
import PathNormalizer from "./Util/PathNormalizer.js";
import { getDirectoryFromUrl } from "./Util/UrlUtil.js";

@@ -39,2 +40,3 @@ const { set: lodashSet, get: lodashGet } = lodash;

#stats;
#preprocessorCache;

@@ -45,3 +47,3 @@ constructor(templatePath, templateData, extensionMap, config) {

this.parsed = path.parse(templatePath);
this.parsed = parse(templatePath);

@@ -70,2 +72,4 @@ // for pagination

this.behavior.setOutputFormat(this.outputFormat);
this.templatePreprocessor = new TemplatePreprocessors(this.config.preprocessors);
}

@@ -137,2 +141,6 @@

if (types.data || types.read) {
this.#preprocessorCache = undefined;
}
if (types.data) {

@@ -211,11 +219,2 @@ delete this._dataCache;

async _getRawPermalinkInstance(permalinkValue) {
let perm = new TemplatePermalink(permalinkValue, this.extraOutputSubdirectory);
perm.setUrlTransforms(this.config.urlTransforms);
this.behavior.setFromPermalink(perm);
return perm;
}
async _getLink(data) {

@@ -230,2 +229,4 @@ if (!data) {

let permalinkValue;
let isDynamicPermalinkEnabled =
this.config.dynamicPermalinks && data.dynamicPermalink !== false;

@@ -237,3 +238,4 @@ // `permalink: false` means render but no file system write, e.g. use in collections only)

permalinkValue = permalink;
} else if (permalink && (!this.config.dynamicPermalinks || data.dynamicPermalink === false)) {
} else if (permalink && !isDynamicPermalinkEnabled) {
// Issue #838
debugDev("Not using dynamic permalinks, using %o", permalink);

@@ -282,3 +284,3 @@ permalinkValue = permalink;

// Override default permalink behavior. Only do this if permalink was _not_ in the data cascade
if (!permalink && this.config.dynamicPermalinks && data.dynamicPermalink !== false) {
if (!permalink && isDynamicPermalinkEnabled) {
let tr = await this.getTemplateRender();

@@ -301,3 +303,10 @@ let permalinkCompilation = tr.engine.permalinkNeedsCompilation("");

if (permalinkValue !== undefined) {
return this._getRawPermalinkInstance(permalinkValue);
let p = new TemplatePermalink(
permalinkValue,
this.extraOutputSubdirectory,
isDynamicPermalinkEnabled,
);
p.setUrlTransforms(this.config.urlTransforms);
this.behavior.setFromPermalink(p);
return p;
}

@@ -311,2 +320,3 @@

this.engine.defaultTemplateFileExtension,
isDynamicPermalinkEnabled,
);

@@ -339,7 +349,9 @@ p.setUrlTransforms(this.config.urlTransforms);

let href = link.toHref();
return {
linkInstance: link,
rawPath: link.toOutputPath(),
href: link.toHref(),
rawPath: link.toOutputPath(), // includes output directory
href,
path: path,
dir: getDirectoryFromUrl(href), // for `page.dir`
};

@@ -414,10 +426,3 @@ }

try {
let mergedData = TemplateData.mergeDeep(
this.config.dataDeepMerge,
{},
globalData,
mergedLayoutData,
localData,
frontMatterData,
);
let mergedData = Merge({}, globalData, mergedLayoutData, localData, frontMatterData);

@@ -466,8 +471,8 @@ if (this.config.freezeReservedData) {

data.page.inputPath = this.inputPath;
// parsed dir never has the trailing slash
data.page.inputPathDir = PathNormalizer.getDirectoryFromFilePath(this.inputPath);
data.page.fileSlug = this.fileSlugStr;
data.page.filePathStem = this.filePathStem;
data.page.outputFileExtension = this.engine.defaultTemplateFileExtension;
data.page.templateSyntax = this.templateRender.getEnginesList(
data[this.config.keys.engineOverride],
);
data.page.templateSyntax = this.getEngineNames(data[this.config.keys.engineOverride]);

@@ -667,5 +672,6 @@ let newDate = await this.getMappedDate(data);

let { href, path } = await this.getOutputLocations(data);
let { href, path, dir } = await this.getOutputLocations(data);
data.page.url = href;
data.page.outputPath = path;
data.page.dir = dir;
}

@@ -736,78 +742,20 @@ }

static async runPreprocessors(inputPath, content, data, preprocessors) {
let skippedVia = false;
for (let [name, preprocessor] of Object.entries(preprocessors)) {
let { filter, callback } = preprocessor;
let filters;
if (Array.isArray(filter)) {
filters = filter;
} else if (typeof filter === "string") {
filters = filter.split(",");
} else {
throw new Error(
`Expected file extensions passed to "${name}" content preprocessor to be a string or array. Received: ${filter}`,
);
}
filters = filters.map((extension) => {
if (extension.startsWith(".") || extension === "*") {
return extension;
}
return `.${extension}`;
});
if (!filters.some((extension) => extension === "*" || inputPath.endsWith(extension))) {
// skip
continue;
}
try {
let ret = await callback.call(
{
inputPath,
},
data,
content,
);
// Returning explicit false is the same as ignoring the template
if (ret === false) {
skippedVia = name;
continue;
}
// Different from transforms: returning falsy (not false) here does nothing (skips the preprocessor)
if (ret) {
content = ret;
}
} catch (e) {
throw new EleventyBaseError(
`Preprocessor \`${name}\` encountered an error when transforming ${inputPath}.`,
e,
);
}
async runPreprocessors(data) {
// @cachedproperty
if (!this.#preprocessorCache) {
this.#preprocessorCache = this.templatePreprocessor.runAll(this, data);
}
return {
skippedVia,
content,
};
return this.#preprocessorCache;
}
async getTemplates(data) {
let content = await this.getPreRender();
let { skippedVia, content: rawInput } = await Template.runPreprocessors(
this.inputPath,
content,
data,
this.config.preprocessors,
);
let { skippedVia: skippedViaPreprocessorName, content: rawInput } =
await this.runPreprocessors(data);
if (skippedVia) {
if (skippedViaPreprocessorName) {
debug(
"Skipping %o, the %o preprocessor returned an explicit `false`",
this.inputPath,
skippedVia,
skippedViaPreprocessorName,
);

@@ -1139,5 +1087,9 @@ return [];

if (dateValue.toLowerCase() === "git last modified") {
let d = await getDateFromGitLastUpdated(this.inputPath);
if (d) {
return d;
let timestamp = await getUpdatedTimestamp(this.inputPath);
if (timestamp) {
debug(
`getMappedDate: found git last modified timestamp for ${this.inputPath}: %o`,
timestamp,
);
return new Date(timestamp);
}

@@ -1152,5 +1104,9 @@

if (dateValue.toLowerCase() === "git created") {
let d = await getDateFromGitFirstAdded(this.inputPath);
if (d) {
return d;
let timestamp = await getCreatedTimestamp(this.inputPath);
if (timestamp) {
debug(
`getMappedDate: found git created timestamp for ${this.inputPath}: %o`,
timestamp,
);
return new Date(timestamp);
}

@@ -1157,0 +1113,0 @@

@@ -55,2 +55,3 @@ import { existsSync } from "node:fs";

#previousBuildModifiedFile;
#activeConfigPath;

@@ -177,3 +178,3 @@ constructor(customRootConfig, projectConfigPath) {

let configFiles = this.getLocalProjectConfigFiles();
let configFile = configFiles.find((path) => path && existsSync(path));
let configFile = configFiles.find((path) => path && this.existsCache.exists(path));
if (configFile) {

@@ -192,2 +193,9 @@ return configFile;

getActiveConfigPath() {
if (!this.#activeConfigPath) {
this.#activeConfigPath = this.getLocalProjectConfigFile();
}
return this.#activeConfigPath;
}
setProjectUsingEsm(isEsmProject) {

@@ -206,2 +214,4 @@ this.isEsm = !!isEsmProject;

async reset() {
this.#existsCache.reset();
debugDev("Resetting configuration: TemplateConfig and UserConfig.");

@@ -235,2 +245,4 @@ this.userConfig.reset();

async init(overrides) {
this.#activeConfigPath = undefined; // reset
await this.initializeRootConfig();

@@ -390,3 +402,3 @@

let path = this.projectConfigPaths.filter((path) => path).find((path) => existsSync(path));
let path = this.getActiveConfigPath();

@@ -393,0 +405,0 @@ if (this.projectConfigPaths.length > 0 && this.#configManuallyDefined && !path) {

@@ -29,3 +29,3 @@ import { readFileSync } from "node:fs";

#templateRender;
#preprocessorEngine;
#renderPreprocessorEngine;
#extensionMap;

@@ -56,3 +56,3 @@ #configOptions;

let engine = await this.templateRender.getEngineByName(preprocessorEngineName);
this.#preprocessorEngine = engine;
this.#renderPreprocessorEngine = engine;
}

@@ -438,2 +438,6 @@ }

getEngineNames(engineOverride) {
return this.templateRender.getEnginesList(engineOverride);
}
async getEngineOverride() {

@@ -447,4 +451,4 @@ return this.getFrontMatterData().then((data) => {

isTemplateCacheable() {
if (this.#preprocessorEngine) {
return this.#preprocessorEngine.cacheable;
if (this.#renderPreprocessorEngine) {
return this.#renderPreprocessorEngine.cacheable;
}

@@ -552,4 +556,4 @@ return this.engine.cacheable;

// TODO pass in engineOverride here
if (this.#preprocessorEngine) {
engine = this.#preprocessorEngine;
if (this.#renderPreprocessorEngine) {
engine = this.#renderPreprocessorEngine;
}

@@ -556,0 +560,0 @@

@@ -12,3 +12,3 @@ import path from "node:path";

this.inputPath = inputPath;
this.cleanInputPath = inputPath.replace(/^.\//, "");
this.cleanInputPath = TemplatePath.stripLeadingDotSlash(inputPath);

@@ -15,0 +15,0 @@ let dirs = this.cleanInputPath.split("/");

@@ -1,2 +0,2 @@

import { TemplatePath } from "@11ty/eleventy-utils";
import { Merge, TemplatePath } from "@11ty/eleventy-utils";
import debugUtil from "debug";

@@ -6,3 +6,2 @@

import TemplateContent from "./TemplateContent.js";
import TemplateData from "./Data/TemplateData.js";
import layoutCache from "./LayoutCache.js";

@@ -13,2 +12,32 @@

// https://github.com/11ty/eleventy/issues/3954
class CdataWrapper {
static PREFIX = "<![CDATA[STARTRAW";
static POSTFIX = "ENDRAW]]>";
constructor(templateSyntax) {
this.isEligible = CdataWrapper.isEligible(templateSyntax);
}
// Markdown only
static isEligible(templateSyntax) {
return templateSyntax.split(",").includes("md");
}
wrap(content) {
if (this.isEligible) {
return CdataWrapper.PREFIX + content + CdataWrapper.POSTFIX;
}
return content;
}
unwrap(content) {
if (this.isEligible) {
return content.replaceAll(CdataWrapper.PREFIX, "").replaceAll(CdataWrapper.POSTFIX, "");
}
return content;
}
}
class TemplateLayout extends TemplateContent {

@@ -148,3 +177,3 @@ constructor(key, extensionMap, eleventyConfig) {

// Deep merge of layout front matter
let data = TemplateData.mergeDeep(this.config.dataDeepMerge, {}, ...dataToMerge);
let data = Merge({}, ...dataToMerge);
delete data[this.config.keys.layout];

@@ -183,2 +212,4 @@

fns.push({
inputPath: this.inputPath,
template: this,
render: await this.getCachedCompiledLayoutFunction(),

@@ -222,9 +253,13 @@ });

let compiledFunctions = await this.getCompiledLayoutFunctions();
for (let { render } of compiledFunctions) {
for (let { render, template } of compiledFunctions) {
let templateSyntax = template.getEngineNames(pageEntry.data[this.config.keys.engineOverride]);
let cdata = new CdataWrapper(templateSyntax);
let data = {
content: templateContent,
content: cdata.wrap(templateContent),
...pageEntry.data,
};
templateContent = await render(data);
templateContent = cdata.unwrap(await render(data));
}

@@ -231,0 +266,0 @@

@@ -7,3 +7,2 @@ import { isPlainObject, TemplatePath } from "@11ty/eleventy-utils";

import UsingCircularTemplateContentReferenceError from "./Errors/UsingCircularTemplateContentReferenceError.js";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import DuplicatePermalinkOutputError from "./Errors/DuplicatePermalinkOutputError.js";

@@ -15,5 +14,2 @@ import TemplateData from "./Data/TemplateData.js";

class EleventyMapPagesError extends EleventyBaseError {}
class EleventyDataSchemaError extends EleventyBaseError {}
// These template URL filenames are allowed to exclude file extensions

@@ -41,2 +37,3 @@ const EXTENSIONLESS_URL_ALLOWLIST = [

this.map = [];
this.inputPathMap = new Map(); // NEW: O(1) lookup Map for performance
this.collectionsData = null;

@@ -75,5 +72,11 @@ this.cached = false;

let entries = await template.getTemplateMapEntries(data);
let { skippedVia } = await template.runPreprocessors(data);
if (skippedVia) {
return;
}
for (let map of entries) {
this.map.push(map);
this._addToInputPathMap(map); // NEW: Add to lookup Map for O(1) access
}

@@ -86,2 +89,8 @@ }

_addToInputPathMap(mapEntry) {
// Store under absolute path
let absoluteInputPath = TemplatePath.absolutePath(mapEntry.inputPath);
this.inputPathMap.set(absoluteInputPath, mapEntry);
}
getTagTarget(str) {

@@ -199,6 +208,3 @@ if (str === "collections") {

} catch (e) {
throw new EleventyMapPagesError(
"Error generating template page(s) for " + map.inputPath + ".",
e,
);
throw new Error("Error generating template page(s) for " + map.inputPath + ".", { cause: e });
}

@@ -334,10 +340,5 @@

// TODO(slightlyoff): hot inner loop?
getMapEntryForInputPath(inputPath) {
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.map.find((entry) => {
if (entry.inputPath === inputPath || entry.inputPath === absoluteInputPath) {
return entry;
}
});
return this.inputPathMap.get(absoluteInputPath);
}

@@ -361,5 +362,5 @@

} catch (e) {
throw new EleventyDataSchemaError(
throw new Error(
`Error in the data schema for: ${map.inputPath} (via \`eleventyDataSchema\`)`,
e,
{ cause: e },
);

@@ -366,0 +367,0 @@ }

@@ -5,4 +5,6 @@ import path from "node:path";

class TemplatePermalink {
#dynamicPermalinkEnabled;
// `link` with template syntax should have already been rendered in Template.js
constructor(link, extraSubdir) {
constructor(link, extraSubdir, isDynamicPermalinkEnabled = true) {
let isLinkAnObject = isPlainObject(link);

@@ -12,2 +14,3 @@

this._writeToFileSystem = true;
this.#dynamicPermalinkEnabled = isDynamicPermalinkEnabled;

@@ -49,6 +52,3 @@ let buildLink;

throw new Error(
"Expected permalink value to be a string. Received `" +
typeof buildLink +
"`" +
stringToString,
`Expected permalink value to be a string. Received \`${typeof buildLink}\` (dynamicPermalink: ${this.#dynamicPermalinkEnabled})${stringToString}`,
);

@@ -181,3 +181,9 @@ }

static generate(dir, filenameNoExt, extraSubdir, fileExtension = "html") {
static generate(
dir,
filenameNoExt,
extraSubdir,
fileExtension = "html",
isDynamicPermalinkEnabled,
) {
let path;

@@ -195,3 +201,3 @@ if (fileExtension === "html") {

return new TemplatePermalink(path, extraSubdir);
return new TemplatePermalink(path, extraSubdir, isDynamicPermalinkEnabled);
}

@@ -198,0 +204,0 @@ }

@@ -0,6 +1,6 @@

import debugUtil from "debug";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import TemplateEngineManager from "./Engines/TemplateEngineManager.js";
// import debugUtil from "debug";
// const debug = debugUtil("Eleventy:TemplateRender");
const debugConfiguration = debugUtil("Eleventy:UserConfig");

@@ -22,2 +22,8 @@ class TemplateRenderUnknownEngineError extends EleventyBaseError {}

this.parseMarkdownWith = this.config.markdownTemplateEngine;
if (this.parseMarkdownWith === "md") {
this.parseMarkdownWith = false;
debugConfiguration(
"Misconfiguration warning: the preprocessing template syntax for Markdown files cannot be Markdown, we’re assuming you meant `false` to skip preprocessing altogether (via the `markdownTemplateEngine` configuration property or the `setMarkdownTemplateEngine` configuration method). Read more: https://www.11ty.dev/docs/config/#default-template-engine-for-markdown-files",
);
}
this.parseHtmlWith = this.config.htmlTemplateEngine;

@@ -24,0 +30,0 @@ }

@@ -28,4 +28,2 @@ import debugUtil from "debug";

#quietModeLocked = false;
/** @type {boolean} */
#dataDeepMergeModified = false;
/** @type {number|undefined} */

@@ -66,9 +64,2 @@ #uniqueId;

// this.DateTime removed in v4
get DateTime() {
throw new Error(
'Luxon’s DateTime property in configuration was removed in Eleventy v4. Please `import { DateTime } from "luxon"` directly.',
);
}
// Internally used in TemplateContent for cache keys

@@ -120,3 +111,3 @@ _getUniqueId() {

pairedShortcodes: {},
parameterParsing: "legacy", // or builtin
parameterParsing: "builtin", // or legacy (Breaking: default swapped in v4.0.0)
};

@@ -179,3 +170,2 @@

this.dataDeepMerge = true;
this.extensionMap = new Set();

@@ -344,5 +334,5 @@ this.extensionMapClasses = {};

// This is a method for plugins, probably shouldn’t use this in projects.
// Projects should use `setLibrary` as documented here:
// https://github.com/11ty/eleventy/blob/master/docs/engines/markdown.md#use-your-own-options
// Don’t use this, projects should use `amendLibrary` as documented here:
// https://www.11ty.dev/docs/languages/markdown/#optional-amend-the-library-instance
// Warning: this is in use by the Syntax Highlighting plugin (as of v5.0.2)
addMarkdownHighlighter(highlightFn) {

@@ -805,6 +795,2 @@ this.markdownHighlighter = highlightFn;

*/
_normalizeTemplateFormats() {
throw new Error("The internal _normalizeTemplateFormats() method was removed in Eleventy 3.0");
}
setTemplateFormats(templateFormats) {

@@ -875,12 +861,2 @@ this.templateFormats = templateFormats;

setDataDeepMerge(deepMerge) {
this.#dataDeepMergeModified = true;
this.dataDeepMerge = !!deepMerge;
}
// Used by the Upgrade Helper Plugin
isDataDeepMergeModified() {
return this.#dataDeepMergeModified;
}
addWatchTarget(additionalWatchTargets, options = {}) {

@@ -907,9 +883,2 @@ // Reset the config when the target path changes

setBrowserSyncConfig() {
this._attemptedBrowserSyncUse = true;
debug(
"The `setBrowserSyncConfig` method was removed in Eleventy 2.0.0. Use `setServerOptions` with the new Eleventy development server or the `@11ty/eleventy-browser-sync` plugin moving forward.",
);
}
setChokidarConfig(options = {}) {

@@ -1183,7 +1152,2 @@ this.chokidarConfig = options;

configureTemplateHandling(options = {}) {
// Was used for sync/async swapping on file write operations
throw new Error("Internal configuration API method `configureTemplateHandling` was removed.");
}
/*

@@ -1280,3 +1244,2 @@ * Collections

watchIgnores: this.watchIgnores,
dataDeepMerge: this.dataDeepMerge,
watchJavaScriptDependencies: this.watchJavaScriptDependencies,

@@ -1323,2 +1286,33 @@ additionalWatchTargets: this.additionalWatchTargets,

// Removed features
get DateTime() {
throw new Error(
'Luxon’s DateTime property in configuration was removed in Eleventy v4. Please `import { DateTime } from "luxon"` directly.',
);
}
_normalizeTemplateFormats() {
throw new Error("The internal _normalizeTemplateFormats() method was removed in Eleventy v3");
}
setBrowserSyncConfig() {
this._attemptedBrowserSyncUse = true;
debug(
"The `setBrowserSyncConfig` method was removed in Eleventy v2. Use `setServerOptions` with the new Eleventy development server or the `@11ty/eleventy-browser-sync` plugin moving forward.",
);
}
configureTemplateHandling(options = {}) {
// Was used for sync/async swapping on file write operations
throw new Error("Internal configuration API method `configureTemplateHandling` was removed.");
}
setDataDeepMerge(deepMerge) {
if (Boolean(deepMerge) === false) {
throw new Error(
"The `setDataDeepMerge(false)` Configuration API feature was removed in Eleventy v4. Read more at https://github.com/11ty/eleventy/issues/3937",
);
}
}
// No-op functions for backwards compat

@@ -1330,4 +1324,8 @@ addHandlebarsHelper() {}

addPairedHandlebarsShortcode() {}
// Used by the Upgrade Helper Plugin v1 (no longer relevant)
// https://github.com/11ty/eleventy-upgrade-help/blob/v1.x/src/data-deep-merge.js#L5-L9
isDataDeepMergeModified() {}
}
export default UserConfig;

@@ -13,2 +13,7 @@ import { existsSync, statSync } from "node:fs";

reset() {
this.#exists = new Map();
this.#dirs = new Map();
}
get size() {

@@ -15,0 +20,0 @@ return this.#exists.size;

import path from "node:path";
import { TemplatePath } from "@11ty/eleventy-utils";
import isValidUrl from "../Util/ValidUrl.js";
import { isValidUrl } from "./UrlUtil.js";
import { isGlobMatch } from "./GlobMatcher.js";

@@ -5,0 +5,0 @@

import dependencyTree from "@11ty/dependency-tree";
import { find } from "@11ty/dependency-tree-esm";
import { find, findGraph, mergeGraphs } from "@11ty/dependency-tree-esm";
import { TemplatePath } from "@11ty/eleventy-utils";
import { DepGraph } from "dependency-graph";
import { union } from "./SetUtil.js";
import EleventyBaseError from "../Errors/EleventyBaseError.js";

@@ -12,3 +14,3 @@

static async getDependencies(inputFiles, isProjectUsingEsm) {
static async getCommonJsDependencies(inputFiles, isProjectUsingEsm) {
let depSet = new Set();

@@ -38,2 +40,8 @@

return depSet;
}
static async getEsmDependencies(inputFiles, isProjectUsingEsm) {
let depSet = new Set();
let esmFiles = inputFiles.filter(

@@ -53,6 +61,30 @@ (file) => (isProjectUsingEsm && file.endsWith(".js")) || file.endsWith(".mjs"),

return Array.from(depSet).sort();
return depSet;
}
static async getDependencies(inputFiles, isProjectUsingEsm) {
let cjs = await this.getCommonJsDependencies(inputFiles, isProjectUsingEsm);
let esm = await this.getEsmDependencies(inputFiles, isProjectUsingEsm);
return Array.from(union(cjs, esm));
}
static async getEsmGraph(inputFiles, isProjectUsingEsm) {
let rootGraph = new DepGraph();
let esmFiles = inputFiles.filter(
(file) => (isProjectUsingEsm && file.endsWith(".js")) || file.endsWith(".mjs"),
);
for (let file of esmFiles) {
try {
let graph = await findGraph(file);
mergeGraphs(rootGraph, graph);
} catch (e) {
throw new EleventyBaseError(this.getErrorMessage(file, "ESM"), e);
}
}
return rootGraph;
}
}
export default JavaScriptDependencies;

@@ -1,2 +0,2 @@

import path from "node:path";
import { parse, sep } from "node:path";
import { TemplatePath } from "@11ty/eleventy-utils";

@@ -12,4 +12,4 @@ import { fileURLToPath } from "../Adapters/Packages/url.js";

let separator = "/";
if (inputPath.includes(path.sep)) {
separator = path.sep;
if (inputPath.includes(sep)) {
separator = sep;
}

@@ -39,5 +39,19 @@

}
return inputPath.split(path.sep).join("/");
return inputPath.split(sep).join("/");
}
static addTrailingSlashToDirectory(dir) {
if (dir.endsWith("/")) {
return dir;
}
return dir + "/";
}
// returns a path
static getDirectoryFromFilePath(filePath) {
let parsed = parse(filePath);
return this.addTrailingSlashToDirectory(parsed.dir);
}
static fullNormalization(inputPath) {

@@ -44,0 +58,0 @@ if (typeof inputPath !== "string") {

@@ -137,6 +137,6 @@ import { existsSync, statSync } from "node:fs";

// Normalize absolute paths to relative, #3805
// if(path.isAbsolute(dirOrFile)) {
// dirOrFile = path.relative(".", dirOrFile);
// }
// Normalize absolute paths to relative, #3805 #3896
if (path.isAbsolute(dirOrFile)) {
dirOrFile = path.relative(".", dirOrFile);
}

@@ -230,2 +230,8 @@ // Input has to exist (assumed glob if it does not exist)

this.#raw.output = dir;
// Normalize absolute paths to relative, #3805 #3896
if (path.isAbsolute(dir)) {
dir = path.relative(".", dir);
}
this.#dirs.output = ProjectDirectories.normalizeDirectory(dir || "");

@@ -232,0 +238,0 @@ }

@@ -1,2 +0,2 @@

function withResolvers() {
export function withResolvers() {
if ("withResolvers" in Promise) {

@@ -14,3 +14,1 @@ return Promise.withResolvers();

}
export { withResolvers };

@@ -5,3 +5,7 @@ import { RetrieveGlobals as NodeRetrieveGlobals } from "node-retrieve-globals";

export async function RetrieveGlobals(code, filePath) {
export async function RetrieveGlobals(code, filePath, options = {}) {
let { isJavaScriptFrontMatterCompat } = Object.assign(
{ isJavaScriptFrontMatterCompat: false },
options,
);
let data = {

@@ -19,3 +23,4 @@ page: {

if (nonBuiltinImports.length === 0) {
return importFromString(code, { ast, data, filePath });
let implicitExports = isJavaScriptFrontMatterCompat ? false : true;
return importFromString(code, { ast, data, filePath, implicitExports });
}

@@ -22,0 +27,0 @@

import { TemplatePath } from "@11ty/eleventy-utils";
import { DepGraph } from "dependency-graph";
import { mergeGraphs } from "@11ty/dependency-tree-esm";

@@ -77,3 +78,3 @@ import JavaScriptDependencies from "./Util/JavaScriptDependencies.js";

static normalize(targets) {
static toArray(targets) {
if (!targets) {

@@ -90,7 +91,7 @@ return [];

add(targets) {
this.addRaw(WatchTargets.normalize(targets));
this.addRaw(WatchTargets.toArray(targets));
}
static normalizeToGlobs(targets) {
return WatchTargets.normalize(targets).map((entry) =>
return WatchTargets.toArray(targets).map((entry) =>
TemplatePath.convertToRecursiveGlobSync(entry),

@@ -106,12 +107,33 @@ );

targets = WatchTargets.normalize(targets);
let deps = await JavaScriptDependencies.getDependencies(targets, this.isEsm);
targets = WatchTargets.toArray(targets);
let cjsDeps = Array.from(
await JavaScriptDependencies.getCommonJsDependencies(targets, this.isEsm),
);
if (filterCallback) {
deps = deps.filter(filterCallback);
cjsDeps = cjsDeps.filter(filterCallback);
}
for (let target of targets) {
this.addToDependencyGraph(target, deps);
this.addToDependencyGraph(target, cjsDeps);
}
this.addRaw(deps, true);
this.addRaw(cjsDeps, true);
// https://github.com/11ty/eleventy/issues/3899
// Note that this fix is ESM-only, dependency-tree CJS doesn’t support returning graphs (yet?)
let esmGraph = await JavaScriptDependencies.getEsmGraph(targets, this.isEsm);
if (filterCallback) {
for (let node of esmGraph.overallOrder()) {
if (!filterCallback(node)) {
esmGraph.removeNode(node);
}
}
}
mergeGraphs(this.graph, esmGraph);
// ESM graph includes original targets, which we do not want for addRaw so we’ll remove them before adding
let rawEsmGraph = esmGraph.clone();
for (let t of targets) {
rawEsmGraph.removeNode(t);
}
this.addRaw(rawEsmGraph.overallOrder(), true);
}

@@ -140,6 +162,5 @@

// Use GlobalDependencyMap
if (this.#templateConfig) {
for (let dep of this.#templateConfig.usesGraph.getDependantsFor(filePath)) {
paths.add(dep);
}
let dependantsMapped = this.#templateConfig?.usesGraph.getDependantsFor(filePath) || [];
for (let dep of dependantsMapped) {
paths.add(dep);
}

@@ -146,0 +167,0 @@ }

import { spawnAsync } from "./spawn.js";
async function getGitFirstAddedTimeStamp(filePath) {
try {
let timestamp = await spawnAsync(
"git",
// Formats https://www.git-scm.com/docs/git-log#_pretty_formats
// %at author date, UNIX timestamp
["log", "--diff-filter=A", "--follow", "-1", "--format=%at", filePath],
);
return parseInt(timestamp, 10) * 1000;
} catch (e) {
// do nothing
}
}
// return a Date
export default async function (inputPath) {
let timestamp = await getGitFirstAddedTimeStamp(inputPath);
if (timestamp) {
return new Date(timestamp);
}
}
import { spawnAsync } from "./spawn.js";
async function getGitLastUpdatedTimeStamp(filePath) {
try {
let timestamp = await spawnAsync(
"git",
// Formats https://www.git-scm.com/docs/git-log#_pretty_formats
// %at author date, UNIX timestamp
["log", "-1", "--format=%at", filePath],
);
return parseInt(timestamp, 10) * 1000;
} catch (e) {
// do nothing
}
}
// return a Date
export default async function (inputPath) {
let timestamp = await getGitLastUpdatedTimeStamp(inputPath);
if (timestamp) {
return new Date(timestamp);
}
}
function setUnion(...sets) {
let root = new Set();
for (let set of sets) {
for (let entry of set) {
root.add(entry);
}
}
return root;
}
export { setUnion };
export default function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (e) {
// invalid url OR local path
return false;
}
}