From 22b75cfad68c341ae74ccef70a7161ff03373ac3 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Mon, 30 Nov 2020 14:04:56 -0800 Subject: [PATCH 1/9] workling prerenderer --- packages/cli/lib/commands/build.js | 3 + packages/cli/lib/lib/fast-render/index.js | 67 +++++++++++++++++++ packages/cli/lib/lib/fast-render/renderer.js | 60 +++++++++++++++++ .../cli/lib/lib/webpack/render-html-plugin.js | 13 ++-- .../lib/lib/webpack/webpack-client-config.js | 4 ++ .../cli/lib/resources/body-end-modern.ejs | 13 ++++ .../cli/lib/resources/head-end-modern.ejs | 5 ++ packages/cli/package.json | 1 + yarn.lock | 64 +++++++++++++++++- 9 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 packages/cli/lib/lib/fast-render/index.js create mode 100644 packages/cli/lib/lib/fast-render/renderer.js create mode 100644 packages/cli/lib/resources/body-end-modern.ejs create mode 100644 packages/cli/lib/resources/head-end-modern.ejs diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js index f70c36f52..887bb364d 100644 --- a/packages/cli/lib/commands/build.js +++ b/packages/cli/lib/commands/build.js @@ -3,6 +3,7 @@ const { resolve } = require('path'); const { promisify } = require('util'); const { isDir, error } = require('../util'); const runWebpack = require('../lib/webpack/run-webpack'); +const fastPrerender = require('../lib/fast-render'); const toBool = val => val === void 0 || (val === 'false' ? false : val); @@ -32,4 +33,6 @@ module.exports = async function (src, argv) { if (argv.json) { await runWebpack.writeJsonStats(stats); } + + await fastPrerender(argv, stats); }; diff --git a/packages/cli/lib/lib/fast-render/index.js b/packages/cli/lib/lib/fast-render/index.js new file mode 100644 index 000000000..5b841bf26 --- /dev/null +++ b/packages/cli/lib/lib/fast-render/index.js @@ -0,0 +1,67 @@ +const { info, warn } = require('../../util'); +const { join, resolve } = require('path'); +const { Worker } = require('worker_threads'); +const { writeFile, mkdir } = require('../../fs'); +const { PRERENDER_DATA_FILE_NAME } = require('../constants'); + +module.exports = async function fastPrerender( + { src, dest, cwd, prerenderUrls }, + stats +) { + try { + let result = require(resolve(cwd, prerenderUrls)); + if (typeof result.default !== 'undefined') { + result = result.default(); + } + if (typeof result === 'function') { + result = await result(); + } + if (typeof result === 'string') { + result = JSON.parse(result); + } + + // eslint-disable-next-line no-console + console.log('\n\n'); + info( + `Prerendering ${result.length} page${ + result.length > 0 && 's' + } from ${prerenderUrls}.` + ); + + result.forEach(data => { + const webpack = { + publicPath: stats.compilation.outputOptions.publicPath, + assets: Object.keys(stats.compilation.assets), + }; + const worker = new Worker(join(__dirname, 'renderer.js'), { + workerData: { + src, + dest, + cwd, + webpack, + data, + }, + }); + worker.once('message', async result => { + worker.terminate(); + // console.log(result); + const dirPath = result.url.endsWith('.html') + ? result.url.substring(0, result.url.lastIndexOf('/')) + : result.url; + const filePath = result.url.endsWith('.html') + ? result.url + : join(result.url, 'index.html'); + await mkdir(join(dest, dirPath), { + recursive: true, + }); + await writeFile(join(dest, filePath), result.content); + await writeFile( + join(dest, dirPath, PRERENDER_DATA_FILE_NAME), + JSON.stringify(data) + ); + }); + }); + } catch (error) { + warn('Failed to load prerenderUrls file, using default!\n'); + } +}; diff --git a/packages/cli/lib/lib/fast-render/renderer.js b/packages/cli/lib/lib/fast-render/renderer.js new file mode 100644 index 000000000..6e06ac3a7 --- /dev/null +++ b/packages/cli/lib/lib/fast-render/renderer.js @@ -0,0 +1,60 @@ +const { isMainThread, parentPort, workerData } = require('worker_threads'); +const { readFile } = require('../../fs'); +const { join } = require('path'); +const prerender = require('../webpack/prerender'); + +const ejs = require('ejs'); + +async function render(src, dest, cwd, webpack, data) { + const { url, title, ...routeData } = data; + const templateSrc = await readFile(join(src, 'template.html'), 'utf-8'); + const manifest = await readFile(join(dest, 'manifest.json'), 'utf-8'); + const headEndTemplate = await readFile( + join(__dirname, '..', '..', 'resources', 'head-end-modern.ejs'), + 'utf-8' + ); + const bodyEndTemplate = await readFile( + join(__dirname, '..', '..', 'resources', 'body-end-modern.ejs'), + 'utf-8' + ); + const options = { + url, + manifest, + ssr: () => { + return prerender({ cwd, dest, src }, data); + }, + CLI_DATA: { url, ...routeData }, + webpack, + }; + const htmlWebpackPlugin = { + options: { + ...options, + title: title || manifest.name || manifest.short_name || 'Preact App', + }, + }; + + const headEnd = ejs.render(headEndTemplate, { + options, + htmlWebpackPlugin, + }); + + const bodyEnd = ejs.render(bodyEndTemplate, { + options, + htmlWebpackPlugin, + }); + const template = templateSrc + .replace(/<%\s+preact\.headEnd\s+%>/, headEnd) + .replace(/<%\s+preact\.bodyEnd\s+%>/, bodyEnd); + parentPort.postMessage({ + url, + content: ejs.render(template, { + options, + htmlWebpackPlugin, + }), + }); +} + +if (!isMainThread) { + const { src, dest, cwd, webpack, data } = workerData; + render(src, dest, cwd, webpack, data); +} diff --git a/packages/cli/lib/lib/webpack/render-html-plugin.js b/packages/cli/lib/lib/webpack/render-html-plugin.js index 55d365e87..aa8d8bede 100644 --- a/packages/cli/lib/lib/webpack/render-html-plugin.js +++ b/packages/cli/lib/lib/webpack/render-html-plugin.js @@ -55,7 +55,7 @@ module.exports = async function (config) { writeFileSync(template, content); } - const htmlWebpackConfig = (values) => { + const htmlWebpackConfig = values => { const { url, title, ...routeData } = values; // Do not create a folder if the url is for a specific file. const filename = url.endsWith('.html') @@ -107,7 +107,8 @@ module.exports = async function (config) { let pages = [{ url: '/' }]; - if (config.prerenderUrls) { + // eslint-disable-next-line no-constant-condition + if (config.prerenderUrls && false) { if (existsSync(resolve(cwd, config.prerenderUrls))) { try { let result = require(resolve(cwd, config.prerenderUrls)); @@ -151,14 +152,14 @@ module.exports = async function (config) { * And we dont have to cache every single html file. * Go easy on network usage of clients. */ - !pages.find((page) => page.url === PREACT_FALLBACK_URL) && + !pages.find(page => page.url === PREACT_FALLBACK_URL) && pages.push({ url: PREACT_FALLBACK_URL }); const resultPages = pages .map(htmlWebpackConfig) - .map((conf) => new HtmlWebpackPlugin(conf)) + .map(conf => new HtmlWebpackPlugin(conf)) .concat([new HtmlWebpackExcludeAssetsPlugin()]) - .concat([...pages.map((page) => new PrerenderDataExtractPlugin(page))]); + .concat([...pages.map(page => new PrerenderDataExtractPlugin(page))]); return resultPages; }; @@ -171,7 +172,7 @@ class PrerenderDataExtractPlugin { this.data_ = JSON.stringify(cliData.preRenderData || {}); } apply(compiler) { - compiler.hooks.emit.tap('PrerenderDataExtractPlugin', (compilation) => { + compiler.hooks.emit.tap('PrerenderDataExtractPlugin', compilation => { if (this.location_ === `${PREACT_FALLBACK_URL}/`) { // We dont build prerender data for `200.html`. It can re-use the one for homepage. return; diff --git a/packages/cli/lib/lib/webpack/webpack-client-config.js b/packages/cli/lib/lib/webpack/webpack-client-config.js index 2add797cf..bc39ecaf4 100644 --- a/packages/cli/lib/lib/webpack/webpack-client-config.js +++ b/packages/cli/lib/lib/webpack/webpack-client-config.js @@ -18,6 +18,7 @@ const { InjectManifest } = require('workbox-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const RefreshPlugin = require('@prefresh/webpack'); const { normalizePath, warn } = require('../../util'); +const FileManifestPlugin = require('./file-manifest'); const cleanFilename = name => name.replace( @@ -85,6 +86,9 @@ async function clientConfig(env) { plugins: [ new PushManifestPlugin(env), ...(await renderHTMLPlugin(env)), + new FileManifestPlugin({ + fileName: 'file-manifest.json', + }), ...getBabelEsmPlugin(env), new CopyWebpackPlugin( [ diff --git a/packages/cli/lib/resources/body-end-modern.ejs b/packages/cli/lib/resources/body-end-modern.ejs new file mode 100644 index 000000000..5fc095d96 --- /dev/null +++ b/packages/cli/lib/resources/body-end-modern.ejs @@ -0,0 +1,13 @@ +<%- options.ssr() %> + +<% /* Fix for safari < 11 nomodule bug. TODO: Do the following only for safari. */ %> + + +<% + /*Fetch and Promise polyfills are not needed for browsers that support type=module + Please re-evaluate below line if adding more polyfills.*/ +%> + + diff --git a/packages/cli/lib/resources/head-end-modern.ejs b/packages/cli/lib/resources/head-end-modern.ejs new file mode 100644 index 000000000..6736b4df4 --- /dev/null +++ b/packages/cli/lib/resources/head-end-modern.ejs @@ -0,0 +1,5 @@ + +<% if (options.manifest.theme_color) { %> + +<% } %> + diff --git a/packages/cli/package.json b/packages/cli/package.json index 95da49dcf..b281eb94a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -135,6 +135,7 @@ "webpack-bundle-analyzer": "^3.3.2", "webpack-dev-server": "^3.4.1", "webpack-fix-style-only-entries": "^0.5.1", + "webpack-manifest-plugin": "^2.2.0", "webpack-merge": "^4.1.0", "webpack-plugin-replace": "^1.2.0", "which": "^2.0.2", diff --git a/yarn.lock b/yarn.lock index f10cda2b7..b9f4b259d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3860,6 +3860,14 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +call-bind@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce" + integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.0" + call-limit@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4" @@ -5724,6 +5732,24 @@ es-abstract@^1.18.0-next.0: string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" +es-abstract@^1.18.0-next.1: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -6516,7 +6542,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^7.0.1: +fs-extra@^7.0.0, fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -6663,6 +6689,15 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be" + integrity sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -7679,6 +7714,11 @@ is-callable@^1.1.4, is-callable@^1.2.0: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" @@ -9248,7 +9288,7 @@ lodash.without@~4.4.0: resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.10: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.10: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -10493,6 +10533,16 @@ object.assign@^4.1.1: has-symbols "^1.0.1" object-keys "^1.1.1" +object.entries@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6" + integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + object.entries@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" @@ -14662,6 +14712,16 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" +webpack-manifest-plugin@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz#19ca69b435b0baec7e29fbe90fb4015de2de4f16" + integrity sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ== + dependencies: + fs-extra "^7.0.0" + lodash ">=3.5 <5" + object.entries "^1.1.0" + tapable "^1.0.0" + webpack-merge@^4.1.0: version "4.2.2" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" From 565cea0a4340fcc8fe55a85ecc68c9f68784d3e1 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Mon, 30 Nov 2020 14:22:40 -0800 Subject: [PATCH 2/9] adding html minification --- packages/cli/lib/lib/fast-render/renderer.js | 15 ++++++---- .../lib/lib/webpack/webpack-client-config.js | 4 --- packages/cli/package.json | 1 + yarn.lock | 30 +++++++++++++++---- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/cli/lib/lib/fast-render/renderer.js b/packages/cli/lib/lib/fast-render/renderer.js index 6e06ac3a7..e7509581d 100644 --- a/packages/cli/lib/lib/fast-render/renderer.js +++ b/packages/cli/lib/lib/fast-render/renderer.js @@ -2,8 +2,8 @@ const { isMainThread, parentPort, workerData } = require('worker_threads'); const { readFile } = require('../../fs'); const { join } = require('path'); const prerender = require('../webpack/prerender'); - const ejs = require('ejs'); +const { minify } = require('html-minifier'); async function render(src, dest, cwd, webpack, data) { const { url, title, ...routeData } = data; @@ -47,10 +47,15 @@ async function render(src, dest, cwd, webpack, data) { .replace(/<%\s+preact\.bodyEnd\s+%>/, bodyEnd); parentPort.postMessage({ url, - content: ejs.render(template, { - options, - htmlWebpackPlugin, - }), + content: minify( + ejs.render(template, { + options, + htmlWebpackPlugin, + }), + { + collapseWhitespace: true, + } + ), }); } diff --git a/packages/cli/lib/lib/webpack/webpack-client-config.js b/packages/cli/lib/lib/webpack/webpack-client-config.js index bc39ecaf4..2add797cf 100644 --- a/packages/cli/lib/lib/webpack/webpack-client-config.js +++ b/packages/cli/lib/lib/webpack/webpack-client-config.js @@ -18,7 +18,6 @@ const { InjectManifest } = require('workbox-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const RefreshPlugin = require('@prefresh/webpack'); const { normalizePath, warn } = require('../../util'); -const FileManifestPlugin = require('./file-manifest'); const cleanFilename = name => name.replace( @@ -86,9 +85,6 @@ async function clientConfig(env) { plugins: [ new PushManifestPlugin(env), ...(await renderHTMLPlugin(env)), - new FileManifestPlugin({ - fileName: 'file-manifest.json', - }), ...getBabelEsmPlugin(env), new CopyWebpackPlugin( [ diff --git a/packages/cli/package.json b/packages/cli/package.json index b281eb94a..52201bcf2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -100,6 +100,7 @@ "get-port": "^5.0.0", "gittar": "^0.1.0", "glob": "^7.1.4", + "html-minifier": "^4.0.0", "html-webpack-exclude-assets-plugin": "0.0.7", "html-webpack-plugin": "^3.2.0", "ip": "^1.1.5", diff --git a/yarn.lock b/yarn.lock index b9f4b259d..74c90feae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3902,7 +3902,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@3.0.x: +camel-case@3.0.x, camel-case@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= @@ -4139,7 +4139,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -clean-css@4.2.x: +clean-css@4.2.x, clean-css@^4.2.1: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== @@ -4369,7 +4369,7 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.18.0, commander@^2.20.0: +commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -7122,7 +7122,7 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -he@1.2.x: +he@1.2.x, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -7223,6 +7223,19 @@ html-minifier@^3.2.3: relateurl "0.2.x" uglify-js "3.4.x" +html-minifier@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56" + integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig== + dependencies: + camel-case "^3.0.0" + clean-css "^4.2.1" + commander "^2.19.0" + he "^1.2.0" + param-case "^2.1.1" + relateurl "^0.2.7" + uglify-js "^3.5.1" + html-webpack-exclude-assets-plugin@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/html-webpack-exclude-assets-plugin/-/html-webpack-exclude-assets-plugin-0.0.7.tgz#ee69906adb3d869e4e29f29b0f3e99b53fa87c99" @@ -10954,7 +10967,7 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -param-case@2.1.x: +param-case@2.1.x, param-case@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= @@ -12312,7 +12325,7 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" -relateurl@0.2.x: +relateurl@0.2.x, relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= @@ -14200,6 +14213,11 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.4.tgz#dd680f5687bc0d7a93b14a3482d16db6eba2bfbb" integrity sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw== +uglify-js@^3.5.1: + version "3.12.1" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.1.tgz#78307f539f7b9ca5557babb186ea78ad30cc0375" + integrity sha512-o8lHP20KjIiQe5b/67Rh68xEGRrc2SRsCuuoYclXXoC74AfSRGblU1HKzJWH3HxPZ+Ort85fWHpSX7KwBUC9CQ== + uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" From 4e278be587c9ad49fa6a3bf277c66d8ebbfc1189 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 1 Dec 2020 10:51:25 -0800 Subject: [PATCH 3/9] perf improvement for pages with huge data --- packages/cli/lib/resources/body-end-modern.ejs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/lib/resources/body-end-modern.ejs b/packages/cli/lib/resources/body-end-modern.ejs index 5fc095d96..3ae32ece3 100644 --- a/packages/cli/lib/resources/body-end-modern.ejs +++ b/packages/cli/lib/resources/body-end-modern.ejs @@ -1,7 +1,4 @@ <%- options.ssr() %> - <% /* Fix for safari < 11 nomodule bug. TODO: Do the following only for safari. */ %> @@ -11,3 +8,6 @@ %> + From 65058b5c0def4a78d5f9c4998bdad07d121b0d49 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Fri, 4 Dec 2020 13:22:34 -0800 Subject: [PATCH 4/9] pooled worker thread --- packages/cli/lib/lib/fast-render/index.js | 70 ++++++++++++-------- packages/cli/lib/lib/fast-render/pool.js | 25 +++++++ packages/cli/lib/lib/fast-render/renderer.js | 8 ++- packages/cli/package.json | 1 + yarn.lock | 5 ++ 5 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 packages/cli/lib/lib/fast-render/pool.js diff --git a/packages/cli/lib/lib/fast-render/index.js b/packages/cli/lib/lib/fast-render/index.js index 5b841bf26..8533bdf74 100644 --- a/packages/cli/lib/lib/fast-render/index.js +++ b/packages/cli/lib/lib/fast-render/index.js @@ -3,6 +3,7 @@ const { join, resolve } = require('path'); const { Worker } = require('worker_threads'); const { writeFile, mkdir } = require('../../fs'); const { PRERENDER_DATA_FILE_NAME } = require('../constants'); +const pool = require('./pool'); module.exports = async function fastPrerender( { src, dest, cwd, prerenderUrls }, @@ -27,39 +28,54 @@ module.exports = async function fastPrerender( result.length > 0 && 's' } from ${prerenderUrls}.` ); - - result.forEach(data => { - const webpack = { - publicPath: stats.compilation.outputOptions.publicPath, - assets: Object.keys(stats.compilation.assets), - }; - const worker = new Worker(join(__dirname, 'renderer.js'), { - workerData: { + const pageData = {}; + const renderedContents = await pool( + result.map(data => { + pageData[data.url] = data; + return { src, dest, cwd, - webpack, + webpack: { + publicPath: stats.compilation.outputOptions.publicPath, + assets: Object.keys(stats.compilation.assets), + }, data, - }, - }); - worker.once('message', async result => { - worker.terminate(); - // console.log(result); - const dirPath = result.url.endsWith('.html') - ? result.url.substring(0, result.url.lastIndexOf('/')) - : result.url; - const filePath = result.url.endsWith('.html') - ? result.url - : join(result.url, 'index.html'); - await mkdir(join(dest, dirPath), { - recursive: true, + }; + }), + ({ src, dest, cwd, webpack, data }) => { + return new Promise(resolve => { + const worker = new Worker(join(__dirname, 'renderer.js'), { + workerData: { + src, + dest, + cwd, + webpack, + data, + }, + }); + worker.once('message', async result => { + worker.terminate(); + resolve(result); + }); }); - await writeFile(join(dest, filePath), result.content); - await writeFile( - join(dest, dirPath, PRERENDER_DATA_FILE_NAME), - JSON.stringify(data) - ); + } + ); + renderedContents.forEach(async result => { + const dirPath = result.url.endsWith('.html') + ? result.url.substring(0, result.url.lastIndexOf('/')) + : result.url; + const filePath = result.url.endsWith('.html') + ? result.url + : join(result.url, 'index.html'); + await mkdir(join(dest, dirPath), { + recursive: true, }); + await writeFile(join(dest, filePath), result.content); + await writeFile( + join(dest, dirPath, PRERENDER_DATA_FILE_NAME), + JSON.stringify(pageData[result.url]) + ); }); } catch (error) { warn('Failed to load prerenderUrls file, using default!\n'); diff --git a/packages/cli/lib/lib/fast-render/pool.js b/packages/cli/lib/lib/fast-render/pool.js new file mode 100644 index 000000000..37899ad06 --- /dev/null +++ b/packages/cli/lib/lib/fast-render/pool.js @@ -0,0 +1,25 @@ +const { cpus } = require('os'); +const resolved = Promise.resolve(); +module.exports = async function pool( + items, + iteratorFn, + concurrency = cpus().length +) { + const itemsLength = items.length; + const returnable = []; + const executing = []; + for (const item of items) { + const promise = resolved.then(() => iteratorFn(item, items)); + returnable.push(promise); + if (concurrency <= itemsLength) { + const execute = promise.then(() => + executing.splice(executing.indexOf(execute), 1) + ); + executing.push(execute); + if (executing.length >= concurrency) { + await Promise.race(executing); + } + } + } + return Promise.all(returnable); +}; diff --git a/packages/cli/lib/lib/fast-render/renderer.js b/packages/cli/lib/lib/fast-render/renderer.js index e7509581d..6eb9f183e 100644 --- a/packages/cli/lib/lib/fast-render/renderer.js +++ b/packages/cli/lib/lib/fast-render/renderer.js @@ -21,7 +21,13 @@ async function render(src, dest, cwd, webpack, data) { url, manifest, ssr: () => { - return prerender({ cwd, dest, src }, data); + const params = { + ...data, + CLI_DATA: { + preRenderData: data, + }, + }; + return prerender({ cwd, dest, src }, params); }, CLI_DATA: { url, ...routeData }, webpack, diff --git a/packages/cli/package.json b/packages/cli/package.json index 52201bcf2..91be90b00 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -77,6 +77,7 @@ "@babel/plugin-transform-react-jsx": "^7.9.0", "@babel/preset-env": "^7.9.0", "@babel/preset-typescript": "^7.9.0", + "@kristoferbaxter/async": "^1.0.0", "@preact/async-loader": "^3.0.1", "@prefresh/webpack": "^1.0.2", "autoprefixer": "^9.6.0", diff --git a/yarn.lock b/yarn.lock index 74c90feae..79ba32fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1458,6 +1458,11 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@kristoferbaxter/async@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@kristoferbaxter/async/-/async-1.0.0.tgz#33c8e8c6b055f15f4ee8a711b9594336a6fb1671" + integrity sha512-CAQQQ8mUUBKI2P7stYojHwHb+ShdFN8SrC90/96V4m2XlDSctXAA+5PD5CqCICs0o5c/83cYJCeub+FSetKLrw== + "@lerna/add@3.21.0": version "3.21.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" From fdd975dd1e7464a3f341ac95ac918408d001554f Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Fri, 4 Dec 2020 17:04:30 -0800 Subject: [PATCH 5/9] adding an experimental flag --- packages/cli/lib/commands/build.js | 4 +- packages/cli/lib/lib/fast-render/index.js | 139 ++++++++++-------- .../cli/lib/lib/webpack/render-html-plugin.js | 2 +- 3 files changed, 78 insertions(+), 67 deletions(-) diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js index 887bb364d..97bc95626 100644 --- a/packages/cli/lib/commands/build.js +++ b/packages/cli/lib/commands/build.js @@ -34,5 +34,7 @@ module.exports = async function (src, argv) { await runWebpack.writeJsonStats(stats); } - await fastPrerender(argv, stats); + if (argv['experimental-fast-rendering']) { + await fastPrerender(argv, stats); + } }; diff --git a/packages/cli/lib/lib/fast-render/index.js b/packages/cli/lib/lib/fast-render/index.js index 8533bdf74..dec5f6cdd 100644 --- a/packages/cli/lib/lib/fast-render/index.js +++ b/packages/cli/lib/lib/fast-render/index.js @@ -9,75 +9,84 @@ module.exports = async function fastPrerender( { src, dest, cwd, prerenderUrls }, stats ) { - try { - let result = require(resolve(cwd, prerenderUrls)); - if (typeof result.default !== 'undefined') { - result = result.default(); - } - if (typeof result === 'function') { - result = await result(); - } - if (typeof result === 'string') { - result = JSON.parse(result); + let pages = [{ url: '/' }]; + if (prerenderUrls) { + try { + let result = require(resolve(cwd, prerenderUrls)); + if (typeof result.default !== 'undefined') { + result = result.default(); + } + if (typeof result === 'function') { + result = await result(); + } + if (typeof result === 'string') { + result = JSON.parse(result); + } + if (result instanceof Array) { + pages = result; + } + } catch (error) { + warn('Failed to load prerenderUrls file, using default!\n'); } + } - // eslint-disable-next-line no-console - console.log('\n\n'); - info( - `Prerendering ${result.length} page${ - result.length > 0 && 's' - } from ${prerenderUrls}.` - ); - const pageData = {}; - const renderedContents = await pool( - result.map(data => { - pageData[data.url] = data; - return { - src, - dest, - cwd, - webpack: { - publicPath: stats.compilation.outputOptions.publicPath, - assets: Object.keys(stats.compilation.assets), + // eslint-disable-next-line no-console + console.log('\n\n'); + info( + `Prerendering ${pages.length} page${ + pages.length > 0 && 's' + } from ${prerenderUrls}.` + ); + const pageData = {}; + const renderedContents = await pool( + pages.map(data => { + pageData[data.url] = data; + return { + src, + dest, + cwd, + webpack: { + publicPath: stats.compilation.outputOptions.publicPath, + assets: Object.keys(stats.compilation.assets), + }, + data, + }; + }), + ({ src, dest, cwd, webpack, data }) => { + return new Promise(resolve => { + const worker = new Worker(join(__dirname, 'renderer.js'), { + workerData: { + src, + dest, + cwd, + webpack, + data, }, - data, - }; - }), - ({ src, dest, cwd, webpack, data }) => { - return new Promise(resolve => { - const worker = new Worker(join(__dirname, 'renderer.js'), { - workerData: { - src, - dest, - cwd, - webpack, - data, - }, - }); - worker.once('message', async result => { - worker.terminate(); - resolve(result); - }); }); - } - ); - renderedContents.forEach(async result => { - const dirPath = result.url.endsWith('.html') - ? result.url.substring(0, result.url.lastIndexOf('/')) - : result.url; - const filePath = result.url.endsWith('.html') - ? result.url - : join(result.url, 'index.html'); - await mkdir(join(dest, dirPath), { - recursive: true, + worker.once('message', async preRenderContent => { + worker.terminate(); + resolve(preRenderContent); + }); }); - await writeFile(join(dest, filePath), result.content); - await writeFile( - join(dest, dirPath, PRERENDER_DATA_FILE_NAME), - JSON.stringify(pageData[result.url]) - ); + } + ); + renderedContents.forEach(async preRenderedContent => { + const dirPath = preRenderedContent.url.endsWith('.html') + ? preRenderedContent.url.substring( + 0, + preRenderedContent.url.lastIndexOf('/') + ) + : preRenderedContent.url; + const filePath = preRenderedContent.url.endsWith('.html') + ? preRenderedContent.url + : join(preRenderedContent.url, 'index.html'); + await mkdir(join(dest, dirPath), { + recursive: true, }); - } catch (error) { - warn('Failed to load prerenderUrls file, using default!\n'); - } + await writeFile(join(dest, filePath), preRenderedContent.content); + await writeFile( + join(dest, dirPath, PRERENDER_DATA_FILE_NAME), + JSON.stringify(pageData[preRenderedContent.url]) + ); + }); }; diff --git a/packages/cli/lib/lib/webpack/render-html-plugin.js b/packages/cli/lib/lib/webpack/render-html-plugin.js index aa8d8bede..1e2828e0b 100644 --- a/packages/cli/lib/lib/webpack/render-html-plugin.js +++ b/packages/cli/lib/lib/webpack/render-html-plugin.js @@ -108,7 +108,7 @@ module.exports = async function (config) { let pages = [{ url: '/' }]; // eslint-disable-next-line no-constant-condition - if (config.prerenderUrls && false) { + if (config.prerenderUrls && !config['experimental-fast-rendering']) { if (existsSync(resolve(cwd, config.prerenderUrls))) { try { let result = require(resolve(cwd, config.prerenderUrls)); From c5f55cfe04720f919a32232eb8f3fac3ea90eb34 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Fri, 4 Dec 2020 17:14:42 -0800 Subject: [PATCH 6/9] adding in valid args list --- packages/cli/lib/commands/build.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js index 923b91498..00325e030 100644 --- a/packages/cli/lib/commands/build.js +++ b/packages/cli/lib/commands/build.js @@ -80,6 +80,10 @@ const options = [ name: '-v, --verbose', description: 'Verbose output', }, + { + name: '--experimental-fast-rendering', + description: 'Experimental pre-rendering. Highly unstable!', + }, ]; async function command(src, argv) { From b55db466e48fc289d6168402a1512738c1d59ef3 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 8 Dec 2020 12:41:37 -0800 Subject: [PATCH 7/9] fixing title --- packages/cli/lib/lib/fast-render/renderer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/lib/lib/fast-render/renderer.js b/packages/cli/lib/lib/fast-render/renderer.js index 6eb9f183e..057a49564 100644 --- a/packages/cli/lib/lib/fast-render/renderer.js +++ b/packages/cli/lib/lib/fast-render/renderer.js @@ -49,6 +49,7 @@ async function render(src, dest, cwd, webpack, data) { htmlWebpackPlugin, }); const template = templateSrc + .replace(/<%[=]?\s+preact\.title\s+%>/, '<%= options.title %>') .replace(/<%\s+preact\.headEnd\s+%>/, headEnd) .replace(/<%\s+preact\.bodyEnd\s+%>/, bodyEnd); parentPort.postMessage({ From 7b7a6c80f6ceab43c3a85990656f8f2c2d918bd3 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 8 Dec 2020 13:26:25 -0800 Subject: [PATCH 8/9] adding test --- packages/cli/lib/commands/build.js | 5 +++- packages/cli/tests/build.test.js | 21 +++++++++++++++ .../subjects/experimental-rendering/index.js | 3 +++ .../experimental-rendering/manifest.json | 26 +++++++++++++++++++ .../experimental-rendering/package.json | 4 +++ .../experimental-rendering/template.html | 15 +++++++++++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/cli/tests/subjects/experimental-rendering/index.js create mode 100644 packages/cli/tests/subjects/experimental-rendering/manifest.json create mode 100644 packages/cli/tests/subjects/experimental-rendering/package.json create mode 100644 packages/cli/tests/subjects/experimental-rendering/template.html diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js index 00325e030..eac184264 100644 --- a/packages/cli/lib/commands/build.js +++ b/packages/cli/lib/commands/build.js @@ -1,7 +1,7 @@ const rimraf = require('rimraf'); const { resolve } = require('path'); const { promisify } = require('util'); -const { isDir, error } = require('../util'); +const { isDir, error, warn } = require('../util'); const runWebpack = require('../lib/webpack/run-webpack'); const fastPrerender = require('../lib/fast-render'); const { validateArgs } = require('./validate-args'); @@ -115,6 +115,9 @@ async function command(src, argv) { } if (argv['experimental-fast-rendering']) { + warn( + 'You have enabled experiment fast rendering. You might have to adjust template.html according to the new API.' + ); await fastPrerender(argv, stats); } } diff --git a/packages/cli/tests/build.test.js b/packages/cli/tests/build.test.js index 65b4ddb12..5576d74c3 100644 --- a/packages/cli/tests/build.test.js +++ b/packages/cli/tests/build.test.js @@ -244,4 +244,25 @@ describe('preact build', () => { expect(mockExit).toHaveBeenCalledWith(1); mockExit.mockRestore(); }); + + it.only('should build with experimental fast rendering', async () => { + let dir = await subject('experimental-rendering'); + await build(dir); + const buildDir = join(dir, 'build'); + const prerenderedContentByHTMLWebpackPlugin = await readFile( + join(buildDir, 'index.html'), + 'utf-8' + ); + dir = await subject('experimental-rendering'); + await build(dir, { + 'experimental-fast-rendering': true, + }); + const prerenderedContentByExperimentalRendered = await readFile( + join(buildDir, 'index.html'), + 'utf-8' + ); + expect(prerenderedContentByHTMLWebpackPlugin).toEqual( + prerenderedContentByExperimentalRendered + ); + }); }); diff --git a/packages/cli/tests/subjects/experimental-rendering/index.js b/packages/cli/tests/subjects/experimental-rendering/index.js new file mode 100644 index 000000000..dcf3bb109 --- /dev/null +++ b/packages/cli/tests/subjects/experimental-rendering/index.js @@ -0,0 +1,3 @@ +import { h } from 'preact'; + +export default () =>

This is an app with custom template

; diff --git a/packages/cli/tests/subjects/experimental-rendering/manifest.json b/packages/cli/tests/subjects/experimental-rendering/manifest.json new file mode 100644 index 000000000..307721828 --- /dev/null +++ b/packages/cli/tests/subjects/experimental-rendering/manifest.json @@ -0,0 +1,26 @@ +{ + "name": "Experimental renderer", + "short_name": "Experimental renderer", + "start_url": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#fff", + "theme_color": "#fff", + "icons": [ + { + "src": "/assets/logo/android-chrome-192x192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/assets/logo/android-chrome-512x512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/assets/logo/apple-touch-icon.png", + "type": "image/png", + "sizes": "180x180" + } + ] +} diff --git a/packages/cli/tests/subjects/experimental-rendering/package.json b/packages/cli/tests/subjects/experimental-rendering/package.json new file mode 100644 index 000000000..94671fbec --- /dev/null +++ b/packages/cli/tests/subjects/experimental-rendering/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "name": "preact-webpack" +} diff --git a/packages/cli/tests/subjects/experimental-rendering/template.html b/packages/cli/tests/subjects/experimental-rendering/template.html new file mode 100644 index 000000000..770c48b2b --- /dev/null +++ b/packages/cli/tests/subjects/experimental-rendering/template.html @@ -0,0 +1,15 @@ + + + + + <% preact.title %> + + + + + <% preact.headEnd %> + + + <% preact.bodyEnd %> + + From 963b3ecac11cfb210326343d44432490224ff553 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 8 Dec 2020 13:29:25 -0800 Subject: [PATCH 9/9] bug fix --- packages/cli/tests/build.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/tests/build.test.js b/packages/cli/tests/build.test.js index 5576d74c3..f10313a24 100644 --- a/packages/cli/tests/build.test.js +++ b/packages/cli/tests/build.test.js @@ -245,7 +245,7 @@ describe('preact build', () => { mockExit.mockRestore(); }); - it.only('should build with experimental fast rendering', async () => { + it('should build with experimental fast rendering', async () => { let dir = await subject('experimental-rendering'); await build(dir); const buildDir = join(dir, 'build');