Repository: maizzle/framework
Pull request #901 (2023-02-13 14:52:25)

Fix paths and throw errors in local server
This PR:

- fixes an issue where saving a template file while developing locally was outputting it in a wrong location 804b100
- ensures an error is thrown when local server rebuilds fail e317e4c


Patch:
diff --git a/src/commands/serve.js b/src/commands/serve.js
index 0dc73b5..c869781 100644
--- a/src/commands/serve.js
+++ b/src/commands/serve.js
@@ -36,137 +36,154 @@ const serve = async (env = 'local', config = {}) => {
 
   const spinner = ora()
 
-  try {
-    await buildToFile(env, config)
-
-    let templates = get(config, 'build.templates')
-    templates = Array.isArray(templates) ? templates : [templates]
-
-    const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
-    const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
-    const globalPaths = [
-      'src/**',
-      ...new Set(get(config, 'build.browsersync.watch', []))
-    ]
-
-    if (typeof tailwindConfig === 'string') {
-      globalPaths.push(tailwindConfig)
-    }
-
-    // Watch for Template file changes
-    browsersync()
-      .watch(templatePaths)
-      .on('change', async file => {
-        config = await getConfig(env, config)
-
-        if (config.events && typeof config.events.beforeCreate === 'function') {
-          await config.events.beforeCreate(config)
-        }
-
-        // Don't render if file type is not configured
-        // eslint-disable-next-line
-        const filetypes = templates.reduce((acc, template) => {
-          return [...acc, ...get(template, 'filetypes', ['html'])]
-        }, [])
-
-        if (!filetypes.includes(path.extname(file).slice(1))) {
-          return
-        }
-
-        if (get(config, 'build.console.clear')) {
-          clearConsole()
-        }
-
-        const start = new Date()
-
-        spinner.start('Building email...')
+  // Build all emails first
+  await buildToFile(env, config)
+
+  // Set some paths to watch
+  let templates = get(config, 'build.templates')
+  templates = Array.isArray(templates) ? templates : [templates]
+
+  const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
+  const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
+  const globalPaths = [
+    'src/**',
+    ...new Set(get(config, 'build.browsersync.watch', []))
+  ]
+
+  if (typeof tailwindConfig === 'string') {
+    globalPaths.push(tailwindConfig)
+  }
 
-        renderToString(
-          await fs.readFile(file.replace(/\\/g, '/'), 'utf8'),
-          {
-            maizzle: merge(
-              config,
-              {
-                build: {
-                  current: {
-                    path: path.parse(file)
-                  }
+  // Watch for Template file changes
+  browsersync()
+    .watch(templatePaths)
+    .on('change', async file => {
+      config = await getConfig(env, config)
+
+      if (config.events && typeof config.events.beforeCreate === 'function') {
+        await config.events.beforeCreate(config)
+      }
+
+      // Don't render if file type is not configured
+      // eslint-disable-next-line
+      const filetypes = templates.reduce((acc, template) => {
+        return [...acc, ...get(template, 'filetypes', ['html'])]
+      }, [])
+
+      if (!filetypes.includes(path.extname(file).slice(1))) {
+        return
+      }
+
+      // Clear console if enabled
+      if (get(config, 'build.console.clear')) {
+        clearConsole()
+      }
+
+      // Start the spinner
+      const start = new Date()
+      spinner.start('Building email...')
+
+      // Render the template
+      renderToString(
+        await fs.readFile(file.replace(/\\/g, '/'), 'utf8'),
+        {
+          maizzle: merge(
+            config,
+            {
+              build: {
+                current: {
+                  path: path.parse(file)
                 }
               }
-            ),
-            ...config.events
-          }
-        )
-          .then(async ({html, config}) => {
-            let source = ''
-            let dest = ''
-            let ext = ''
-
-            if (Array.isArray(config.build.templates)) {
-              const match = config.build.templates.find(template => template.source === path.parse(file).dir)
-              source = get(match, 'source')
-              dest = get(match, 'destination.path', 'build_local')
-              ext = get(match, 'destination.ext', 'html')
-            } else if (isObject(config.build.templates)) {
-              source = get(config, 'build.templates.source')
-              dest = get(config, 'build.templates.destination.path', 'build_local')
-              ext = get(config, 'build.templates.destination.ext', 'html')
             }
+          ),
+          ...config.events
+        }
+      )
+        .then(async ({html, config}) => {
+          // Write the file to disk
+          let source = ''
+          let dest = ''
+          let ext = ''
+
+          if (Array.isArray(config.build.templates)) {
+            const match = config.build.templates.find(template => template.source === path.parse(file).dir)
+            source = path.normalize(get(match, 'source'))
+            dest = path.normalize(get(match, 'destination.path', 'build_local'))
+            ext = get(match, 'destination.ext', 'html')
+          } else if (isObject(config.build.templates)) {
+            source = path.normalize(get(config, 'build.templates.source'))
+            dest = path.normalize(get(config, 'build.templates.destination.path', 'build_local'))
+            ext = get(config, 'build.templates.destination.ext', 'html')
+          }
 
-            const fileDir = path.parse(file).dir.replace(source, '')
-            const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
-
-            await fs.outputFile(config.permalink || finalDestination, html)
-          })
-          .then(() => {
-            browsersync().reload()
-            spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
-          })
-          .catch(() => spinner.warn(`Received empty HTML, please save your file again [${file}]`))
+          const fileDir = path.parse(file).dir.replace(source, '')
+          const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
+
+          await fs.outputFile(config.permalink || finalDestination, html)
+        })
+        .then(() => {
+          browsersync().reload()
+          spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
+        })
+        .catch(error => {
+          throw error
+        })
+    })
+
+  // Watch for changes in all other files
+  browsersync()
+    .watch(globalPaths, {ignored: templatePaths})
+    .on('change', () => buildToFile(env, config)
+      .then(() => browsersync().reload())
+      .catch(error => {
+        throw error
       })
-
-    // Watch for changes in all other files
-    browsersync()
-      .watch(globalPaths, {ignored: templatePaths})
-      .on('change', () => buildToFile(env, config).then(() => browsersync().reload()))
-      .on('unlink', () => buildToFile(env, config).then(() => browsersync().reload()))
-
-    // Watch for changes in config files
-    browsersync()
-      .watch('config*.js')
-      .on('change', async file => {
-        const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
-
-        Config
-          .getMerged(parsedEnv)
-          .then(config => buildToFile(parsedEnv, config).then(() => browsersync().reload()))
+    )
+    .on('unlink', () => buildToFile(env, config)
+      .then(() => browsersync().reload())
+      .catch(error => {
+        throw error
       })
-
-    // Browsersync options
-    const baseDir = templates.map(t => t.destination.path)
-
-    // Initialize Browsersync
-    browsersync()
-      .init(
-        merge(
-          {
-            notify: false,
-            open: false,
-            port: 3000,
-            server: {
-              baseDir,
-              directory: true
-            },
-            tunnel: false,
-            ui: {port: 3001},
-            logFileChanges: false
+    )
+
+  // Watch for changes in config files
+  browsersync()
+    .watch('config*.js')
+    .on('change', async file => {
+      const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
+
+      Config
+        .getMerged(parsedEnv)
+        .then(config => buildToFile(parsedEnv, config)
+          .then(() => browsersync().reload())
+          .catch(error => {
+            throw error
+          })
+        )
+    })
+
+  // Browsersync options
+  const baseDir = templates.map(t => t.destination.path)
+
+  // Initialize Browsersync
+  browsersync()
+    .init(
+      merge(
+        {
+          notify: false,
+          open: false,
+          port: 3000,
+          server: {
+            baseDir,
+            directory: true
           },
-          get(config, 'build.browsersync', {})
-        ), () => {})
-  } catch (error) {
-    spinner.fail(error)
-    throw error
-  }
+          tunnel: false,
+          ui: {port: 3001},
+          logFileChanges: false
+        },
+        get(config, 'build.browsersync', {})
+      ), () => {})
 }
 
 module.exports = serve


Test patch:
diff --git a/test/test-serve.js b/test/test-serve.js
new file mode 100644
index 0000000..9cff306
--- /dev/null
+++ b/test/test-serve.js
@@ -0,0 +1,57 @@
+const test = require('ava')
+const fs = require('fs-extra')
+const Maizzle = require('../src')
+
+test.beforeEach(t => {
+  t.context.folder = '_temp_' + Math.random().toString(36).slice(2, 9)
+  t.context.log = console.log()
+})
+
+test.afterEach.always(async t => {
+  if (t.context.folder) {
+    await fs.remove(t.context.folder)
+    delete t.context.folder
+  }
+})
+
+test('throws if it cannot spin up local development server', async t => {
+  // Should throw because there are no template sources in `build.templates`
+  await t.throwsAsync(async () => {
+    await Maizzle.serve('local', {})
+  }, {instanceOf: Error})
+})
+
+test('local server does not compile unwanted file types', async t => {
+  await Maizzle.serve('local', {
+    build: {
+      console: {
+        clear: true
+      },
+      browsersync: {
+        ui: false
+      },
+      templates: {
+        source: 'test/stubs/templates',
+        destination: {
+          path: `${t.context.folder}`
+        }
+      }
+    },
+    events: {
+      beforeCreate(config) {
+        config.foo = 'bar'
+      }
+    }
+  })
+
+  t.true(await fs.pathExists(`${t.context.folder}`))
+  t.true(await fs.pathExists(`${t.context.folder}/2.test`))
+
+  // Tests watching changes to files
+  await fs.outputFile('test/stubs/templates/2.html', '<div class="inline">html modified</div>')
+  t.is(await fs.readFile('test/stubs/templates/2.html', 'utf8'), '<div class="inline">html modified</div>')
+
+  // Don't trigger rebuilds on files not in `filetypes`
+  await fs.outputFile('test/stubs/templates/2.test', 'test')
+  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test')
+})
diff --git a/test/test-todisk.js b/test/test-todisk.js
index bd996cd..bfbedf5 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -351,47 +351,6 @@ test('warns if a template cannot be rendered and `fail` option is `silent`', asy
   t.false(files.includes('empty.html'))
 })
 
-test('local server does not compile unwanted file types', async t => {
-  await Maizzle.serve('local', {
-    build: {
-      console: {
-        clear: true
-      },
-      browsersync: {
-        ui: false
-      },
-      templates: {
-        source: 'test/stubs/templates',
-        destination: {
-          path: `${t.context.folder}`
-        }
-      }
-    },
-    events: {
-      beforeCreate(config) {
-        config.foo = 'bar'
-      }
-    }
-  })
-
-  t.true(await fs.pathExists(`${t.context.folder}`))
-  t.true(await fs.pathExists(`${t.context.folder}/2.test`))
-
-  // Tests watching changes to files
-  await fs.outputFile('test/stubs/templates/2.html', '<div class="inline">html modified</div>')
-  t.is(await fs.readFile('test/stubs/templates/2.html', 'utf8'), '<div class="inline">html modified</div>')
-
-  // Don't trigger rebuilds on files not in `filetypes`
-  await fs.outputFile('test/stubs/templates/2.test', 'test')
-  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test')
-})
-
-test('throws if it cannot spin up local development server', async t => {
-  await t.throwsAsync(async () => {
-    await Maizzle.serve('local', {})
-  }, {instanceOf: Error})
-})
-
 test('`templates.source` undefined', async t => {
   await t.throwsAsync(async () => {
     await Maizzle.build('maizzle-ci')