Repository: maizzle/framework
Pull request #1184 (2024-02-15 16:16:18)

feat: add config assets array support
I would like to add support for `Array` to config `templates.assets`. I need to add several assets and build each of them into separate directories.

## Issue Description
In my case, there is an `email-kit` directory with components and images but without templates. I would like to add assets and build each of them into their own directory. I can add `email-kit` assets to some other template config, but there is no option to separate asset destinations for different assets. In this case, adding to config `templates.assets` `Array` support will be helpful.

## Proposed solution
Normalize `assets` to `Array` and handle assets via `for` loop.

## Example
```
// Simplified monorepo directory structure with shared packages for different projects
├───projects
│   ├───project-1
│   │   └───maizzle // <-- config.js
│   │       ├───images // 🖼 assets
│   │       └───templates
│   └───project-2
└───shared-packages
    ├───email-kit
    │   ├───components
    │   └───images // 🖼 assets without own templates
    ├───package-A
    │   ├───images // 🖼 assets
    │   └───templates
    └───package-B // 🖼 assets
```
```
// Shortened expected output result
└───images
    ├───email-kit
    ├───package-A
    ├───package-B
    └───project-1
```
``` js
module.exports = {
  build: {
    templates: [
      {
        source: 'src/templates',
        destination: {
          path: destinationPath,
        },
        assets: [
          {
            source: 'src/images',
            destination: 'images/project-1',
          },
          {
            source: '../../../shared-packages/email-kit/images',
            destination: 'images/email-kit',
          },
        ]
      },
      // ... template config for each package
      {
        source: '../../../shared-packages/package-A/templates',
        destination: `${destinationPath}/package-A`,
        assets: {
          source: '../../../shared-packages/package-A/images',
          destination: 'images/package-A'
        }
      },
      // ...
    ],
    // ...
  }
}
```

Thank you for the great work!


Patch:
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index 8417e58..f4b65ce 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -209,22 +209,26 @@ module.exports = async (env, spinner, config) => {
             }
           }
 
-          const assets = {source: '', destination: 'assets', ...get(templateConfig, 'assets')}
-
-          if (Array.isArray(assets.source)) {
-            for await (const source of assets.source) {
-              if (fs.existsSync(source)) {
+          const assets = Array.isArray(get(templateConfig, 'assets'))
+            ? get(templateConfig, 'assets')
+            : [{source: '', destination: 'assets', ...get(templateConfig, 'assets')}]
+
+          for await (const asset of assets) {
+            if (Array.isArray(asset.source)) {
+              for await (const source of asset.source) {
+                if (fs.existsSync(source)) {
+                  await fs
+                    .copy(source, path.join(templateConfig.destination.path, asset.destination))
+                    .catch(error => spinner.warn(error.message))
+                }
+              }
+            } else {
+              if (fs.existsSync(asset.source)) {
                 await fs
-                  .copy(source, path.join(templateConfig.destination.path, assets.destination))
+                  .copy(asset.source, path.join(templateConfig.destination.path, asset.destination))
                   .catch(error => spinner.warn(error.message))
               }
             }
-          } else {
-            if (fs.existsSync(assets.source)) {
-              await fs
-                .copy(assets.source, path.join(templateConfig.destination.path, assets.destination))
-                .catch(error => spinner.warn(error.message))
-            }
           }
 
           await glob(path.join(templateConfig.destination.path, '/**').replace(/\\/g, '/'))


Test patch:
diff --git a/test/stubs/assets2/foo1.bar b/test/stubs/assets2/foo1.bar
new file mode 100644
index 0000000..5f5bdb4
--- /dev/null
+++ b/test/stubs/assets2/foo1.bar
@@ -0,0 +1,1 @@
+Some asset file...
diff --git a/test/stubs/assets2/foo2.bar b/test/stubs/assets2/foo2.bar
new file mode 100644
index 0000000..5f5bdb4
--- /dev/null
+++ b/test/stubs/assets2/foo2.bar
@@ -0,0 +1,1 @@
+Some asset file...
diff --git a/test/test-todisk.js b/test/test-todisk.js
index 47ea1df..2acd403 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -237,6 +237,39 @@ test('copies assets to destination', async t => {
   t.is(filelist.length, 1)
 })
 
+test('copies assets array to destinations', async t => {
+  await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: 'test/stubs/templates',
+        destination: {
+          path: t.context.folder
+        },
+        assets: [
+          {
+            source: 'test/stubs/assets',
+            destination: 'assets/images1'
+          },
+          {
+            source: 'test/stubs/assets2',
+            destination: 'assets/images2'
+          }
+        ]
+      }
+    }
+  })
+
+  const images1 = await fs.readdir(`${t.context.folder}/assets/images1`)
+  t.is(await fs.pathExists(`${t.context.folder}/assets/images1`), true)
+  t.true(images1.includes('foo.bar'))
+
+  const images2 = await fs.readdir(`${t.context.folder}/assets/images2`)
+  t.is(await fs.pathExists(`${t.context.folder}/assets/images2`), true)
+  t.true(images2.includes('foo1.bar'))
+  t.true(images2.includes('foo2.bar'))
+})
+
 test('runs the `beforeCreate` event', async t => {
   await Maizzle.build('maizzle-ci', {
     build: {
@@ -306,6 +339,96 @@ test('supports multiple asset paths', async t => {
   t.false(files.includes(`${t.context.folder}/extras/invalid`))
 })
 
+test('supports multiple assets array paths', async t => {
+  const {files} = await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: {
+        source: 'test/stubs/templates',
+        destination: {
+          path: t.context.folder
+        },
+        assets: [
+          {
+            source: ['test/stubs/plaintext', 'test/stubs/invalid'],
+            destination: 'extras'
+          },
+          {
+            source: ['test/stubs/assets'],
+            destination: 'assets/images1'
+          },
+          {
+            source: ['test/stubs/assets2'],
+            destination: 'assets/images2'
+          }
+        ]
+      }
+    }
+  })
+
+  t.true(files.includes(`${t.context.folder}/extras/plaintext.html`))
+  t.false(files.includes(`${t.context.folder}/extras/invalid`))
+
+  t.true(files.includes(`${t.context.folder}/assets/images1/foo.bar`))
+
+  t.true(files.includes(`${t.context.folder}/assets/images2/foo1.bar`))
+  t.true(files.includes(`${t.context.folder}/assets/images2/foo2.bar`))
+})
+
+test('supports multiple assets array paths with templates array', async t => {
+  const {files} = await Maizzle.build('maizzle-ci', {
+    build: {
+      fail: 'silent',
+      templates: [
+        {
+          source: 'test/stubs/plaintext',
+          destination: {
+            path: t.context.folder
+          },
+          assets: [
+            {
+              source: ['test/stubs/invalid', 'test/stubs/assets'],
+              destination: 'assets/extras1'
+            },
+            {
+              source: ['test/stubs/assets2'],
+              destination: 'assets/extras2'
+            }
+          ]
+        },
+        {
+          source: 'test/stubs/templates',
+          destination: {
+            path: t.context.folder
+          },
+          assets: [
+            {
+              source: ['test/stubs/invalid', 'test/stubs/assets'],
+              destination: 'assets/extras3'
+            },
+            {
+              source: ['test/stubs/assets2'],
+              destination: 'assets/extras4'
+            }
+          ]
+        }
+      ]
+    }
+  })
+
+  t.false(files.includes(`${t.context.folder}/assets/extras1/invalid`))
+  t.true(files.includes(`${t.context.folder}/assets/extras1/foo.bar`))
+
+  t.true(files.includes(`${t.context.folder}/assets/extras2/foo1.bar`))
+  t.true(files.includes(`${t.context.folder}/assets/extras2/foo2.bar`))
+
+  t.false(files.includes(`${t.context.folder}/assets/extras3/invalid`))
+  t.true(files.includes(`${t.context.folder}/assets/extras3/foo.bar`))
+
+  t.true(files.includes(`${t.context.folder}/assets/extras4/foo1.bar`))
+  t.true(files.includes(`${t.context.folder}/assets/extras4/foo2.bar`))
+})
+
 test('warns if a template cannot be rendered and `fail` option is undefined', async t => {
   const {files} = await Maizzle.build('maizzle-ci', {
     build: {