Skip to content

Commit

Permalink
Throw errors on content, page, collections in data cascade.
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Apr 12, 2024
1 parent 93cdaa5 commit 70df967
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 58 deletions.
5 changes: 3 additions & 2 deletions src/Eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class Eleventy {
this._hasConfigInitialized = false;
}

async initializeConfig() {
async initializeConfig(initOverrides) {
if (!this.eleventyConfig) {
this.eleventyConfig = new TemplateConfig(null, this.options.configPath);
} else if (this.options.configPath) {
Expand Down Expand Up @@ -160,7 +160,7 @@ class Eleventy {
this.initializeEnvironmentVariables(this.env);

// Async initialization of configuration
await this.eleventyConfig.init();
await this.eleventyConfig.init(initOverrides);

/**
* @member {Object} - Initialize Eleventy’s configuration, including the user config file
Expand Down Expand Up @@ -428,6 +428,7 @@ class Eleventy {
await this.config.events.emit("eleventy.extensionmap", this.extensionMap);

// eleventyServe is always available, even when not in --serve mode
// TODO directorynorm
this.eleventyServe.setOutputDir(this.outputDir);

// TODO
Expand Down
1 change: 1 addition & 0 deletions src/EleventyServe.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class EleventyServe {
}
}

// TODO directorynorm
setOutputDir(outputDir) {
// TODO check if this is different and if so, restart server (if already running)
// This applies if you change the output directory in your config file during watch/serve
Expand Down
30 changes: 25 additions & 5 deletions src/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import TemplateBehavior from "./TemplateBehavior.js";
import TemplateContentPrematureUseError from "./Errors/TemplateContentPrematureUseError.js";
import TemplateContentUnrenderedTemplateError from "./Errors/TemplateContentUnrenderedTemplateError.js";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import ReservedData from "./Util/ReservedData.js";

const { set: lodashSet, get: lodashGet } = lodash;
const writeFile = util.promisify(fs.writeFile);
Expand All @@ -31,6 +32,7 @@ const debug = debugUtil("Eleventy:Template");
const debugDev = debugUtil("Dev:Eleventy:Template");

class EleventyTransformError extends EleventyBaseError {}
class EleventyReservedDataError extends TypeError {}

class Template extends TemplateContent {
constructor(templatePath, templateData, extensionMap, config) {
Expand Down Expand Up @@ -253,7 +255,8 @@ class Template extends TemplateContent {
async usePermalinkRoot() {
if (this._usePermalinkRoot === undefined) {
// TODO this only works with immediate front matter and not data files
this._usePermalinkRoot = (await this.getFrontMatterData())[this.config.keys.permalinkRoot];
let { data } = await this.getFrontMatterData();
this._usePermalinkRoot = data[this.config.keys.permalinkRoot];
}

return this._usePermalinkRoot;
Expand Down Expand Up @@ -305,7 +308,8 @@ class Template extends TemplateContent {
}

async _testGetAllLayoutFrontMatterData() {
let frontMatterData = await this.getFrontMatterData();
let { data: frontMatterData } = await this.getFrontMatterData();

if (frontMatterData[this.config.keys.layout]) {
let layout = this.getLayout(frontMatterData[this.config.keys.layout]);
return await layout.getData();
Expand All @@ -328,7 +332,7 @@ class Template extends TemplateContent {
debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath);
}

let frontMatterData = await this.getFrontMatterData();
let { data: frontMatterData, excerpt } = await this.getFrontMatterData();
let layoutKey =
frontMatterData[this.config.keys.layout] ||
localData[this.config.keys.layout] ||
Expand All @@ -353,16 +357,32 @@ class Template extends TemplateContent {
frontMatterData,
);

let reserved = this.config.freezeReservedData ? ReservedData.getReservedKeys(mergedData) : [];
if (reserved.length > 0) {
let e = new EleventyReservedDataError(
`Cannot override reserved Eleventy properties: ${reserved.join(", ")}`,
);
e.reservedNames = reserved;
throw e;
}

this.addExcerpt(mergedData, excerpt);
mergedData = await this.addPageDate(mergedData);
mergedData = this.addPageData(mergedData);
debugDev("%o getData mergedData", this.inputPath);

this._dataCache = mergedData;

return mergedData;
} catch (e) {
if (e instanceof TypeError) {
if (
e instanceof EleventyReservedDataError ||
(e instanceof TypeError &&
e.message.startsWith("Cannot add property") &&
e.message.endsWith("not extensible"))
) {
throw new EleventyBaseError(
"Error with reserved data in Eleventy. Use `eleventyConfig.setFreezeReservedData(false)` or remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, `content`, `page`, `collections`). Read more: https://www.11ty.dev/docs/data-eleventy-supplied/",
`You attempted to set one of Eleventy’s reserved data property names${e.reservedNames ? `: ${e.reservedNames.join(", ")}` : ""}. You can opt-out of this behavior with \`eleventyConfig.setFreezeReservedData(false)\` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. \`eleventy\`, \`pkg\`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/`,
e,
);
}
Expand Down
36 changes: 26 additions & 10 deletions src/TemplateContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ class TemplateContent {
get frontMatter() {
if (this.frontMatterOverride) {
return this.frontMatterOverride;
} else if (this._frontMatter) {
return this._frontMatter;
} else {
throw new Error(
"Unfortunately you’re using code that monkey patched some Eleventy internals and it isn’t async-friendly. Change your code to use the async `read()` method on the template instead!",
Expand Down Expand Up @@ -182,6 +180,16 @@ class TemplateContent {
return this.config.virtualTemplates[inputDirRelativeInputPath];
}

addExcerpt(data, excerpt) {
if (!data || !excerpt) {
return;
}

// alias, defaults to page.excerpt
let alias = this.config.frontMatterParsingOptions?.excerpt_alias || "page.excerpt";
lodashSet(data, alias, excerpt);
}

async read() {
if (!this.readingPromise) {
if (!this.inputContent) {
Expand Down Expand Up @@ -224,14 +232,14 @@ class TemplateContent {
}

// alias, defaults to page.excerpt
let alias = options.excerpt_alias || "page.excerpt";
lodashSet(fm.data, alias, fm.excerpt);
// let alias = options.excerpt_alias || "page.excerpt";
// lodashSet(fm.data, alias, fm.excerpt);
}

// For monkey patchers that used `frontMatter` 🤧
// https:/11ty/eleventy/issues/613#issuecomment-999637109
// https:/11ty/eleventy/issues/2710#issuecomment-1373854834
this._frontMatter = fm;
// Removed this._frontMatter monkey patcher help in 3.0.0-alpha.7

This comment has been minimized.

Copy link
@uncenter

uncenter Apr 29, 2024

Contributor

😢


resolve(fm);
} else {
Expand Down Expand Up @@ -303,9 +311,9 @@ class TemplateContent {
return content;
}

// This might only be used in tests
async getFrontMatter() {
async _testGetFrontMatter() {
let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();

return fm;
}

Expand All @@ -321,6 +329,11 @@ class TemplateContent {
try {
let fm = await this.read();

// gray-matter isn’t async-friendly but can return a promise from custom front matter
if (fm.data instanceof Promise) {
fm.data = await fm.data;
}

let extraData = await this.engine.getExtraDataFromFile(this.inputPath);

let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
Expand All @@ -330,9 +343,12 @@ class TemplateContent {
}

let data = TemplateData.mergeDeep(false, fm.data, extraData, virtualTemplateData);

let cleanedData = TemplateData.cleanupData(data);
resolve(cleanedData);

resolve({
data: cleanedData,
excerpt: fm.excerpt,
});
} catch (e) {
reject(e);
}
Expand All @@ -343,7 +359,7 @@ class TemplateContent {
}

async getEngineOverride() {
let frontMatterData = await this.getFrontMatterData();
let { data: frontMatterData } = await this.getFrontMatterData();
return frontMatterData[this.config.keys.engineOverride];
}

Expand Down
3 changes: 2 additions & 1 deletion src/TemplateLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ class TemplateLayout extends TemplateContent {
}

async getTemplateLayoutMapEntry() {
let { data: frontMatterData } = await this.getFrontMatterData();
return {
// Used by `TemplateLayout.getTemplate()`
key: this.dataKeyLayoutPath,

// used by `this.getData()`
frontMatterData: await this.getFrontMatterData(),
frontMatterData,
};
}

Expand Down
11 changes: 11 additions & 0 deletions src/Util/ReservedData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class ReservedData {
static properties = ["page", "content", "collections"];

static getReservedKeys(data) {
return this.properties.filter((key) => {
return key in data;
});
}
}

export default ReservedData;
130 changes: 126 additions & 4 deletions test/EleventyTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -950,22 +950,144 @@ test("Eleventy config export (CommonJS)", async (t) => {
});

test("Eleventy setting reserved data throws error (eleventy)", async (t) => {
let elev = new Eleventy("./test/stubs-reserved-data/");
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
eleventy:
key1: NOOOOO
---`);
}
});
elev.disableLogger();

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'Error with reserved data in Eleventy. Use `eleventyConfig.setFreezeReservedData(false)` or remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, `content`, `page`, `collections`). Read more: https://www.11ty.dev/docs/data-eleventy-supplied/'
message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot add property key1, object is not extensible");
});

test("Eleventy setting reserved data throws error (pkg)", async (t) => {
let elev = new Eleventy("./test/stubs-reserved-data-pkg/");
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
pkg:
myOwn: OVERRIDE
---`);
}
});
elev.disableLogger();

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
});

test("Eleventy setting reserved data throws error (page)", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
page: "My page value"
---`)
}
});
elev.disableLogger();

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'You attempted to set one of Eleventy’s reserved data property names: page. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: page");
});

test("Eleventy setting reserved data throws error (content)", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
content: "My page value"
---`)
}
});
elev.disableLogger();

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'You attempted to set one of Eleventy’s reserved data property names: content. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: content");
});

test("Eleventy setting reserved data throws error (collections)", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
collections: []
---`)
}
});
elev.disableLogger();

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'You attempted to set one of Eleventy’s reserved data property names: collections. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: collections");
});

test("Eleventy setting pkg data is okay when pkg is remapped to parkour", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
pkg:
myOwn: OVERRIDE
---`);
}
});
elev.disableLogger();

await elev.initializeConfig({
keys: {
package: "parkour"
}
});

// Remap successful
t.is(elev.eleventyConfig.config.keys.package, "parkour");

let [result] = await elev.toJSON();
t.deepEqual(result, {
content: "",
inputPath: "./test/stubs-virtual/index.html",
outputPath: "./_site/index.html",
rawInput: "",
url: "/"
});
});

test("Eleventy setting reserved data throws error (pkg remapped to parkour)", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addTemplate("index.html", `---
parkour:
myOwn: OVERRIDE
---`);
}
});
elev.disableLogger();

await elev.initializeConfig({
keys: {
package: "parkour"
}
});

// Remap successful
t.is(elev.eleventyConfig.config.keys.package, "parkour");

let e = await t.throwsAsync(() => elev.toJSON(), {
message: 'Error with reserved data in Eleventy. Use `eleventyConfig.setFreezeReservedData(false)` or remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, `content`, `page`, `collections`). Read more: https://www.11ty.dev/docs/data-eleventy-supplied/'
message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://www.11ty.dev/docs/data-eleventy-supplied/'
});

t.is(e.originalError.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
Expand Down
4 changes: 2 additions & 2 deletions test/JavaScriptFrontMatterTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Eleventy from "../src/Eleventy.js";

test("Custom Front Matter Parsing Options (using JavaScript node-retrieve-globals)", async (t) => {
let elev = new Eleventy("./test/stubs/script-frontmatter/test.njk", "./_site");
elev.setIsVerbose(false);
elev.disableLogger();

let result = await elev.toJSON();

Expand All @@ -20,7 +20,7 @@ test("Custom Front Matter Parsing Options (using JavaScript node-retrieve-global
});
},
});
elev.setIsVerbose(false);
elev.disableLogger();

let result = await elev.toJSON();

Expand Down
Loading

0 comments on commit 70df967

Please sign in to comment.