├── .gitignore
├── sample
├── css
│ ├── footer.css
│ └── header.css
├── other-page.md
├── sample-page.md
├── _includes
│ └── layout.njk
└── .eleventy.js
├── AssetManager.js
├── README.md
├── package.json
├── InlineCodeManager.js
└── test
└── InlineCodeManagerTest.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | _site
--------------------------------------------------------------------------------
/sample/css/footer.css:
--------------------------------------------------------------------------------
1 | footer {
2 | color: #112/*ty*/;
3 | }
--------------------------------------------------------------------------------
/sample/css/header.css:
--------------------------------------------------------------------------------
1 | header {
2 | color: #111/*ty*/;
3 | }
--------------------------------------------------------------------------------
/sample/other-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layout.njk
3 | ---
4 | {% usingComponent "header" %}
5 |
--------------------------------------------------------------------------------
/sample/sample-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layout.njk
3 | ---
4 | {% usingComponent "header" %}
5 |
6 | {% usingComponent "footer" %}
7 |
--------------------------------------------------------------------------------
/AssetManager.js:
--------------------------------------------------------------------------------
1 | const InlineCodeManager = require("./InlineCodeManager");
2 |
3 | // Placeholder for other types later
4 | class AssetManager {}
5 |
6 | module.exports = AssetManager;
7 | module.exports.InlineCodeManager = InlineCodeManager;
--------------------------------------------------------------------------------
/sample/_includes/layout.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 | {{ content | safe }}
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Eleventy Assets
2 |
3 | _⚠️ This utility is retired and [superseded by the Eleventy Bundle Plugin](https://github.com/11ty/eleventy-plugin-bundle)._
4 |
5 | ---
6 |
7 | Code to help manage assets in your Eleventy project. This is not an `addPlugin` compatible Eleventy plugin. It is an npm package for use in your config or other plugins.
8 |
9 | Currently supported features:
10 |
11 | * Generate and inline code-split CSS specific to individual pages.
12 | * Can work as a standalone implementation (check out the `./sample/` directory) or in tandem with [`eleventy-plugin-vue`](https://github.com/11ty/eleventy-plugin-vue/).
13 |
14 | ## Installation
15 |
16 | ```sh
17 | npm install @11ty/eleventy-assets
18 | ```
19 |
20 | ## Usage
21 |
22 | See the `./sample/` directory for an example implementation.
23 |
24 | * A `usingComponent` shortcode to log component use in each template.
25 | * A `getCSS` filter for use in layout templates to output the code-split CSS for the current URL (only).
26 | * Component CSS is stored in `./sample/css/`
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@11ty/eleventy-assets",
3 | "version": "1.0.6",
4 | "description": "Manage per-route code (CSS, SVG, JS) in Eleventy.",
5 | "publishConfig": {
6 | "access": "public"
7 | },
8 | "main": "AssetManager.js",
9 | "scripts": {
10 | "test": "ava",
11 | "sample": "cd sample && eleventy"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/11ty/eleventy-assets.git"
16 | },
17 | "funding": {
18 | "type": "opencollective",
19 | "url": "https://opencollective.com/11ty"
20 | },
21 | "keywords": [
22 | "eleventy"
23 | ],
24 | "author": {
25 | "name": "Zach Leatherman",
26 | "email": "zachleatherman@gmail.com",
27 | "url": "https://zachleat.com/"
28 | },
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/11ty/eleventy-assets/issues"
32 | },
33 | "homepage": "https://github.com/11ty/eleventy-assets#readme",
34 | "devDependencies": {
35 | "ava": "^3.15.0"
36 | },
37 | "dependencies": {
38 | "debug": "^4.3.1",
39 | "dependency-graph": "^0.9.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/sample/.eleventy.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const { InlineCodeManager } = require("../");
3 |
4 | function getCssFilePath(componentName) {
5 | return `./css/${componentName}.css`;
6 | }
7 |
8 | module.exports = function(eleventyConfig) {
9 | let cssManager = new InlineCodeManager();
10 |
11 | eleventyConfig.addShortcode("usingComponent", function(componentName) {
12 | // If a never before seen component, add the CSS code
13 | if(!cssManager.hasComponentCode(componentName)) {
14 | // You could read this file asynchronously too if you use an Asynchronous Shortcode
15 | // Read more: https://www.11ty.dev/docs/shortcodes/
16 | let componentCss = fs.readFileSync(getCssFilePath(componentName), { encoding: "UTF-8" });
17 | cssManager.addComponentCode(componentName, componentCss);
18 | }
19 |
20 | // Log usage for this url
21 | // this.page.url is supported on Eleventy 0.11.0 and newer
22 | cssManager.addComponentForUrl(componentName, this.page.url);
23 |
24 | return "";
25 | });
26 |
27 | // This needs to be called in a Layout template.
28 | eleventyConfig.addFilter("getCss", (url) => {
29 | return cssManager.getCodeForUrl(url);
30 | });
31 |
32 | eleventyConfig.addWatchTarget("./css/*.css");
33 |
34 | // This will tell the `hasComponentCode` check above to reload the
35 | // component CSS to pick up any new changes.
36 |
37 | // `beforeWatch` is supported on Eleventy 0.11.0-beta.3 and newer
38 | eleventyConfig.on("beforeWatch", () => {
39 | cssManager.resetComponentCode();
40 | });
41 |
42 | return {
43 | markdownTemplateEngine: "njk"
44 | }
45 | };
--------------------------------------------------------------------------------
/InlineCodeManager.js:
--------------------------------------------------------------------------------
1 | const DependencyGraph = require("dependency-graph").DepGraph;
2 | const debug = require("debug")("EleventyAssets")
3 |
4 | class InlineCodeManager {
5 | constructor() {
6 | this.urlKeyPrefix = "11ty_URL_KEY::";
7 | this.init();
8 | this.comments = {
9 | pre: "/*",
10 | post: "*/"
11 | };
12 | }
13 |
14 | init() {
15 | debug("Initializing a new dependency graph and new code map");
16 | this.graph = new DependencyGraph();
17 | this.code = {};
18 | }
19 |
20 | setCommentSyntax(pre, post) {
21 | this.comments.pre = pre;
22 | this.comments.post = post;
23 | }
24 |
25 | _isUrlKey(key) {
26 | return key.startsWith(this.urlKeyPrefix);
27 | }
28 |
29 | _normalizeUrlKey(url) {
30 | if(url) {
31 | return this.urlKeyPrefix + url;
32 | }
33 | }
34 |
35 | static getComponentNameFromPath(filePath, fileExtension) {
36 | filePath = filePath.split("/").pop();
37 | return fileExtension ? filePath.substr(0, filePath.lastIndexOf(fileExtension)) : filePath;
38 | }
39 |
40 | hasComponent(componentName) {
41 | return this.graph.hasNode(componentName);
42 | }
43 |
44 | addComponentForUrl(componentName, url) {
45 | this._addDependency(componentName, this._normalizeUrlKey(url));
46 | }
47 |
48 | addComponentRelationship(parent, child) {
49 | this._addDependency(child, parent);
50 | }
51 |
52 | _addDependency(from, to) {
53 | if(from && to) {
54 | debug("Adding dependency from %o to %o", from, to);
55 | if(!this.graph.hasNode(from)) {
56 | this.graph.addNode(from);
57 | }
58 | if(!this.graph.hasNode(to)) {
59 | this.graph.addNode(to);
60 | }
61 | this.graph.addDependency(from, to);
62 | }
63 | }
64 |
65 | /* Deprecated */
66 | // strips file extensions
67 | addRawComponentRelationship(parentComponentFile, childComponentFile, fileExtension) {
68 | let parentName = InlineCodeManager.getComponentNameFromPath(parentComponentFile, fileExtension);
69 | let childName = InlineCodeManager.getComponentNameFromPath(childComponentFile, fileExtension);
70 |
71 | this.addComponentRelationship(parentName, childName);
72 | }
73 |
74 | getComponentListForComponent(componentName) {
75 | return this._getComponentList(componentName);
76 | }
77 |
78 | getComponentListForUrl(url) {
79 | let urlKey = this._normalizeUrlKey(url);
80 | return this._getComponentList(urlKey);
81 | }
82 |
83 | getRelevantComponentListForUrl(url) {
84 | let list = this.getComponentListForUrl(url);
85 | return list.filter(componentName => this.hasComponentCode(componentName));
86 | }
87 |
88 | _getComponentList(key) {
89 | if(!this.graph.hasNode(key)) {
90 | return [];
91 | }
92 | return this.graph.dependantsOf(key).filter(key => !this._isUrlKey(key));
93 | }
94 |
95 | _getUrlList() {
96 | return this.graph.overallOrder().filter(key => this._isUrlKey(key));
97 | }
98 |
99 | // only active components in use on urls
100 | getFullComponentList() {
101 | let list = new Set();
102 | let urls = this._getUrlList();
103 | for(let normalizedUrlKey of urls) {
104 | let components = this._getComponentList(normalizedUrlKey);
105 | for(let name of components) {
106 | list.add(name);
107 | }
108 | }
109 | return Array.from(list);
110 | }
111 |
112 | /* Deprecated */
113 | /* styleNodes come from `rollup-plugin-css-only`->output */
114 | addRollupComponentNodes(styleNodes, fileExtension) {
115 | for(let path in styleNodes) {
116 | let componentName = InlineCodeManager.getComponentNameFromPath(path, fileExtension);
117 | this.addComponentCode(componentName, styleNodes[path]);
118 | }
119 | }
120 |
121 | resetComponentCode() {
122 | this.code = {};
123 | }
124 |
125 | resetComponentCodeFor(componentName) {
126 | this.code[componentName] = new Set();
127 | }
128 |
129 | resetForUrl(url) {
130 | let urlKey = this._normalizeUrlKey(url);
131 | if(!this.graph.hasNode(urlKey)) {
132 | return;
133 | }
134 | let deps = this.graph.dependantsOf(urlKey);
135 | debug("Resetting for url %o: %o dependencies to remove", url, deps.length)
136 |
137 | for(let dep of deps) {
138 | this.graph.removeDependency(dep, urlKey);
139 | }
140 | }
141 |
142 | hasComponentCode(componentName) {
143 | let codeSet = this.code[componentName] || new Set();
144 | let hasNonEmptyCode = false;
145 | for(let code of codeSet) {
146 | if(!!code) {
147 | hasNonEmptyCode = true;
148 | }
149 | }
150 | return hasNonEmptyCode;
151 | }
152 |
153 | addComponentCode(componentName, code) {
154 | if(!this.code[componentName]) {
155 | this.code[componentName] = new Set();
156 | }
157 | this.code[componentName].add(code);
158 | }
159 |
160 | getComponentCode(componentName) {
161 | if(this.code[componentName]) {
162 | return Array.from(this.code[componentName]).map(entry => entry.trim());
163 | }
164 | return [];
165 | }
166 |
167 | // TODO add priority level for components and only inline the ones that are above a priority level
168 | // Maybe high priority corresponds with how high on the page the component is used
169 | // TODO shared bundles if there are a lot of shared code across URLs
170 | getCodeForUrl(url, options) {
171 | return this._getCode(this.getComponentListForUrl(url), options);
172 | }
173 |
174 | /* Code only for components that were used (independent of url) */
175 | getAllCode() {
176 | return this._getCode(this.getFullComponentList());
177 | }
178 |
179 | _getCode(componentList = [], options = {}) {
180 | if(options.filter && typeof options.filter === "function") {
181 | componentList = componentList.filter(options.filter);
182 | }
183 | if(options.sort && typeof options.sort === "function") {
184 | componentList.sort(options.sort);
185 | }
186 |
187 | return componentList.map(componentName => {
188 | let componentCodeArr = this.getComponentCode(componentName);
189 | if(componentCodeArr.length) {
190 | return `${this.comments.pre} ${componentName} Component ${this.comments.post}
191 | ${componentCodeArr.join("\n")}`;
192 | }
193 | return "";
194 | }).filter(entry => !!entry).join("\n");
195 | }
196 | }
197 |
198 | module.exports = InlineCodeManager;
--------------------------------------------------------------------------------
/test/InlineCodeManagerTest.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const InlineCodeManager = require("../InlineCodeManager");
3 |
4 | test("getComponentNameFromPath", t => {
5 | t.is(InlineCodeManager.getComponentNameFromPath("hi.js", ".js"), "hi");
6 | t.is(InlineCodeManager.getComponentNameFromPath("test/hi.js", ".js"), "hi");
7 | t.is(InlineCodeManager.getComponentNameFromPath("sdlfjslkd/test/hi-2.js", ".js"), "hi-2");
8 | });
9 |
10 | test("Log components used on a URL", t => {
11 | let mgr = new InlineCodeManager();
12 | mgr.addComponentForUrl("header", "/");
13 | t.deepEqual(mgr.getComponentListForUrl("/"), ["header"]);
14 | t.deepEqual(mgr.getComponentListForUrl("/child/"), []);
15 |
16 | mgr.addComponentForUrl("other-header", "/other-url/");
17 | t.deepEqual(mgr.getFullComponentList(), ["header", "other-header"]);
18 |
19 | // de-dupes
20 | mgr.addComponentForUrl("header", "/");
21 | t.deepEqual(mgr.getComponentListForUrl("/"), ["header"]);
22 | t.deepEqual(mgr.getComponentListForUrl("/child/"), []);
23 |
24 | mgr.addComponentForUrl("other-header", "/other-url/");
25 | t.deepEqual(mgr.getFullComponentList(), ["header", "other-header"]);
26 | });
27 |
28 | test("Get component list for a URL but only components that have code", t => {
29 | let mgr = new InlineCodeManager();
30 | mgr.addComponentForUrl("header", "/");
31 | mgr.addComponentForUrl("footer", "/");
32 | t.deepEqual(mgr.getRelevantComponentListForUrl("/"), []);
33 | t.deepEqual(mgr.getRelevantComponentListForUrl("/child/"), []);
34 |
35 | mgr.addComponentCode("header", "/* this is code */");
36 | t.deepEqual(mgr.getRelevantComponentListForUrl("/"), ["header"]);
37 | t.deepEqual(mgr.getRelevantComponentListForUrl("/child/"), []);
38 |
39 | mgr.addComponentCode("footer", ""); // code must not be empty
40 | t.deepEqual(mgr.getRelevantComponentListForUrl("/"), ["header"]);
41 | t.deepEqual(mgr.getRelevantComponentListForUrl("/child/"), []);
42 |
43 | mgr.addComponentCode("footer", "/* this is code */");
44 | t.deepEqual(mgr.getRelevantComponentListForUrl("/"), ["header", "footer"]);
45 | t.deepEqual(mgr.getRelevantComponentListForUrl("/child/"), []);
46 | });
47 |
48 | test("Relationships", t => {
49 | let mgr = new InlineCodeManager();
50 | // without a declared fileExtension
51 | mgr.addRawComponentRelationship("parent.js", "child.js");
52 | t.deepEqual(mgr.getComponentListForComponent("parent.js"), ["child.js"]);
53 |
54 | mgr.init();
55 | mgr.addRawComponentRelationship("parent.js", "child.js", ".js");
56 | t.deepEqual(mgr.getComponentListForComponent("parent"), ["child"]);
57 |
58 | mgr.init();
59 | mgr.addRawComponentRelationship("parent", "child");
60 | t.deepEqual(mgr.getComponentListForComponent("parent"), ["child"]);
61 | });
62 |
63 | test("Duplicate Relationships", t => {
64 | let mgr = new InlineCodeManager();
65 | mgr.addRawComponentRelationship("parent.js", "child.js", ".js");
66 | mgr.addRawComponentRelationship("parent.js", "child.js", ".js");
67 | mgr.addRawComponentRelationship("parent.js", "test.js", ".js");
68 |
69 | t.deepEqual(mgr.getComponentListForComponent("parent"), ["child", "test"]);
70 | });
71 |
72 | test("Relationships roll into final component list", t => {
73 | let mgr = new InlineCodeManager();
74 | mgr.addComponentForUrl("parent", "/");
75 | mgr.addRawComponentRelationship("parent.js", "child.js", ".js");
76 | mgr.addRawComponentRelationship("aunt.js", "cousin.js", ".js");
77 |
78 | t.deepEqual(mgr.getComponentListForUrl("/"), ["child", "parent"]);
79 | t.deepEqual(mgr.getFullComponentList(), ["child", "parent"]);
80 |
81 | mgr.addComponentForUrl("other-parent", "/other-path/");
82 | t.deepEqual(mgr.getFullComponentList(), ["child", "parent", "other-parent"]);
83 |
84 | mgr.addComponentForUrl("cousin", "/");
85 | // t.deepEqual(mgr.getComponentListForUrl("/"), ["parent", "child", "cousin"]);
86 | t.deepEqual(mgr.getComponentListForUrl("/"), ["child", "parent", "cousin"]);
87 | t.deepEqual(mgr.getFullComponentList(), ["child", "parent", "cousin", "other-parent"]);
88 |
89 | mgr.addComponentForUrl("aunt", "/");
90 | t.deepEqual(mgr.getComponentListForUrl("/"), ["child", "parent", "cousin", "aunt"]);
91 | t.deepEqual(mgr.getFullComponentList(), ["child", "parent", "cousin", "aunt", "other-parent"]);
92 | });
93 |
94 | test("Relationships roll into final component list (sibling/child)", t => {
95 | let mgr = new InlineCodeManager();
96 | mgr.addComponentForUrl("parent", "/");
97 | mgr.addRawComponentRelationship("parent.js", "child.js", ".js");
98 | mgr.addRawComponentRelationship("parent.js", "sibling.js", ".js");
99 |
100 | t.deepEqual(mgr.getComponentListForUrl("/"), ["child", "sibling", "parent"]);
101 | t.deepEqual(mgr.getFullComponentList(), ["child", "sibling", "parent"]);
102 | });
103 |
104 | test("Add Component Code", t => {
105 | let cssMgr = new InlineCodeManager();
106 | let fontWeight = "p { font-weight: 700; }";
107 | let fontColor = "div { color: blue; }";
108 |
109 | cssMgr.addComponentCode("header", fontWeight);
110 | cssMgr.addComponentCode("header", fontColor);
111 |
112 | // de-dupes duplicate code
113 | cssMgr.addComponentCode("header", fontWeight);
114 | t.deepEqual(cssMgr.getComponentCode("header"), [fontWeight, fontColor]);
115 | });
116 |
117 | test("getCodeForUrl", t => {
118 | let mgr = new InlineCodeManager();
119 | let fontWeight = "p { font-weight: 700; }";
120 | let fontColor = "div { color: blue; }";
121 |
122 | mgr.addComponentCode("header", fontWeight);
123 | mgr.addComponentCode("footer", fontColor);
124 |
125 | mgr.addComponentForUrl("header", "/");
126 | mgr.addComponentForUrl("footer", "/");
127 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
128 | p { font-weight: 700; }
129 | /* footer Component */
130 | div { color: blue; }`);
131 | });
132 |
133 | test("getCodeForUrl sorted", t => {
134 | let mgr = new InlineCodeManager();
135 | let fontWeight = "p { font-weight: 700; }";
136 | let fontColor = "div { color: blue; }";
137 |
138 | mgr.addComponentCode("header", fontWeight);
139 | mgr.addComponentCode("footer", fontColor);
140 |
141 | mgr.addComponentForUrl("header", "/");
142 | mgr.addComponentForUrl("footer", "/");
143 | t.deepEqual(mgr.getCodeForUrl("/", {
144 | sort: function(a, b) {
145 | // alphabetical
146 | if(a < b) {
147 | return -1;
148 | } else if(a > b) {
149 | return 1;
150 | }
151 | return 0;
152 | }
153 | }), `/* footer Component */
154 | div { color: blue; }
155 | /* header Component */
156 | p { font-weight: 700; }`);
157 | });
158 |
159 | test("getCodeForUrl filtered", t => {
160 | let mgr = new InlineCodeManager();
161 | let fontWeight = "p { font-weight: 700; }";
162 | let fontColor = "div { color: blue; }";
163 |
164 | mgr.addComponentCode("header", fontWeight);
165 | mgr.addComponentCode("footer", fontColor);
166 |
167 | mgr.addComponentForUrl("header", "/");
168 | mgr.addComponentForUrl("footer", "/");
169 | t.deepEqual(mgr.getCodeForUrl("/", {
170 | filter: entry => entry !== "header"
171 | }), `/* footer Component */
172 | div { color: blue; }`);
173 | });
174 |
175 | test("getCodeForUrl filtered and sorted", t => {
176 | let mgr = new InlineCodeManager();
177 | let fontWeight = "p { font-weight: 700; }";
178 | let fontColor = "div { color: blue; }";
179 |
180 | mgr.addComponentCode("header", fontWeight);
181 | mgr.addComponentCode("footer", fontColor);
182 | mgr.addComponentCode("footer2", fontColor);
183 |
184 | mgr.addComponentForUrl("header", "/");
185 | mgr.addComponentForUrl("footer", "/");
186 | mgr.addComponentForUrl("footer2", "/");
187 | t.deepEqual(mgr.getCodeForUrl("/", {
188 | filter: entry => entry !== "header",
189 | sort: function(a, b) {
190 | // reverse alphabetical
191 | if(a < b) {
192 | return 1;
193 | } else if(a > b) {
194 | return -1;
195 | }
196 | return 0;
197 | }
198 | }), `/* footer2 Component */
199 | div { color: blue; }
200 | /* footer Component */
201 | div { color: blue; }`);
202 | });
203 |
204 | test("Reset and reset for component", t => {
205 | let mgr = new InlineCodeManager();
206 | mgr.addComponentForUrl("header", "/");
207 | mgr.addComponentCode("header", "/* this is header code */");
208 |
209 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
210 | /* this is header code */`);
211 |
212 | mgr.addComponentForUrl("footer", "/");
213 | mgr.addComponentCode("footer", "/* this is footer code */");
214 |
215 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
216 | /* this is header code */
217 | /* footer Component */
218 | /* this is footer code */`);
219 |
220 | mgr.addComponentForUrl("nav", "/");
221 | mgr.addComponentCode("nav", "/* this is nav code */");
222 |
223 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
224 | /* this is header code */
225 | /* footer Component */
226 | /* this is footer code */
227 | /* nav Component */
228 | /* this is nav code */`);
229 |
230 | mgr.resetComponentCodeFor("footer");
231 |
232 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
233 | /* this is header code */
234 | /* nav Component */
235 | /* this is nav code */`);
236 |
237 | mgr.resetComponentCodeFor("nav");
238 |
239 | t.deepEqual(mgr.getCodeForUrl("/"), `/* header Component */
240 | /* this is header code */`);
241 |
242 | mgr.resetComponentCode();
243 |
244 | t.deepEqual(mgr.getCodeForUrl("/"), ``);
245 | });
246 |
247 | test("Reset component lists", t => {
248 | let mgr = new InlineCodeManager();
249 | mgr.addComponentForUrl("header", "/");
250 |
251 | t.deepEqual(mgr.getComponentListForUrl("/"), ["header"]);
252 |
253 | mgr.addComponentForUrl("footer", "/");
254 |
255 | t.deepEqual(mgr.getComponentListForUrl("/"), ["header", "footer"]);
256 |
257 | mgr.addComponentForUrl("nav", "/");
258 |
259 | t.deepEqual(mgr.getComponentListForUrl("/"), ["header", "footer", "nav"]);
260 |
261 | mgr.resetForUrl("/");
262 | t.deepEqual(mgr.getComponentListForUrl("/"), []);
263 | });
--------------------------------------------------------------------------------