├── .gitignore ├── AssetManager.js ├── InlineCodeManager.js ├── README.md ├── package.json ├── sample ├── .eleventy.js ├── _includes │ └── layout.njk ├── css │ ├── footer.css │ └── header.css ├── other-page.md └── sample-page.md └── test └── InlineCodeManagerTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | _site -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /sample/_includes/layout.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | {{ content | safe }} 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------