diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index 9579c8240..4c8a211d0 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -38,13 +38,7 @@ const runProductionBuild = async (compilation) => { }); } - await Promise.all( - servers.map(async (server) => { - await server.start(); - - return Promise.resolve(server); - }), - ); + await Promise.all(servers.map((server) => server.start())); if (prerenderPlugin.executeModuleUrl) { await preRenderCompilationWorker(compilation, prerenderPlugin); diff --git a/packages/cli/src/lib/async-utils.js b/packages/cli/src/lib/async-utils.js index 9cedd3626..1c086853b 100644 --- a/packages/cli/src/lib/async-utils.js +++ b/packages/cli/src/lib/async-utils.js @@ -1,27 +1,33 @@ // https://stackoverflow.com/a/76974728/417806 +// Constraint: callback functions must not depend on each other async function asyncFilter(arr, cb) { const filtered = []; - for (const element of arr) { + await asyncForEach(arr, async (element) => { const needAdd = await cb(element); if (needAdd) { filtered.push(element); } - } + }); return filtered; } // https://stackoverflow.com/a/71278238/417806 +// Constraint: mapper functions must not depend on each other async function asyncMap(items, mapper) { - const mappedItems = []; + const promises = []; for (const item of items) { - mappedItems.push(await mapper(item)); + promises.push(mapper(item)); } - return mappedItems; + return await Promise.all(promises); +} + +async function asyncForEach(items, callback) { + await asyncMap(items, callback); } -export { asyncFilter, asyncMap }; +export { asyncFilter, asyncMap, asyncForEach }; diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index e72146e62..60ab9f215 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -16,6 +16,7 @@ import { import path from "node:path"; import { rollup } from "rollup"; import { pruneGraph } from "../lib/content-utils.js"; +import { asyncForEach } from "../lib/async-utils.js"; async function interceptPage(url, request, plugins, body) { let response = new Response(body, { @@ -101,7 +102,7 @@ async function emitResources(compilation) { async function cleanUpResources(compilation) { const { outputDir } = compilation.context; - for (const resource of compilation.resources.values()) { + await asyncForEach(compilation.resources.values(), async (resource) => { const { src, optimizedFileName, optimizationAttr } = resource; const optConfig = ["inline", "static"].indexOf(compilation.config.optimization) >= 0; const optAttr = ["inline", "static"].indexOf(optimizationAttr) >= 0; @@ -109,57 +110,53 @@ async function cleanUpResources(compilation) { if (optimizedFileName && (!src || optAttr || optConfig)) { await fs.unlink(new URL(`./${optimizedFileName}`, outputDir)); } - } + }); } async function optimizeStaticPages(compilation, plugins) { const { scratchDir, outputDir } = compilation.context; - return Promise.all( - compilation.graph - .filter( - (page) => - !page.isSSR || - (page.isSSR && page.prerender) || - (page.isSSR && compilation.config.prerender), - ) - .map(async (page) => { - const { route, outputHref } = page; - const outputDirUrl = new URL(outputHref.replace("index.html", "").replace("404.html", "")); - const url = new URL(`http://localhost:${compilation.config.port}${route}`); - const contents = await fs.readFile( - new URL(`./${outputHref.replace(outputDir.href, "")}`, scratchDir), - "utf-8", - ); - const headers = new Headers({ "Content-Type": "text/html" }); - let response = new Response(contents, { headers }); - - if (!(await checkResourceExists(outputDirUrl))) { - await fs.mkdir(outputDirUrl, { - recursive: true, - }); - } + const pages = compilation.graph.filter( + (page) => + !page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender), + ); - for (const plugin of plugins) { - if (plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone()))) { - const currentResponse = await plugin.optimize(url, response.clone()); + await asyncForEach(pages, async (page) => { + const { route, outputHref } = page; + const outputDirUrl = new URL(outputHref.replace("index.html", "").replace("404.html", "")); + const url = new URL(`http://localhost:${compilation.config.port}${route}`); + const contents = await fs.readFile( + new URL(`./${outputHref.replace(outputDir.href, "")}`, scratchDir), + "utf-8", + ); + const headers = new Headers({ "Content-Type": "text/html" }); + let response = new Response(contents, { headers }); + + if (!(await checkResourceExists(outputDirUrl))) { + await fs.mkdir(outputDirUrl, { + recursive: true, + }); + } - response = mergeResponse(response.clone(), currentResponse.clone()); - } - } + for (const plugin of plugins) { + if (plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone()))) { + const currentResponse = await plugin.optimize(url, response.clone()); - // clean up optimization markers - const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, ""); + response = mergeResponse(response.clone(), currentResponse.clone()); + } + } - await fs.writeFile(new URL(outputHref), body); - }), - ); + // clean up optimization markers + const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, ""); + + await fs.writeFile(new URL(outputHref), body); + }); } async function bundleStyleResources(compilation, resourcePlugins) { const { outputDir } = compilation.context; - for (const resource of compilation.resources.values()) { + await asyncForEach(compilation.resources.values(), async (resource) => { const { contents, src = "", type } = resource; if (["style", "link"].includes(type)) { @@ -202,84 +199,58 @@ async function bundleStyleResources(compilation, resourcePlugins) { const request = new Request(url, { headers }); const initResponse = new Response(contents, { headers }); - let response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + let response = initResponse; + + for (const plugin of resourcePlugins) { const shouldServe = plugin.shouldServe && (await plugin.shouldServe(url, request)); if (shouldServe) { const currentResponse = await plugin.serve(url, request); - const mergedResponse = mergeResponse( - intermediateResponse.clone(), - currentResponse.clone(), - ); + const mergedResponse = mergeResponse(response.clone(), currentResponse.clone()); if (mergedResponse.headers.get("Content-Type").indexOf(contentType) >= 0) { - return Promise.resolve(mergedResponse.clone()); + response = mergedResponse.clone(); } } + } - return Promise.resolve(responsePromise); - }, Promise.resolve(initResponse)); - - response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + for (const plugin of resourcePlugins) { const shouldPreIntercept = plugin.shouldPreIntercept && - (await plugin.shouldPreIntercept(url, request, intermediateResponse.clone())); + (await plugin.shouldPreIntercept(url, request, response.clone())); if (shouldPreIntercept) { - const currentResponse = await plugin.preIntercept( - url, - request, - intermediateResponse.clone(), - ); - const mergedResponse = mergeResponse( - intermediateResponse.clone(), - currentResponse.clone(), - ); + const currentResponse = await plugin.preIntercept(url, request, response.clone()); + const mergedResponse = mergeResponse(response.clone(), currentResponse.clone()); if (mergedResponse.headers.get("Content-Type").indexOf(contentType) >= 0) { - return Promise.resolve(mergedResponse.clone()); + response = mergedResponse.clone(); } } + } - return Promise.resolve(responsePromise); - }, Promise.resolve(response.clone())); - - response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + for (const plugin of resourcePlugins) { const shouldIntercept = - plugin.shouldIntercept && - (await plugin.shouldIntercept(url, request, intermediateResponse.clone())); + plugin.shouldIntercept && (await plugin.shouldIntercept(url, request, response.clone())); if (shouldIntercept) { - const currentResponse = await plugin.intercept( - url, - request, - intermediateResponse.clone(), - ); - const mergedResponse = mergeResponse( - intermediateResponse.clone(), - currentResponse.clone(), - ); + const currentResponse = await plugin.intercept(url, request, response.clone()); + const mergedResponse = mergeResponse(response.clone(), currentResponse.clone()); if (mergedResponse.headers.get("Content-Type").indexOf(contentType) >= 0) { - return Promise.resolve(mergedResponse.clone()); + response = mergedResponse.clone(); } } + } - return Promise.resolve(responsePromise); - }, Promise.resolve(response.clone())); - - response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + for (const plugin of resourcePlugins) { const shouldOptimize = - plugin.shouldOptimize && (await plugin.shouldOptimize(url, intermediateResponse.clone())); + plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone())); - return shouldOptimize - ? Promise.resolve(await plugin.optimize(url, intermediateResponse.clone())) - : Promise.resolve(responsePromise); - }, Promise.resolve(response.clone())); + if (shouldOptimize) { + response = await plugin.optimize(url, response.clone()); + } + } optimizedFileContents = await response.text(); @@ -291,7 +262,7 @@ async function bundleStyleResources(compilation, resourcePlugins) { await fs.writeFile(new URL(`./${optimizedFileName}`, outputDir), optimizedFileContents); } - } + }); } async function bundleApiRoutes(compilation) { @@ -300,11 +271,10 @@ async function bundleApiRoutes(compilation) { if (apiConfigs.length > 0 && apiConfigs[0].input.length !== 0) { console.info("bundling API routes..."); - for (const configIndex in apiConfigs) { - const rollupConfig = apiConfigs[configIndex]; + await asyncForEach(apiConfigs, async (rollupConfig) => { const bundle = await rollup(rollupConfig); await bundle.write(rollupConfig.output); - } + }); } } @@ -326,7 +296,7 @@ async function bundleSsrPages(compilation, optimizePlugins) { // one pass to generate initial static HTML and to track all combined static resources across layouts // and before we optimize so that all bundled assets can tracked up front // would be nice to see if this can be done in a single pass though... - for (const page of ssrPages) { + await asyncForEach(ssrPages, async (page) => { const { route } = page; let staticHtml = ""; @@ -346,7 +316,7 @@ async function bundleSsrPages(compilation, optimizePlugins) { await trackResourcesForRoute(staticHtml, compilation, route); ssrPrerenderPagesRouteMapper[route] = staticHtml; - } + }); // technically this happens in the start of bundleCompilation once // so might be nice to detect those static assets to see if they have be "de-duped" from bundling here @@ -354,7 +324,7 @@ async function bundleSsrPages(compilation, optimizePlugins) { await bundleStyleResources(compilation, optimizePlugins); // second pass to link all bundled assets to their resources before optimizing and generating SSR bundles - for (const page of ssrPages) { + await asyncForEach(ssrPages, async (page) => { const { id, route, pageHref } = page; const pagePath = new URL(pageHref).pathname.replace(pagesDir.pathname, "./"); const entryFileUrl = new URL(pageHref); @@ -422,17 +392,16 @@ async function bundleSsrPages(compilation, optimizePlugins) { id, inputPath: normalizePathnameForWindows(entryFileOutputUrl), }); - } + }); const ssrConfigs = await getRollupConfigForSsrPages(compilation, input); if (ssrConfigs.length > 0 && ssrConfigs[0].input !== "") { console.info("bundling dynamic pages..."); - for (const configIndex in ssrConfigs) { - const rollupConfig = ssrConfigs[configIndex]; + await asyncForEach(ssrConfigs, async (rollupConfig) => { const bundle = await rollup(rollupConfig); await bundle.write(rollupConfig.output); - } + }); } } } diff --git a/packages/cli/src/lifecycles/compile.js b/packages/cli/src/lifecycles/compile.js index c00426efb..fb1d0b626 100644 --- a/packages/cli/src/lifecycles/compile.js +++ b/packages/cli/src/lifecycles/compile.js @@ -59,7 +59,7 @@ const generateCompilation = async () => { if (!(await checkResourceExists(new URL("./graph.json", outputDir)))) { return Promise.reject( - new Error("No build output detected. Make sure you have run greenwood build"), + new Error("No build output detected. Make sure you have run greenwood build"), ); } diff --git a/packages/cli/src/lifecycles/config.js b/packages/cli/src/lifecycles/config.js index f3ab21ede..fa74e5a3f 100644 --- a/packages/cli/src/lifecycles/config.js +++ b/packages/cli/src/lifecycles/config.js @@ -1,6 +1,7 @@ // @ts-nocheck import fs from "node:fs/promises"; import { checkResourceExists } from "../lib/resource-utils.js"; +import { asyncMap } from "../lib/async-utils.js"; const cwd = new URL(`file://${process.cwd()}/`); const greenwoodPluginsDirectoryUrl = new URL("../plugins/", import.meta.url); @@ -9,26 +10,25 @@ const PLUGINS_FLATTENED_DEPTH = 2; // get and "tag" all plugins provided / maintained by the @greenwood/cli // and include as the default set, with all user plugins getting appended const greenwoodPlugins = ( - await Promise.all( + await asyncMap( [ new URL("./copy/", greenwoodPluginsDirectoryUrl), new URL("./renderer/", greenwoodPluginsDirectoryUrl), new URL("./resource/", greenwoodPluginsDirectoryUrl), new URL("./server/", greenwoodPluginsDirectoryUrl), - ].map(async (pluginDirectoryUrl) => { + ], + async (pluginDirectoryUrl) => { const files = await fs.readdir(pluginDirectoryUrl); - return await Promise.all( - files.map(async (file) => { - const importUrl = new URL(`./${file}`, pluginDirectoryUrl); - // @ts-expect-error see https://github.com/microsoft/TypeScript/issues/42866 - const pluginImport = await import(importUrl); - const plugin = pluginImport[Object.keys(pluginImport)[0]]; + return await asyncMap(files, async (file) => { + const importUrl = new URL(`./${file}`, pluginDirectoryUrl); + // @ts-expect-error see https://github.com/microsoft/TypeScript/issues/42866 + const pluginImport = await import(importUrl); + const plugin = pluginImport[Object.keys(pluginImport)[0]]; - return Array.isArray(plugin) ? plugin : [plugin]; - }), - ); - }), + return Array.isArray(plugin) ? plugin : [plugin]; + }); + }, ) ) .flat(PLUGINS_FLATTENED_DEPTH) diff --git a/packages/cli/src/lifecycles/copy.js b/packages/cli/src/lifecycles/copy.js index 83672d9c8..070adc003 100644 --- a/packages/cli/src/lifecycles/copy.js +++ b/packages/cli/src/lifecycles/copy.js @@ -1,17 +1,17 @@ import fs from "node:fs/promises"; import { checkResourceExists } from "../lib/resource-utils.js"; +import { asyncForEach } from "../lib/async-utils.js"; async function rreaddir(dir, allFiles = []) { const files = (await fs.readdir(dir)).map((f) => new URL(`./${f}`, dir)); allFiles.push(...files); - await Promise.all( - files.map( - async (f) => - (await fs.stat(f)).isDirectory() && - (await rreaddir(new URL(`file://${f.pathname}/`), allFiles)), - ), + await asyncForEach( + files, + async (f) => + (await fs.stat(f)).isDirectory() && + (await rreaddir(new URL(`file://${f.pathname}/`), allFiles)), ); return allFiles; @@ -39,7 +39,7 @@ async function copyDirectory(fromUrl, toUrl, projectDirectory) { }); } - for (const fileUrl of files) { + await asyncForEach(files, async (fileUrl) => { const targetUrl = new URL( `file://${fileUrl.pathname.replace(fromUrl.pathname, toUrl.pathname)}`, ); @@ -52,7 +52,7 @@ async function copyDirectory(fromUrl, toUrl, projectDirectory) { } else if (!isDirectory) { await copyFile(fileUrl, targetUrl, projectDirectory); } - } + }); } } catch (e) { console.error("ERROR", e); @@ -63,10 +63,10 @@ const copyAssets = async (compilation) => { const copyPlugins = compilation.config.plugins.filter((plugin) => plugin.type === "copy"); const { projectDirectory } = compilation.context; - for (const plugin of copyPlugins) { + await asyncForEach(copyPlugins, async (plugin) => { const locations = await plugin.provider(compilation); - for (const location of locations) { + await asyncForEach(locations, async (location) => { const { from, to } = location; if (from.pathname.endsWith("/")) { @@ -74,8 +74,8 @@ const copyAssets = async (compilation) => { } else { await copyFile(from, to, projectDirectory); } - } - } + }); + }); }; export { copyAssets }; diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 994c15b8a..e3f29b79c 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -6,6 +6,7 @@ import { } from "../lib/resource-utils.js"; import os from "node:os"; import { WorkerPool } from "../lib/threadpool.js"; +import { asyncForEach } from "../lib/async-utils.js"; async function createOutputDirectory(outputDir) { // ignore creating directory for 404 pages since they live at the root of the output directory @@ -81,7 +82,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { new URL("../lib/ssr-route-worker.js", import.meta.url), ); - for (const page of pages) { + await asyncForEach(pages, async (page) => { const { route, outputHref } = page; const scratchUrl = toScratchUrl(outputHref, context); const url = new URL(`http://localhost:${config.port}${route}`); @@ -143,7 +144,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { await fs.writeFile(scratchUrl, body); console.info("generated page...", route); - } + }); } async function preRenderCompilationCustom(compilation, customPrerender) { @@ -187,25 +188,23 @@ async function staticRenderCompilation(compilation) { console.info("pages to generate", `\n ${pages.map((page) => page.route).join("\n ")}`); - await Promise.all( - pages.map(async (page) => { - const { route, outputHref } = page; - const scratchUrl = toScratchUrl(outputHref, context); - const url = new URL(`http://localhost:${config.port}${route}`); - const request = new Request(url); + await asyncForEach(pages, async (page) => { + const { route, outputHref } = page; + const scratchUrl = toScratchUrl(outputHref, context); + const url = new URL(`http://localhost:${config.port}${route}`); + const request = new Request(url); - let body = await (await servePage(url, request, plugins)).text(); - body = await (await interceptPage(url, request, plugins, body)).text(); + let body = await (await servePage(url, request, plugins)).text(); + body = await (await interceptPage(url, request, plugins, body)).text(); - await trackResourcesForRoute(body, compilation, route); - await createOutputDirectory(new URL(scratchUrl.href.replace("index.html", ""))); - await fs.writeFile(scratchUrl, body); + await trackResourcesForRoute(body, compilation, route); + await createOutputDirectory(new URL(scratchUrl.href.replace("index.html", ""))); + await fs.writeFile(scratchUrl, body); - console.info("generated page...", route); + console.info("generated page...", route); - return Promise.resolve(); - }), - ); + return Promise.resolve(); + }); } export { preRenderCompilationWorker, preRenderCompilationCustom, staticRenderCompilation }; diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 227b51fc0..ea5a1711d 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -40,13 +40,13 @@ async function getDevServer(compilation) { try { const url = new URL(`http://localhost:${compilation.config.port}${ctx.url}`); const initRequest = transformKoaRequestIntoStandardRequest(url, ctx.request); - const request = await resourcePlugins.reduce(async (requestPromise, plugin) => { - const intermediateRequest = await requestPromise; - return plugin.shouldResolve && - (await plugin.shouldResolve(url, intermediateRequest.clone())) - ? Promise.resolve(await plugin.resolve(url, intermediateRequest.clone())) - : Promise.resolve(await requestPromise); - }, Promise.resolve(initRequest)); + + let request = initRequest; + for (const plugin of resourcePlugins) { + if (plugin.shouldResolve && (await plugin.shouldResolve(url, request.clone()))) { + request = await plugin.resolve(url, request.clone()); + } + } ctx.url = request.url; } catch (e) { @@ -100,24 +100,18 @@ async function getDevServer(compilation) { status, headers: new Headers(header), }); - const response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + + let response = initResponse; + for (const plugin of resourcePlugins) { if ( plugin.shouldPreIntercept && - (await plugin.shouldPreIntercept(url, request, intermediateResponse.clone())) + (await plugin.shouldPreIntercept(url, request, response.clone())) ) { - const current = await plugin.preIntercept( - url, - request, - await intermediateResponse.clone(), - ); - const merged = mergeResponse(intermediateResponse.clone(), current); - - return Promise.resolve(merged); - } else { - return Promise.resolve(await responsePromise); + const current = await plugin.preIntercept(url, request, response.clone()); + + response = mergeResponse(response.clone(), current.clone()); } - }, Promise.resolve(initResponse.clone())); + } ctx.body = response.body ? Readable.from(response.body) : ""; ctx.message = response.statusText; @@ -143,20 +137,18 @@ async function getDevServer(compilation) { status, headers: new Headers(header), }); - const response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - const intermediateResponse = await responsePromise; + + let response = initResponse; + for (const plugin of resourcePlugins) { if ( plugin.shouldIntercept && - (await plugin.shouldIntercept(url, request, intermediateResponse.clone())) + (await plugin.shouldIntercept(url, request, response.clone())) ) { - const current = await plugin.intercept(url, request, await intermediateResponse.clone()); - const merged = mergeResponse(intermediateResponse.clone(), current); + const current = await plugin.intercept(url, request, response.clone()); - return Promise.resolve(merged); - } else { - return Promise.resolve(await responsePromise); + response = mergeResponse(response.clone(), current.clone()); } - }, Promise.resolve(initResponse.clone())); + } ctx.body = response.body ? Readable.from(response.body) : ""; ctx.message = response.statusText; @@ -240,11 +232,13 @@ async function getStaticServer(compilation, composable) { status: ctx.response.status, headers: new Headers(ctx.response.header), }); - const response = await resourcePlugins.reduce(async (responsePromise, plugin) => { - return plugin.shouldServe && (await plugin.shouldServe(url, request)) - ? Promise.resolve(await plugin.serve(url, request)) - : responsePromise; - }, Promise.resolve(initResponse)); + + let response = initResponse; + for (const plugin of resourcePlugins) { + if (plugin.shouldServe && (await plugin.shouldServe(url, request))) { + response = await plugin.serve(url, request); + } + } if (response.ok) { ctx.body = Readable.from(response.body); diff --git a/packages/plugin-renderer-puppeteer/src/puppeteer-handler.js b/packages/plugin-renderer-puppeteer/src/puppeteer-handler.js index fcc729b44..ca2fc3d8e 100644 --- a/packages/plugin-renderer-puppeteer/src/puppeteer-handler.js +++ b/packages/plugin-renderer-puppeteer/src/puppeteer-handler.js @@ -1,24 +1,24 @@ +import { asyncMap } from "../../cli/src/lib/async-utils.js"; + export default async function (compilation, callback) { const BrowserRunner = (await import("./lib/browser.js")).BrowserRunner; const browserRunner = new BrowserRunner(); const runBrowser = async (serverUrl, pages) => { try { - return Promise.all( - pages.map(async (page) => { - const { route } = page; - console.info("prerendering page...", route); + return asyncMap(pages, async (page) => { + const { route } = page; + console.info("prerendering page...", route); - return await browserRunner.serialize(`${serverUrl}${route}`).then(async (html) => { - console.info(`prerendering complete for page ${route}.`); + return await browserRunner.serialize(`${serverUrl}${route}`).then(async (html) => { + console.info(`prerendering complete for page ${route}.`); - // clean this up here to avoid sending webcomponents-bundle to rollup - html = html.replace(/