Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions packages/cli/src/commands/build.js

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -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()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this looks very elegant! 🤓


if (prerenderPlugin.executeModuleUrl) {
await preRenderCompilationWorker(compilation, prerenderPlugin);
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/lib/async-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ async function asyncFilter(arr, cb) {
}

// https://stackoverflow.com/a/71278238/417806
// Constraint: mapper functions must not depend on each other

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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);
}

export { asyncFilter, asyncMap };
141 changes: 56 additions & 85 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import path from "node:path";
import { rollup } from "rollup";
import { pruneGraph } from "../lib/content-utils.js";
import { asyncMap } from "../lib/async-utils.js";

async function interceptPage(url, request, plugins, body) {
let response = new Response(body, {
Expand Down Expand Up @@ -115,45 +116,41 @@ async function cleanUpResources(compilation) {
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),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 asyncMap(pages, async (page) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could argue that this would be better off as asyncForEach, since the callbacks have side-effects and we're not actually using the return values.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense, I would be open to an asyncForEach

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) {
Expand Down Expand Up @@ -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) {
Comment thread
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();

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lifecycles/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
}

Expand Down
24 changes: 12 additions & 12 deletions packages/cli/src/lifecycles/config.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 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)
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/lifecycles/copy.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import fs from "node:fs/promises";
import { checkResourceExists } from "../lib/resource-utils.js";
import { asyncMap } 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 asyncMap(
files,
async (f) =>
(await fs.stat(f)).isDirectory() &&
(await rreaddir(new URL(`file://${f.pathname}/`), allFiles)),
);

return allFiles;
Expand Down
29 changes: 14 additions & 15 deletions packages/cli/src/lifecycles/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "../lib/resource-utils.js";
import os from "node:os";
import { WorkerPool } from "../lib/threadpool.js";
import { asyncMap } from "../lib/async-utils.js";

async function createOutputDirectory(route, outputDir) {
if (!route.endsWith("/404/") && !(await checkResourceExists(outputDir))) {
Expand Down Expand Up @@ -186,25 +187,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 asyncMap(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(route, new URL(scratchUrl.href.replace("index.html", "")));
await fs.writeFile(scratchUrl, body);
await trackResourcesForRoute(body, compilation, route);
await createOutputDirectory(route, 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 };
Loading