Repository: maizzle/framework
Pull request #851 (2022-12-07 15:14:16)

Add options to omit and skip templates
This PR adds two new options to `build.templates`: `omit` and `skip`.

### `omit`

This can be used to define paths to files or directories from your `source` that should not be copied over to the build destination.

For example, imagine your project's `src/templates` contained these files:

```
src/templates
├── 1.html
├── 2.html
└── archive
    ├── 3.html
    └── 4.html
```

You can prevent `1.html` and `4.html` from being copied to the build destination like this:

```js
module.exports = {
  build: {
    templates: {
      source: 'src/templates',
      omit: ['1.html', 'archive/4.html'],
      // ...
    }
  }
}
```

In this case, running `maizzle build production` would create a `build_production` folder with these files inside:

```
build_production
├── 2.html
└── archive
    ├── 3.html
```

It supports directory paths, so you can omit the entire `archive` folder:

```js
module.exports = {
  build: {
    templates: {
      source: 'src/templates',
      omit: ['1.html', 'archive'],
      // ...
    }
  }
}
```

The result would be:

```
build_production
├── 2.html
```

### `skip`

Use the new `skip` option if you want to skip the compilation of a template. The file will only be copied as-is to the build destination, it will not be parsed in any way by Maizzle.

```js
module.exports = {
  build: {
    templates: {
      source: 'src/templates',
      skip: ['1.html', 'archive/3.html'],
      // ...
    }
  }
}
```

`skip` can be a string (excludes just one template) or an array of strings like in the example above. 

Each string must be a file path that is relative to your `templates.source` directory.


Patch:
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index 9e1fa44..6468327 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -67,6 +67,17 @@ module.exports = async (env, spinner, config) => {
       }
     }
 
+    // Create a pipe-delimited list of allowed extensions
+    // We only compile these, the rest are copied as-is
+    const extensions = Array.isArray(templateConfig.filetypes)
+      ? templateConfig.filetypes.join('|')
+      : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
+
+    // List of files that won't be copied to the output directory
+    const omitted = Array.isArray(templateConfig.omit) ?
+      templateConfig.omit :
+      [get(templateConfig, 'omit', '')]
+
     // Parse each template source
     await asyncForEach(templateSource, async source => {
       /**
@@ -77,13 +88,22 @@ module.exports = async (env, spinner, config) => {
       const out = fs.lstatSync(source).isFile() ? `${outputDir}/${path.basename(source)}` : outputDir
 
       await fs
-        .copy(source, out)
+        .copy(source, out, {filter: file => {
+          // Do not copy omitted files
+          return !omitted
+            .filter(Boolean)
+            .some(omit => path.normalize(file).includes(path.normalize(omit)))
+        }})
         .then(async () => {
-          const extensions = Array.isArray(templateConfig.filetypes)
-            ? templateConfig.filetypes.join('|')
-            : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
+          const allSourceFiles = await glob(`${outputDir}/**/*.+(${extensions})`)
 
-          const templates = await glob(`${outputDir}/**/*.+(${extensions})`)
+          const skipped = Array.isArray(templateConfig.skip) ?
+            templateConfig.skip :
+            [get(templateConfig, 'skip', '')]
+
+          const templates = allSourceFiles.filter(template => {
+            return !skipped.includes(template.replace(`${outputDir}/`, ''))
+          })
 
           if (templates.length === 0) {
             spinner.warn(`Error: no files with the .${extensions} extension found in ${templateConfig.source}`)


Test patch:
diff --git a/test/stubs/templates/nested/3.html b/test/stubs/templates/nested/3.html
new file mode 100644
index 0000000..1d49a0d
--- /dev/null
+++ b/test/stubs/templates/nested/3.html
@@ -0,0 +1,1 @@
+<div class="inline">nested template</div>
diff --git a/test/test-todisk.js b/test/test-todisk.js
index c831a04..d8d6c22 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -63,8 +63,8 @@ test('outputs files at the correct location', async t => {
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(string.length, 3)
-  t.is(array.length, 3)
+  t.is(string.length, 4)
+  t.is(array.length, 4)
 })
 
 test('outputs files at the correct location if multiple template sources are used', async t => {
@@ -89,7 +89,7 @@ test('outputs files at the correct location if multiple template sources are use
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 6)
+  t.is(files.length, 7)
 })
 
 test('copies all files in the `filetypes` option to destination', async t => {
@@ -107,7 +107,7 @@ test('copies all files in the `filetypes` option to destination', async t => {
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 3)
+  t.is(files.length, 4)
 })
 
 test('outputs files with the correct extension', async t => {
@@ -283,7 +283,7 @@ test('runs the `afterBuild` event', async t => {
 })
 
 test('supports multiple asset paths', async t => {
-  await Maizzle.build('maizzle-ci', {
+  const {files} = await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
       templates: {
@@ -299,9 +299,9 @@ test('supports multiple asset paths', async t => {
     }
   })
 
-  t.true(await fs.pathExists(`${t.context.folder}/extras/foo.bar`))
-  t.true(await fs.pathExists(`${t.context.folder}/extras/plaintext.html`))
-  t.false(await fs.pathExists(`${t.context.folder}/extras/invalid`))
+  t.true(files.includes(`${t.context.folder}/extras/foo.bar`))
+  t.true(files.includes(`${t.context.folder}/extras/plaintext.html`))
+  t.false(files.includes(`${t.context.folder}/extras/invalid`))
 })
 
 test('warns if a template cannot be rendered and `fail` option is undefined', async t => {
@@ -392,7 +392,7 @@ test('throws if it cannot spin up local development server', async t => {
   }, {instanceOf: TypeError})
 })
 
-test('works with templates.source defined as function (string paths)', async t => {
+test('`templates.source` defined as function (string paths)', async t => {
   const {files} = await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
@@ -405,11 +405,10 @@ test('works with templates.source defined as function (string paths)', async t =
     }
   })
 
-  t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 3)
+  t.is(files.length, 4)
 })
 
-test('works with templates.source defined as function (array paths)', async t => {
+test('`templates.source` defined as function (array paths)', async t => {
   const {files} = await Maizzle.build('maizzle-ci', {
     build: {
       fail: 'silent',
@@ -425,8 +424,7 @@ test('works with templates.source defined as function (array paths)', async t =>
     }
   })
 
-  t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 3)
+  t.is(files.length, 4)
 })
 
 test('throws if templates path is invalid', async t => {
@@ -484,10 +482,89 @@ test('sets config.build.current.path', async t => {
   t.deepEqual(t.context.current, {
     path: {
       root: '',
-      dir: t.context.folder,
-      base: '2.html',
+      dir: `${t.context.folder}/nested`,
+      base: '3.html',
       ext: '.html',
-      name: '2'
+      name: '3'
+    }
+  })
+})
+
+test('skips compiling templates', async t => {
+  let parsedFilesFromStringSource = 0
+  await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: 'test/stubs/templates',
+        skip: '1.html',
+        destination: {
+          path: t.context.folder
+        }
+      }
+    },
+    // Increment counter for each file that was actually parsed
+    events: {
+      afterRender(html) {
+        parsedFilesFromStringSource++
+
+        return html
+      }
+    }
+  })
+
+  let parsedFilesFromArraySource = 0
+  await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: ['test/stubs/templates'],
+        skip: ['1.html', 'nested/3.html'],
+        destination: {
+          path: t.context.folder
+        }
+      }
+    },
+    events: {
+      afterRender(html) {
+        parsedFilesFromArraySource++
+
+        return html
+      }
+    }
+  })
+
+  t.is(parsedFilesFromStringSource, 2)
+  t.is(parsedFilesFromArraySource, 1)
+})
+
+test('does not output omitted files', async t => {
+  const {files: fromStringSource} = await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: 'test/stubs/templates',
+        omit: '1.html',
+        destination: {
+          path: t.context.folder
+        }
+      }
+    }
+  })
+
+  const {files: fromArraySource} = await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: ['test/stubs/templates'],
+        omit: ['1.html', 'nested/3.html'],
+        destination: {
+          path: t.context.folder
+        }
+      }
     }
   })
+
+  t.is(fromStringSource.length, 3)
+  t.is(fromArraySource.length, 2)
 })