diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/package.json b/package.json index 3d5bb6a..30144a0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "node": "14.17.5" }, "main": "src/nginxconfig/mount.js", + "type": "module", "scripts": { "build": "npm run build:clean && npm run build:template && npm run build:prism && npm run build:static && npm run build:tool", "build:clean": "do-vue clean", @@ -24,7 +25,7 @@ "test:eslint": "eslint 'src/**/*.{js,vue}'", "test:eslint:fix": "npm run test:eslint -- --fix", "test:sass-lint": "sass-lint 'src/**/*.scss' --verbose --no-exit --config .sasslintrc", - "test:i18n-packs": "node -r esm src/nginxconfig/i18n/verify.js" + "test:i18n-packs": "node --es-module-specifier-resolution=node src/nginxconfig/i18n/verify.js" }, "jest": { "testRegex": "/test/.*.js?$" diff --git a/src/nginxconfig/build/prism.js b/src/nginxconfig/build/prism.js index a0937d5..a4a8bac 100644 --- a/src/nginxconfig/build/prism.js +++ b/src/nginxconfig/build/prism.js @@ -1,5 +1,5 @@ /* -Copyright 2020 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -24,9 +24,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const fs = require('fs').promises; -const path = require('path'); -const fetch = require('node-fetch'); +import { promises as fs } from 'fs'; +import { URL } from 'url'; +import fetch from 'node-fetch'; const main = async () => { const resp = await fetch('https://assets.digitalocean.com/prism/prism.css'); @@ -35,8 +35,11 @@ const main = async () => { // Fix $676767 -> #676767 const fixed = text.replace(/:\s*\$((?:[0-9a-fA-F]{3}){1,2});/g, ':#$1;'); - const buildDir = path.join(__dirname, '..', '..', '..', 'build'); - await fs.writeFile(path.join(buildDir, 'prism.css'), fixed); + const buildDir = '../../../build'; + await fs.writeFile(new URL(`${buildDir}/prism.css`, import.meta.url), fixed); }; -main().then(() => {}); +main().then(() => {}).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/nginxconfig/build/template.js b/src/nginxconfig/build/template.js index 6681ba7..af0cbda 100644 --- a/src/nginxconfig/build/template.js +++ b/src/nginxconfig/build/template.js @@ -24,13 +24,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const path = require('path'); -const fs = require('fs'); +import fs from 'fs'; +import { URL } from 'url'; // Fetch the posthtml template and convert it to an ejs template const main = () => { - const buildDir = path.join(__dirname, '..', '..', '..', 'build'); - let template = fs.readFileSync(path.join(buildDir, 'base.html'), 'utf8'); + const buildDir = '../../../build'; + let template = fs.readFileSync(new URL(`${buildDir}/base.html`, import.meta.url), 'utf8'); // Inject our title now template = template.replace('DigitalOcean', 'NGINXConfig | DigitalOcean'); @@ -38,7 +38,7 @@ const main = () => { // Inject our app mounting point template = template.replace('', '
'); - fs.writeFileSync(path.join(buildDir, 'index.html'), template); + fs.writeFileSync(new URL(`${buildDir}/index.html`, import.meta.url), template); }; main(); diff --git a/src/nginxconfig/generators/to_yaml.js b/src/nginxconfig/generators/to_yaml.js index b83adf1..c429606 100644 --- a/src/nginxconfig/generators/to_yaml.js +++ b/src/nginxconfig/generators/to_yaml.js @@ -1,5 +1,5 @@ /* -Copyright 2020 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -24,7 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const yaml = require('json-to-pretty-yaml'); +import yaml from 'json-to-pretty-yaml'; export default yamlConf => { return yaml.stringify(yamlConf); diff --git a/src/nginxconfig/i18n/setup.js b/src/nginxconfig/i18n/setup.js index 138504b..a970043 100644 --- a/src/nginxconfig/i18n/setup.js +++ b/src/nginxconfig/i18n/setup.js @@ -1,5 +1,5 @@ /* -Copyright 2021 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -26,9 +26,7 @@ THE SOFTWARE. import Vue from 'vue'; import VueI18n from 'vue-i18n'; -import { defaultPack, defaultPackData } from '../util/language_pack_default'; -import { toSep } from '../util/language_pack_name'; -import { languagePackContext, availablePacks } from '../util/language_pack_context'; +import { defaultPack, defaultPackData, toSep, availablePacks } from '../util/language_packs'; Vue.use(VueI18n); @@ -37,32 +35,58 @@ const i18nPacks = {}; i18nPacks[defaultPack] = defaultPackData; const loadedI18nPacks = [defaultPack]; -// Load in languages data from other packs -// Use webpack magic to only build chunks for lang/languages.js -const languagesContext = require.context('.', true, /^\.\/[^/]+\/languages\.js$/, 'sync'); -for (const availablePack of availablePacks) { - if (availablePack === defaultPack) continue; - i18nPacks[availablePack] = { languages: languagesContext(`./${toSep(availablePack, '-')}/languages.js`).default }; -} +// Cache the i18n instance +let i18n = null; -export const i18n = new VueI18n({ - locale: defaultPack, - fallbackLocale: defaultPack, - messages: i18nPacks, -}); +export const getI18n = async () => { + // Use cached if available + if (i18n) return i18n; -const loadLanguagePack = pack => { + // Load in languages data from other packs + // Use webpack magic to only build chunks for lang/languages.js + // These are eagerly loaded by Webpack, so don't generate extra chunks, and return an already resolved Promise + for (const availablePack of availablePacks) { + if (availablePack === defaultPack) continue; + if (i18nPacks[availablePack]) continue; + const { default: languageData } = await import( + /* webpackInclude: /i18n\/[^/]+\/languages\.js$/ */ + /* webpackMode: "eager" */ + `./${toSep(availablePack, '-')}/languages.js` + ); + i18nPacks[availablePack] = { languages: languageData }; + } + + // Store and return the i18n instance with the loaded packs + i18n = new VueI18n({ + locale: defaultPack, + fallbackLocale: defaultPack, + messages: i18nPacks, + }); + return i18n; +}; + +const loadLanguagePack = async pack => { // If same language, do nothing if (i18n.locale === pack) return; // If language already loaded, do nothing if (loadedI18nPacks.includes(pack)) return; - // Load the pack with webpack magic - return languagePackContext(`./${toSep(pack, '-')}/index.js`).then(packData => i18nPacks[pack] = packData.default); + // Load in the full pack + // Use webpack magic to only build chunks for lang/index.js + const { default: packData } = await import( + /* webpackInclude: /i18n\/[^/]+\/index\.js$/ */ + /* webpackMode: "lazy" */ + `./${toSep(pack, '-')}/index.js` + ); + i18nPacks[pack] = packData; }; export const setLanguagePack = async pack => { + // If i18n not loaded, do nothing + if (!i18n) return; + + // Load the pack if not already loaded, and set it as active await loadLanguagePack(pack); i18n.locale = pack; }; diff --git a/src/nginxconfig/i18n/verify.js b/src/nginxconfig/i18n/verify.js index 66e54ad..ddc6e39 100644 --- a/src/nginxconfig/i18n/verify.js +++ b/src/nginxconfig/i18n/verify.js @@ -1,5 +1,5 @@ /* -Copyright 2020 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -26,19 +26,11 @@ THE SOFTWARE. import { readdirSync, readFileSync } from 'fs'; import { join, sep } from 'path'; +import { URL } from 'url'; import chalk from 'chalk'; -import { defaultPack } from '../util/language_pack_default'; -import { toSep, fromSep } from '../util/language_pack_name'; +import { defaultPack, toSep, fromSep } from '../util/language_packs'; import snakeToCamel from '../util/snake_to_camel'; -// Load all the packs in -const packs = {}; -const packDirectories = readdirSync(__dirname, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); -for (const packDirectory of packDirectories) - packs[fromSep(packDirectory, '-')] = require(`./${packDirectory}/index.js`).default; - // Recursively get all keys in a i18n pack object fragment const explore = packFragment => { const foundKeys = new Set(); @@ -61,7 +53,7 @@ const explore = packFragment => { const files = directory => { const foundFiles = new Set(); - for (const dirent of readdirSync(join(__dirname, directory), { withFileTypes: true })) { + for (const dirent of readdirSync(new URL(`./${directory}`, import.meta.url), { withFileTypes: true })) { const base = join(directory, dirent.name); // If this is a file, store it @@ -83,7 +75,7 @@ const files = directory => { // Get all the todo items in a file const todos = file => { - const content = readFileSync(join(__dirname, file), 'utf8'); + const content = readFileSync(new URL(`./${file}`, import.meta.url), 'utf8'); const lines = content.split('\n'); const items = []; @@ -105,63 +97,78 @@ const fileToObject = file => file // Replace sep with period and use camelCase .split(sep).map(dir => snakeToCamel(dir)).join('.'); -// Get all the keys for the default "source" language pack -const defaultKeys = explore(packs[defaultPack]); +const main = async () => { + // Load all the packs in + const packs = {}; + const packDirectories = readdirSync(new URL('./', import.meta.url), { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + for (const packDirectory of packDirectories) + packs[fromSep(packDirectory, '-')] = await import(`./${packDirectory}/index.js`).then(pack => pack.default); -// Track if we need to exit with an error -let hadError = false; + // Get all the keys for the default "source" language pack + const defaultKeys = explore(packs[defaultPack]); -// Work through all the packs and compare to default -for (const [pack, packData] of Object.entries(packs)) { - console.log(chalk.underline(`Language pack \`${pack}\``)); + // Track if we need to exit with an error + let hadError = false; - // Get the base data - const packKeys = explore(packData); - const packFiles = files(toSep(pack, '-')); - console.log(` Found ${packKeys.size.toLocaleString()} keys, ${packFiles.size.toLocaleString()} files`); + // Work through all the packs and compare to default + for (const [pack, packData] of Object.entries(packs)) { + console.log(chalk.underline(`Language pack \`${pack}\``)); - // Track all our errors and warnings - const errors = [], warnings = []; + // Get the base data + const packKeys = explore(packData); + const packFiles = files(toSep(pack, '-')); + console.log(` Found ${packKeys.size.toLocaleString()} keys, ${packFiles.size.toLocaleString()} files`); - // Get all the keys and the set differences - const missingKeys = [...defaultKeys].filter(x => !packKeys.has(x)); - const extraKeys = [...packKeys].filter(x => !defaultKeys.has(x)); + // Track all our errors and warnings + const errors = [], warnings = []; - // Missing keys and extra keys are errors - missingKeys.forEach(key => errors.push(`Missing key \`${key}\``)); - extraKeys.forEach(key => errors.push(`Unexpected key \`${key}\``)); + // Get all the keys and the set differences + const missingKeys = [...defaultKeys].filter(x => !packKeys.has(x)); + const extraKeys = [...packKeys].filter(x => !defaultKeys.has(x)); - // Get all the files in the pack directory - const packKeyFiles = new Set([...packFiles].filter(file => file.split(sep).slice(-1)[0] !== 'index.js')); + // Missing keys and extra keys are errors + missingKeys.forEach(key => errors.push(`Missing key \`${key}\``)); + extraKeys.forEach(key => errors.push(`Unexpected key \`${key}\``)); - // Get the objects from the pack keys - const packKeyObjects = new Set([...packKeys] - .map(key => key.split('.').slice(0, -1).join('.'))); + // Get all the files in the pack directory + const packKeyFiles = new Set([...packFiles].filter(file => file.split(sep).slice(-1)[0] !== 'index.js')); - // Warn for any files that aren't used as pack objects - [...packKeyFiles].filter(file => !packKeyObjects.has(fileToObject(file))) - .forEach(file => warnings.push(`Unused file \`${file}\``)); + // Get the objects from the pack keys + const packKeyObjects = new Set([...packKeys] + .map(key => key.split('.').slice(0, -1).join('.'))); - // Locate any todos in each file as a warning - for (const file of packFiles) - todos(file).forEach(todo => warnings.push(`TODO in \`${file}\` on line ${todo[0]}`)); + // Warn for any files that aren't used as pack objects + [...packKeyFiles].filter(file => !packKeyObjects.has(fileToObject(file))) + .forEach(file => warnings.push(`Unused file \`${file}\``)); - // Output the pack results - if (warnings.length) - for (const warning of warnings) - console.log(` ${chalk.yellow('warning')} ${warning}`); - if (errors.length) - for (const error of errors) - console.log(` ${chalk.red('error')} ${error}`); - if (!errors.length && !warnings.length) - console.log(` ${chalk.green('No issues')}`); + // Locate any todos in each file as a warning + for (const file of packFiles) + todos(file).forEach(todo => warnings.push(`TODO in \`${file}\` on line ${todo[0]}`)); - // If we had errors, script should exit 1 - if (errors.length) hadError = true; + // Output the pack results + if (warnings.length) + for (const warning of warnings) + console.log(` ${chalk.yellow('warning')} ${warning}`); + if (errors.length) + for (const error of errors) + console.log(` ${chalk.red('error')} ${error}`); + if (!errors.length && !warnings.length) + console.log(` ${chalk.green('No issues')}`); - // Linebreak before next pack or exit - console.log(chalk.reset()); -} + // If we had errors, script should exit 1 + if (errors.length) hadError = true; -// Exit 1 if we had errors -if (hadError) process.exit(1); + // Linebreak before next pack or exit + console.log(chalk.reset()); + } + + // Exit 1 if we had errors + if (hadError) process.exit(1); +}; + +main().then(() => {}).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/nginxconfig/mount.js b/src/nginxconfig/mount.js index fdb5233..b77d7c5 100644 --- a/src/nginxconfig/mount.js +++ b/src/nginxconfig/mount.js @@ -1,5 +1,5 @@ /* -Copyright 2021 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -28,11 +28,14 @@ THE SOFTWARE. import './scss/style.scss'; import Vue from 'vue'; import './util/prism_bundle'; -import { i18n } from './i18n/setup'; +import { getI18n } from './i18n/setup'; import App from './templates/app'; -// Run the app -new Vue({ - i18n, - render: h => h(App), -}).$mount('#app'); +// Load the i18n languages and run the app +getI18n().then(i18n => { + new Vue({ + i18n, + render: h => h(App), + }).$mount('#app'); +}); + diff --git a/src/nginxconfig/templates/app.vue b/src/nginxconfig/templates/app.vue index 322207a..cf46ded 100644 --- a/src/nginxconfig/templates/app.vue +++ b/src/nginxconfig/templates/app.vue @@ -133,8 +133,7 @@ THE SOFTWARE. import isObject from '../util/is_object'; import analytics from '../util/analytics'; import browserLanguage from '../util/browser_language'; - import { defaultPack } from '../util/language_pack_default'; - import { availablePacks } from '../util/language_pack_context'; + import { defaultPack, availablePacks } from '../util/language_packs'; import { setLanguagePack } from '../i18n/setup'; import generators from '../generators'; diff --git a/src/nginxconfig/util/browser_language.js b/src/nginxconfig/util/browser_language.js index 8581137..b0b5e72 100644 --- a/src/nginxconfig/util/browser_language.js +++ b/src/nginxconfig/util/browser_language.js @@ -1,5 +1,5 @@ /* -Copyright 2020 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -24,7 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { fromSep } from './language_pack_name'; +import { fromSep } from './language_packs'; export default availablePacks => { if (typeof window === 'object' && typeof window.navigator === 'object') { diff --git a/src/nginxconfig/util/language_pack_context.js b/src/nginxconfig/util/language_pack_context.js deleted file mode 100644 index 8e03759..0000000 --- a/src/nginxconfig/util/language_pack_context.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2020 DigitalOcean - -This code is licensed under the MIT License. -You may obtain a copy of the License at -https://github.com/digitalocean/nginxconfig.io/blob/master/LICENSE or https://mit-license.org/ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions : - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -import { fromSep } from './language_pack_name'; - -// Use webpack magic to only build chunks for lang/index.js, not subdirectories (e.g. lang/templates/index.js) -export const languagePackContext = require.context('../i18n', true, /^\.\/[^/]+\/index\.js$/, 'lazy'); - -// Webpack magic to get all the packs that are available -export const availablePacks = Object.freeze(languagePackContext - .keys() - .map(pack => pack.match(/^\.\/([^/]+)\/index\.js$/)) - .filter(pack => pack !== null) - .map(pack => fromSep(pack[1], '-'))); diff --git a/src/nginxconfig/util/language_pack_default.js b/src/nginxconfig/util/language_pack_default.js deleted file mode 100644 index aba0bcd..0000000 --- a/src/nginxconfig/util/language_pack_default.js +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2020 DigitalOcean - -This code is licensed under the MIT License. -You may obtain a copy of the License at -https://github.com/digitalocean/nginxconfig.io/blob/master/LICENSE or https://mit-license.org/ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions : - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - - -export const defaultPack = 'en'; - -export { default as defaultPackData } from '../i18n/en'; diff --git a/src/nginxconfig/util/language_pack_name.js b/src/nginxconfig/util/language_packs.js similarity index 73% rename from src/nginxconfig/util/language_pack_name.js rename to src/nginxconfig/util/language_packs.js index 1e485e8..3ca3713 100644 --- a/src/nginxconfig/util/language_pack_name.js +++ b/src/nginxconfig/util/language_packs.js @@ -1,5 +1,5 @@ /* -Copyright 2020 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -24,6 +24,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +export const defaultPack = 'en'; + +export { default as defaultPackData } from '../i18n/en'; + export const toSep = (pack, sep) => pack .match(/^([a-z]+)([A-Z]*)$/) .slice(1) @@ -32,3 +36,13 @@ export const toSep = (pack, sep) => pack .join(sep); export const fromSep = (pack, sep) => pack.split(sep, 2)[0].toLowerCase() + (pack.split(sep, 2)[1] || '').toUpperCase(); + +// Use webpack magic to get all the language packs we've bundled +// If not in a webpack context, return no packs +/* global __webpack_modules__ */ +export const availablePacks = typeof __webpack_modules__ === 'undefined' + ? [] + : Object.keys(__webpack_modules__) + .map(pack => pack.match(/i18n\/([^/]+)\/languages\.js$/)) + .filter(pack => pack !== null) + .map(pack => fromSep(pack[1], '-')); diff --git a/vue.config.js b/vue.config.js index e91be44..5a2ce22 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,5 +1,5 @@ /* -Copyright 2021 DigitalOcean +Copyright 2022 DigitalOcean This code is licensed under the MIT License. You may obtain a copy of the License at @@ -24,12 +24,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const path = require('path'); -const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); -const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin'); -const WebpackRequireFrom = require('webpack-require-from'); +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +import DuplicatePackageCheckerPlugin from 'duplicate-package-checker-webpack-plugin'; +import WebpackRequireFrom from 'webpack-require-from'; +import { URL, fileURLToPath } from 'url'; -module.exports = { +export default { publicPath: './', outputDir: 'dist', filenameHashing: false, // Don't hash the output, so we can embed on the DigitalOcean Community @@ -43,10 +43,10 @@ module.exports = { // Fix dynamic imports from CDN (inject as first entry point before any imports can happen) { apply: compiler => { compiler.options.entry.app.import.unshift( - path.join(__dirname, 'src', 'nginxconfig', 'build', 'webpack-dynamic-import.js'), + fileURLToPath(new URL('src/nginxconfig/build/webpack-dynamic-import.js', import.meta.url)), ); } }, - new WebpackRequireFrom({ methodName: '__webpackDynamicImportURL' }), + new WebpackRequireFrom({ methodName: '__webpackDynamicImportURL', suppressErrors: true }), // Analyze the bundle new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }), new DuplicatePackageCheckerPlugin(), @@ -68,7 +68,7 @@ module.exports = { // Use a custom HTML template config.plugin('html').tap(options => { - options[0].template = path.join(__dirname, 'build', 'index.html'); + options[0].template = fileURLToPath(new URL('build/index.html', import.meta.url)); return options; }); },