Skip to content

Commit 2d5ab74

Browse files
committed
Fixes #3536
1 parent 72d66c9 commit 2d5ab74

9 files changed

Lines changed: 138 additions & 37 deletions

File tree

src/Data/TemplateData.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getEleventyPackageJson, getWorkingProjectPackageJson } from "../Util/Im
1212
import { EleventyImport, EleventyLoadContent } from "../Util/Require.js";
1313
import { DeepFreeze } from "../Util/Objects/DeepFreeze.js";
1414
import { coerce } from "../Util/SemverCoerce.js";
15+
import ReservedData from "../Util/ReservedData.js";
1516

1617
const { set: lodashSet, get: lodashGet } = lodash;
1718

@@ -311,6 +312,10 @@ class TemplateData {
311312
dataFileConflicts[objectPathTargetString] = files[j];
312313
debug(`Found global data file ${files[j]} and adding as: ${objectPathTarget}`);
313314
lodashSet(globalData, objectPathTarget, data);
315+
316+
if (this.config.freezeReservedData) {
317+
ReservedData.check(globalData, files[j]);
318+
}
314319
}
315320

316321
return globalData;

src/Data/TemplateDataInitialGlobalData.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import lodash from "@11ty/lodash-custom";
22

3+
import ReservedData from "../Util/ReservedData.js";
34
import EleventyBaseError from "../Errors/EleventyBaseError.js";
45

56
const { set: lodashSet } = lodash;
@@ -33,6 +34,11 @@ class TemplateDataInitialGlobalData {
3334
}
3435
}
3536

37+
if (this.config.freezeReservedData) {
38+
// TODO-ish might come from the `config` callback too
39+
ReservedData.check(globalData, this.templateConfig.getActiveConfigPath());
40+
}
41+
3642
return globalData;
3743
}
3844
}

src/Template.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ class Template extends TemplateContent {
386386

387387
if (this.templateData) {
388388
localData = await this.templateData.getTemplateDirectoryData(this.inputPath);
389-
globalData = await this.templateData.getGlobalData(this.inputPath);
389+
globalData = await this.templateData.getGlobalData();
390390
debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath);
391391
}
392392

@@ -413,7 +413,7 @@ class Template extends TemplateContent {
413413
let mergedData = Merge({}, globalData, mergedLayoutData, localData, frontMatterData);
414414

415415
if (this.config.freezeReservedData) {
416-
ReservedData.check(mergedData);
416+
ReservedData.checkSubset(mergedData);
417417
}
418418

419419
await this.addPage(mergedData);
@@ -422,18 +422,10 @@ class Template extends TemplateContent {
422422

423423
return mergedData;
424424
} catch (e) {
425-
if (
426-
ReservedData.isReservedDataError(e) ||
427-
(e instanceof TypeError &&
428-
e.message.startsWith("Cannot add property") &&
429-
e.message.endsWith("not extensible"))
430-
) {
431-
throw new EleventyBaseError(
432-
`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://v3.11ty.dev/docs/data-eleventy-supplied/`,
433-
e,
434-
);
425+
// if ReservedDataError, defer to that (from ReservedData.checkSubset above)
426+
if (!ReservedData.isReservedDataError(e) && ReservedData.isFrozenError(e)) {
427+
throw ReservedData.getError({ cause: e });
435428
}
436-
437429
throw e;
438430
}
439431
}
@@ -628,7 +620,7 @@ class Template extends TemplateContent {
628620

629621
// Check for reserved properties in computed data
630622
if (this.config.freezeReservedData) {
631-
ReservedData.check(data[this.config.keys.computed]);
623+
ReservedData.checkSubset(data[this.config.keys.computed]);
632624
}
633625

634626
// actually add the computed data

src/TemplateLayout.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,9 @@ class TemplateLayout extends TemplateContent {
257257
let cdata = new CdataWrapper(pageTemplateSyntax, layoutTemplateSyntax);
258258

259259
let data = {
260-
content: cdata.wrap(templateContent),
261260
...pageEntry.data,
261+
// This should come *after* data, so `content` have override `content` props set in data cascade
262+
content: cdata.wrap(templateContent),
262263
};
263264

264265
templateContent = cdata.unwrap(await render(data));

src/Util/ReservedData.js

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
class EleventyReservedDataError extends TypeError {}
22

33
class ReservedData {
4+
static fullProperties = [
5+
"pkg", // Object.freeze’d upstream
6+
"eleventy", // Object.freeze’d upstream
7+
// "page" is only frozen for specific subproperties below
8+
"content",
9+
"collections",
10+
];
11+
412
static properties = [
5-
// "pkg", // Object.freeze’d upstream
6-
// "eleventy", // Object.freeze’d upstream
713
// "page" is only frozen for specific subproperties below
814
"content",
915
"collections",
@@ -22,12 +28,12 @@ class ReservedData {
2228
];
2329

2430
// Check in the data cascade for reserved data properties.
25-
static getReservedKeys(data) {
31+
static getReservedKeys(data, globalProperties = this.fullProperties) {
2632
if (!data) {
2733
return [];
2834
}
2935

30-
let keys = this.properties.filter((key) => {
36+
let keys = globalProperties.filter((key) => {
3137
return key in data;
3238
});
3339

@@ -46,19 +52,50 @@ class ReservedData {
4652
return keys;
4753
}
4854

49-
static check(data) {
50-
let reserved = ReservedData.getReservedKeys(data);
51-
if (reserved.length === 0) {
55+
static #check(data, sourceLocation, propertiesList) {
56+
let reservedNames = ReservedData.getReservedKeys(data, propertiesList);
57+
if (reservedNames.length === 0) {
5258
return;
5359
}
60+
throw this.getError({
61+
reservedNames,
62+
sourceLocation,
63+
});
64+
}
65+
66+
// check for frozen objects too
67+
static check(data, sourceLocation) {
68+
this.#check(data, sourceLocation, this.fullProperties);
69+
}
5470

55-
let error = new EleventyReservedDataError(
56-
`Cannot override reserved Eleventy properties: ${reserved.join(", ")}`,
71+
static checkSubset(data, sourceLocation) {
72+
this.#check(data, sourceLocation, this.properties);
73+
}
74+
75+
static getError(options = {}) {
76+
let { reservedNames, cause, sourceLocation } = options || {};
77+
78+
if (cause) {
79+
reservedNames ??= cause.reservedNames;
80+
}
81+
82+
let e = new EleventyReservedDataError(
83+
`You attempted to set one of Eleventy’s reserved data property names${reservedNames ? `: ${reservedNames.join(", ")}` : ""}${sourceLocation ? ` (source: ${sourceLocation})` : ""}. 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://v3.11ty.dev/docs/data-eleventy-supplied/`,
84+
{ cause },
5785
);
5886

59-
error.reservedNames = reserved;
87+
if (reservedNames) {
88+
e.reservedNames = reservedNames;
89+
}
90+
return e;
91+
}
6092

61-
throw error;
93+
static isFrozenError(e) {
94+
return (
95+
e instanceof TypeError &&
96+
e.message.startsWith("Cannot add property") &&
97+
e.message.endsWith("not extensible")
98+
);
6299
}
63100

64101
static isReservedDataError(e) {

test/EleventyTest.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ eleventy:
965965
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://v3.11ty.dev/docs/data-eleventy-supplied/'
966966
});
967967

968-
t.is(e.originalError.toString(), "TypeError: Cannot add property key1, object is not extensible");
968+
t.is(e.cause.toString(), "TypeError: Cannot add property key1, object is not extensible");
969969
});
970970

971971
test("Eleventy setting reserved data throws error (pkg)", async (t) => {
@@ -983,7 +983,7 @@ pkg:
983983
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://v3.11ty.dev/docs/data-eleventy-supplied/'
984984
});
985985

986-
t.is(e.originalError.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
986+
t.is(e.cause.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
987987
});
988988

989989
test("Eleventy pagination works okay with reserved data throws (eleventy) Issue #3262", async (t) => {
@@ -1020,8 +1020,6 @@ page: "My page value"
10201020
let e = await t.throwsAsync(() => elev.toJSON(), {
10211021
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://v3.11ty.dev/docs/data-eleventy-supplied/'
10221022
});
1023-
1024-
t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: page");
10251023
});
10261024

10271025
test("Eleventy setting reserved data throws error (content)", async (t) => {
@@ -1034,11 +1032,9 @@ content: "My page value"
10341032
});
10351033
elev.disableLogger();
10361034

1037-
let e = await t.throwsAsync(() => elev.toJSON(), {
1035+
await t.throwsAsync(() => elev.toJSON(), {
10381036
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://v3.11ty.dev/docs/data-eleventy-supplied/'
10391037
});
1040-
1041-
t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: content");
10421038
});
10431039

10441040
test("Eleventy setting reserved data throws error (collections)", async (t) => {
@@ -1051,11 +1047,9 @@ collections: []
10511047
});
10521048
elev.disableLogger();
10531049

1054-
let e = await t.throwsAsync(() => elev.toJSON(), {
1050+
await t.throwsAsync(() => elev.toJSON(), {
10551051
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://v3.11ty.dev/docs/data-eleventy-supplied/'
10561052
});
1057-
1058-
t.is(e.originalError.toString(), "TypeError: Cannot override reserved Eleventy properties: collections");
10591053
});
10601054

10611055
test("Eleventy setting pkg data is okay when pkg is remapped to parkour", async (t) => {
@@ -1143,7 +1137,7 @@ parkour:
11431137
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://v3.11ty.dev/docs/data-eleventy-supplied/'
11441138
});
11451139

1146-
t.is(e.originalError.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
1140+
t.is(e.cause.toString(), "TypeError: Cannot add property myOwn, object is not extensible");
11471141
});
11481142

11491143
test("Eleventy data schema (success) #879", async (t) => {

test/ReservedDataTest.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import test from "ava";
22
import ReservedData from "../src/Util/ReservedData.js";
3+
import Eleventy from "../src/Eleventy.js";
34

45
test("No reserved Keys", t => {
56
t.deepEqual(ReservedData.getReservedKeys({ key: {} }).sort(), []);
@@ -24,3 +25,61 @@ test("`page` subkeys", t => {
2425
t.deepEqual(ReservedData.getReservedKeys({ page: { outputPath: "", otherkey: "" } }).sort(), ["page.outputPath"]);
2526
t.deepEqual(ReservedData.getReservedKeys({ page: { date: "", outputPath: "", otherkey: "" } }).sort(), ["page.date", "page.outputPath"]);
2627
});
28+
29+
test("Eleventy freeze data set via config API throws error (page)", async (t) => {
30+
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
31+
configPath: "./test/stubs-virtual/eleventy.config.js",
32+
config: eleventyConfig => {
33+
eleventyConfig.addGlobalData("page", "lol no");
34+
eleventyConfig.addTemplate("index.html", ``);
35+
}
36+
});
37+
elev.disableLogger();
38+
39+
await t.throwsAsync(() => elev.toJSON(), {
40+
message: 'You attempted to set one of Eleventy’s reserved data property names: page (source: ./test/stubs-virtual/eleventy.config.js). 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://v3.11ty.dev/docs/data-eleventy-supplied/'
41+
});
42+
});
43+
44+
test("Eleventy freeze data set via config API throws error (eleventy)", async (t) => {
45+
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
46+
configPath: "./test/stubs-virtual/eleventy.config.js",
47+
config: eleventyConfig => {
48+
eleventyConfig.addGlobalData("eleventy", "lol no");
49+
eleventyConfig.addTemplate("index.html", ``);
50+
}
51+
});
52+
elev.disableLogger();
53+
54+
await t.throwsAsync(() => elev.toJSON(), {
55+
message: 'You attempted to set one of Eleventy’s reserved data property names: eleventy (source: ./test/stubs-virtual/eleventy.config.js). 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://v3.11ty.dev/docs/data-eleventy-supplied/'
56+
});
57+
});
58+
59+
test("Eleventy freeze data set global data file throws error (page)", async (t) => {
60+
let elev = new Eleventy({
61+
input: "./test/stubs-freeze/page/",
62+
config: eleventyConfig => {
63+
eleventyConfig.addTemplate("index.html", ``);
64+
}
65+
});
66+
elev.disableLogger();
67+
68+
await t.throwsAsync(() => elev.toJSON(), {
69+
message: 'You attempted to set one of Eleventy’s reserved data property names: page.url (source: ./test/stubs-freeze/page/_data/page.js). 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://v3.11ty.dev/docs/data-eleventy-supplied/'
70+
});
71+
});
72+
73+
test("Eleventy freeze data set global data file throws error (eleventy)", async (t) => {
74+
let elev = new Eleventy({
75+
input: "./test/stubs-freeze/eleventy/",
76+
config: eleventyConfig => {
77+
eleventyConfig.addTemplate("index.html", ``);
78+
}
79+
});
80+
elev.disableLogger();
81+
82+
await t.throwsAsync(() => elev.toJSON(), {
83+
message: 'You attempted to set one of Eleventy’s reserved data property names: eleventy (source: ./test/stubs-freeze/eleventy/_data/eleventy.js). 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://v3.11ty.dev/docs/data-eleventy-supplied/'
84+
});
85+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
"key": "value", // not allowed
3+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
"key": "value", // allowed
3+
"url": "lksjdklfjlskdjf", // not allowed
4+
};

0 commit comments

Comments
 (0)