-
Notifications
You must be signed in to change notification settings - Fork 15
enhancement/issue 823 ensure greenwood cli and plugins are favoring idiomatic usages of async #1546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
a805a36
5aa3439
2a8fcf8
1233214
f3575b8
b730cbd
4ecc2bf
5674288
b65ee02
6fcbf11
41e0acc
7e3f0c8
a5af458
2159832
80afa74
2a591b0
7f2339a
6da1e3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recommend hiding whitespace changes for easier reviewing |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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())); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice, this looks very elegant! 🤓 |
||
|
|
||
| if (prerenderPlugin.executeModuleUrl) { | ||
| await preRenderCompilationWorker(compilation, prerenderPlugin); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documented a constraint here, since the callbacks need to be safe to run concurrently. |
||
| async function asyncMap(items, mapper) { | ||
| const mappedItems = []; | ||
| const promises = []; | ||
|
|
||
| for (const item of items) { | ||
| mappedItems.push(await mapper(item)); | ||
| promises.push(mapper(item)); | ||
|
Comment on lines
-21
to
+23
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Promise.all rather than awaiting in the for loop allows them to run in parallel. This technique also matches the stack overflow source linked in the comment. |
||
| } | ||
|
|
||
| return mappedItems; | ||
| return Promise.all(promises); | ||
| } | ||
|
|
||
| async function asyncForEach(items, callback) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I think I must have missed this in a previous review but why would we need a custom
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the difference is that this wraps them in
https://eslint.org/docs/latest/rules/no-await-in-loop This video is a good recap of async/await: https://youtu.be/vn3tm0quoqE?t=341. I recommend you watch it to make sure we're on the same page.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yes, I think you may have mentioned that before and it just passed me by. 🤦♂️ So basically in cases where we want to run a bunch of independent tasks that don't depend on each other, we can run them concurrently using your new Good stuff, thanks for taking the time to explain your approach here, much appreciated! 🙌
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That video link was great btw, Fireship is the best! 🔥 |
||
| await asyncMap(items, callback); | ||
| } | ||
|
|
||
| export { asyncFilter, asyncMap }; | ||
| export { asyncFilter, asyncMap, asyncForEach }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,65 +102,61 @@ async function emitResources(compilation) { | |
| async function cleanUpResources(compilation) { | ||
| const { outputDir } = compilation.context; | ||
|
|
||
| for (const resource of compilation.resources.values()) { | ||
| 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; | ||
|
|
||
| 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), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's an unintentional whitespace change here, but the linter requires this change. |
||
| ); | ||
|
|
||
| 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) { | ||
|
KaiPrince marked this conversation as resolved.
|
||
| 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) => { | ||
|
Comment on lines
-303
to
+274
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I double-checked, and |
||
| 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 = "<content-outlet></content-outlet>"; | ||
|
|
||
|
|
@@ -346,15 +316,15 @@ 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 | ||
| await bundleScriptResources(compilation); | ||
| 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); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my concern with adding this rule with no "constraint" on this is that now we get a ton of warnings
Details
What I was trying to callout in my comment was that using
awaitin afor ... ofis fine. So unless we can limit the linting to justmap,filter, etc then I worry about all the extra noise this adds. The only other option is to add an eslint ignore to all those locations, which seemed pretty tedious.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I realize i kind of left an unfinished sentence here, my apologies 😞
So yeah, my above comment was what I trying to say, so sorry about that mixup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it's a lot. That's what I was trying to say in my other comment. We'd have to add
eslint-disablecomments to a lot of places, or just leave it as a warn (which as you say can get pretty noisy).I thought you confirmed you wanted it for keeping us "honest", but no problem I'll just revert this config change for now. If we decide later we want to add all those eslint-ignore comments, I can open a separate PR.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, apologies I didn't finish my full thought in that most recent comment, but I was trying to say if an eslint rule is added, it should (ideally) be done with the ability to carve out an exception for the use cases we do want it for (e.g.
for...of,for..in)So the options would be
errorand add// eslint-disable-line no-await-in-loopeverywhere its allowed (or see if we can just have it error onmap,filter, etcSo I was leaning towards option #2 depending if the carve out was feasible, or if we wanted to sprinkle in all those ignore lines.
Thoughts?
Like I said I am good with things right now, I've personally broken the habit of the async maps, filters etc so I am at least trying to keep myself honest now 😇
Also, you might want to do one more rebase since I bumped the Node version and so GitHub Actions checks have been updated accordingly. Then we can run the final checks on your PR. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started adding eslint-ignore comments initially, but I think I hit a snag in the rollup config file. Let's do option 2 for now, and I'll start a follow-up PR to see where option 1 takes us.
PR has been updated from master 👍