From 6d78816acc5c80dca9b8d46b4b061fd151cff16d Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 3 Sep 2024 16:28:51 +1000 Subject: [PATCH] Add caching to route chunks logic --- packages/react-router-dev/vite/cache.ts | 25 ++ packages/react-router-dev/vite/plugin.ts | 64 +++- .../vite/route-chunks-test.ts | 133 +++++--- .../react-router-dev/vite/route-chunks.ts | 301 ++++++++++++------ 4 files changed, 352 insertions(+), 171 deletions(-) create mode 100644 packages/react-router-dev/vite/cache.ts diff --git a/packages/react-router-dev/vite/cache.ts b/packages/react-router-dev/vite/cache.ts new file mode 100644 index 0000000000..99a20767ab --- /dev/null +++ b/packages/react-router-dev/vite/cache.ts @@ -0,0 +1,25 @@ +type CacheEntry = { value: T; version: string }; + +export type Cache = Map>; + +export function getOrSetFromCache( + cache: Cache, + key: string, + version: string, + getValue: () => T +): T { + if (!cache) { + return getValue(); + } + + let entry = cache.get(key) as CacheEntry | undefined; + + if (entry?.version === version) { + return entry.value as T; + } + + let value = getValue(); + let newEntry: CacheEntry = { value, version }; + cache.set(key, newEntry); + return value; +} diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 3408700d00..b0ffde6e1e 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -32,6 +32,7 @@ import type { Manifest as ReactRouterManifest, } from "../manifest"; import invariant from "../invariant"; +import type { Cache } from "./cache"; import type { NodeRequestHandler } from "./node-adapter"; import { fromNodeRequest, toNodeRequest } from "./node-adapter"; import { getStylesForUrl, isCssModulesFile } from "./styles"; @@ -198,6 +199,17 @@ let browserManifestId = VirtualModule.id("browser-manifest"); let hmrRuntimeId = VirtualModule.id("hmr-runtime"); let injectHmrRuntimeId = VirtualModule.id("inject-hmr-runtime"); +const normalizeRelativeFilePath = ( + file: string, + reactRouterConfig: ResolvedReactRouterConfig +) => { + let vite = importViteEsmSync(); + let fullPath = path.resolve(reactRouterConfig.appDirectory, file); + let relativePath = path.relative(reactRouterConfig.appDirectory, fullPath); + + return vite.normalizePath(relativePath); +}; + const resolveRelativeRouteFilePath = ( route: RouteManifestEntry, reactRouterConfig: ResolvedReactRouterConfig @@ -476,6 +488,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { let viteChildCompiler: Vite.ViteDevServer | null = null; let routeConfigViteServer: Vite.ViteDevServer | null = null; let viteNodeRunner: ViteNodeRunner | null = null; + let cache: Cache = new Map(); let ssrExternals = isInReactRouterMonorepo() ? [ @@ -673,7 +686,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { let hasClientLoader = sourceExports.includes("clientLoader"); let { hasClientActionChunk, hasClientLoaderChunk } = - await detectRouteChunksIfEnabled(ctx, { routeFile, viteChildCompiler }); + await detectRouteChunksIfEnabled(cache, ctx, routeFile, { + routeFile, + viteChildCompiler, + }); let clientActionAssets = hasClientActionChunk ? getReactRouterManifestBuildAssets( @@ -783,7 +799,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { ); let { hasClientActionChunk, hasClientLoaderChunk } = - await detectRouteChunksIfEnabled(ctx, { routeFile, viteChildCompiler }); + await detectRouteChunksIfEnabled(cache, ctx, routeFile, { + routeFile, + viteChildCompiler, + }); routes[key] = { id: route.id, @@ -1390,8 +1409,13 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { routeModuleId ); - let { hasRouteChunks, hasClientActionChunk, hasClientLoaderChunk } = - await detectRouteChunksIfEnabled(ctx, code); + let { + hasRouteChunks = false, + hasClientActionChunk = false, + hasClientLoaderChunk = false, + } = options?.ssr + ? {} + : await detectRouteChunksIfEnabled(cache, ctx, id, code); let reexports = sourceExports .filter( @@ -1401,19 +1425,15 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { CLIENT_ROUTE_EXPORTS.includes(exportName) ) .filter((exportName) => - !options?.ssr && hasClientActionChunk - ? exportName !== "clientAction" - : true + hasClientActionChunk ? exportName !== "clientAction" : true ) .filter((exportName) => - !options?.ssr && hasClientLoaderChunk - ? exportName !== "clientLoader" - : true + hasClientLoaderChunk ? exportName !== "clientLoader" : true ) .join(", "); return `export { ${reexports} } from "./${routeFileName}${ - !options?.ssr && hasRouteChunks ? MAIN_ROUTE_CHUNK_QUERY_STRING : "" + hasRouteChunks ? MAIN_ROUTE_CHUNK_QUERY_STRING : "" }";`; }, }, @@ -1426,7 +1446,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { // Ignore anything that isn't marked as a route chunk if (!id.includes(ROUTE_CHUNK_QUERY_STRING)) return; - let chunks = await getRouteChunksIfEnabled(ctx, code); + let chunks = await getRouteChunksIfEnabled(cache, ctx, id, code); if (chunks === null) { return "// Route chunks disabled"; @@ -1713,6 +1733,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { let oldRouteMetadata = serverManifest.routes[route.id]; let newRouteMetadata = await getRouteMetadata( + cache, ctx, viteChildCompiler, route, @@ -1899,6 +1920,7 @@ function getRoute( } async function getRouteMetadata( + cache: Cache, ctx: ReactRouterPluginContext, viteChildCompiler: Vite.ViteDevServer | null, route: RouteManifestEntry, @@ -1913,7 +1935,7 @@ async function getRouteMetadata( ); let { hasClientActionChunk, hasClientLoaderChunk } = - await detectRouteChunksIfEnabled(ctx, { + await detectRouteChunksIfEnabled(cache, ctx, routeFile, { routeFile, readRouteFile, viteChildCompiler, @@ -2206,7 +2228,9 @@ const resolveRouteFileCode = async ( }; async function detectRouteChunksIfEnabled( + cache: Cache, ctx: ReactRouterPluginContext, + id: string, input: ResolveRouteFileCodeInput ): Promise> { if (!ctx.reactRouterConfig.future.unstable_routeChunks) { @@ -2226,11 +2250,17 @@ async function detectRouteChunksIfEnabled( }; } - return detectRouteChunks({ code }); + let cacheKey = + normalizeRelativeFilePath(id, ctx.reactRouterConfig) + + (typeof input === "string" ? "" : "?read"); + + return detectRouteChunks(code, cache, cacheKey); } async function getRouteChunksIfEnabled( + cache: Cache, ctx: ReactRouterPluginContext, + id: string, input: ResolveRouteFileCodeInput ): Promise | null> { if (!ctx.reactRouterConfig.future.unstable_routeChunks) { @@ -2239,5 +2269,9 @@ async function getRouteChunksIfEnabled( let code = await resolveRouteFileCode(ctx, input); - return getRouteChunks({ code }); + let cacheKey = + normalizeRelativeFilePath(id, ctx.reactRouterConfig) + + (typeof input === "string" ? "" : "?read"); + + return getRouteChunks(code, cache, cacheKey); } diff --git a/packages/react-router-dev/vite/route-chunks-test.ts b/packages/react-router-dev/vite/route-chunks-test.ts index c1a5b31c4f..19c959d11a 100644 --- a/packages/react-router-dev/vite/route-chunks-test.ts +++ b/packages/react-router-dev/vite/route-chunks-test.ts @@ -1,11 +1,14 @@ import dedent from "dedent"; +import type { Cache } from "./cache"; import { hasChunkableExport, getChunkedExport, omitChunkedExports, } from "./route-chunks"; +let cache: [Cache, string] = [new Map(), "cacheKey"]; + describe("route chunks", () => { describe("chunkable", () => { test("functions with no identifiers", () => { @@ -16,24 +19,32 @@ describe("route chunks", () => { export const target2 = () => null; export const other2 = () => null; `; - expect(hasChunkableExport(code, "default")).toBe(true); - expect(hasChunkableExport(code, "target1")).toBe(true); - expect(hasChunkableExport(code, "target2")).toBe(true); - expect(getChunkedExport(code, "default")?.code).toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "default", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target1", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(true); + expect(getChunkedExport(code, "default", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "export default function () { return null; }" `); - expect(getChunkedExport(code, "target1")?.code).toMatchInlineSnapshot(` + expect(getChunkedExport(code, "target1", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "export function target1() { return null; }" `); - expect(getChunkedExport(code, "target2")?.code).toMatchInlineSnapshot( - `"export const target2 = () => null;"` - ); - expect(omitChunkedExports(code, ["default", "target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect( + getChunkedExport(code, "target2", {}, ...cache)?.code + ).toMatchInlineSnapshot(`"export const target2 = () => null;"`); + expect( + omitChunkedExports( + code, + ["default", "target1", "target2"], + {}, + ...cache + )?.code + ).toMatchInlineSnapshot(` "export function other1() { return null; } @@ -58,24 +69,27 @@ describe("route chunks", () => { export const target2 = () => getTargetMessage2(); export const other2 = () => getOtherMessage2(); `; - expect(hasChunkableExport(code, "target1")).toBe(true); - expect(hasChunkableExport(code, "target2")).toBe(true); - expect(getChunkedExport(code, "target1")?.code).toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "target1", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(true); + expect(getChunkedExport(code, "target1", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "import { targetMessage1 } from "./targetMessage1"; const getTargetMessage1 = () => targetMessage1; export function target1() { return getTargetMessage1(); }" `); - expect(getChunkedExport(code, "target2")?.code).toMatchInlineSnapshot(` + expect(getChunkedExport(code, "target2", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "import { targetMessage2 } from "./targetMessage2"; function getTargetMessage2() { return targetMessage2; } export const target2 = () => getTargetMessage2();" `); - expect(omitChunkedExports(code, ["target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect( + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code + ).toMatchInlineSnapshot(` "import { otherMessage1 } from "./otherMessage1"; import { otherMessage2 } from "./otherMessage2"; const getOtherMessage1 = () => otherMessage1; @@ -100,22 +114,30 @@ describe("route chunks", () => { export const other1 = () => sharedMessage; export const other2 = () => sharedMessage; `; - expect(hasChunkableExport(code, "default")).toBe(true); - expect(hasChunkableExport(code, "target1")).toBe(true); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "default")?.code).toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "default", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target1", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "default", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "export default function () { return null; }" `); - expect(getChunkedExport(code, "target1")?.code).toMatchInlineSnapshot(` + expect(getChunkedExport(code, "target1", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "export function target1() { return null; }" `); - expect(getChunkedExport(code, "target2")).toBeUndefined(); - expect(omitChunkedExports(code, ["default", "target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); + expect( + omitChunkedExports( + code, + ["default", "target1", "target2"], + {}, + ...cache + )?.code + ).toMatchInlineSnapshot(` "import { sharedMessage } from "./sharedMessage"; export const target2 = () => sharedMessage; export const other1 = () => sharedMessage; @@ -138,18 +160,20 @@ describe("route chunks", () => { export const target2 = () => getTargetMessage2(); export const other2 = () => getOtherMessage2(); `; - expect(hasChunkableExport(code, "target1")).toBe(true); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "target1")?.code).toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "target1", ...cache)).toBe(true); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "target1", {}, ...cache)?.code) + .toMatchInlineSnapshot(` "import { targetMessage1 } from "./targetMessage1"; const getTargetMessage1 = () => targetMessage1; export function target1() { return getTargetMessage1(); }" `); - expect(getChunkedExport(code, "target2")).toBeUndefined(); - expect(omitChunkedExports(code, ["target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); + expect( + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code + ).toMatchInlineSnapshot(` "import { sharedMessage } from "./sharedMessage"; const getOtherMessage1 = () => sharedMessage; function getTargetMessage2() { @@ -172,12 +196,12 @@ describe("route chunks", () => { const code = dedent` export default function () {} `; - expect(hasChunkableExport(code, "target1")).toBe(false); - expect(getChunkedExport(code, "target1")).toBeUndefined(); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "target2")).toBeUndefined(); + expect(hasChunkableExport(code, "target1", ...cache)).toBe(false); + expect(getChunkedExport(code, "target1", {}, ...cache)).toBeUndefined(); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); expect( - omitChunkedExports(code, ["target1", "target2"])?.code + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code ).toMatchInlineSnapshot(`"export default function () {}"`); }); @@ -193,12 +217,13 @@ describe("route chunks", () => { export const other1 = () => getOtherMessage1(); export const other2 = () => getOtherMessage2(); `; - expect(hasChunkableExport(code, "target1")).toBe(false); - expect(getChunkedExport(code, "target1")).toBeUndefined(); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "target2")).toBeUndefined(); - expect(omitChunkedExports(code, ["target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "target1", ...cache)).toBe(false); + expect(getChunkedExport(code, "target1", {}, ...cache)).toBeUndefined(); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); + expect( + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code + ).toMatchInlineSnapshot(` "const sharedMessage = "shared"; const getTargetMessage1 = () => sharedMessage; const getTargetMessage2 = () => sharedMessage; @@ -228,12 +253,13 @@ describe("route chunks", () => { export const other1 = () => getOtherMessage1(); export const other2 = () => getOtherMessage2(); `; - expect(hasChunkableExport(code, "target1")).toBe(false); - expect(getChunkedExport(code, "target1")).toBeUndefined(); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "target2")).toBeUndefined(); - expect(omitChunkedExports(code, ["target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "target1", ...cache)).toBe(false); + expect(getChunkedExport(code, "target1", {}, ...cache)).toBeUndefined(); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); + expect( + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code + ).toMatchInlineSnapshot(` "import { targetMessage1, targetMessage2, otherMessage1, otherMessage2 } from "./messages"; const getTargetMessage1 = () => targetMessage1; const getTargetMessage2 = () => targetMessage2; @@ -258,12 +284,13 @@ describe("route chunks", () => { export const other1 = () => getOtherMessage1(); export const other2 = () => getOtherMessage2(); `; - expect(hasChunkableExport(code, "target1")).toBe(false); - expect(getChunkedExport(code, "target1")).toBeUndefined(); - expect(hasChunkableExport(code, "target2")).toBe(false); - expect(getChunkedExport(code, "target2")).toBeUndefined(); - expect(omitChunkedExports(code, ["target1", "target2"])?.code) - .toMatchInlineSnapshot(` + expect(hasChunkableExport(code, "target1", ...cache)).toBe(false); + expect(getChunkedExport(code, "target1", {}, ...cache)).toBeUndefined(); + expect(hasChunkableExport(code, "target2", ...cache)).toBe(false); + expect(getChunkedExport(code, "target2", {}, ...cache)).toBeUndefined(); + expect( + omitChunkedExports(code, ["target1", "target2"], {}, ...cache)?.code + ).toMatchInlineSnapshot(` "import * as messages from "./messages"; const getTargetMessage1 = () => messages.targetMessage1; const getTargetMessage2 = () => messages.targetMessage2; diff --git a/packages/react-router-dev/vite/route-chunks.ts b/packages/react-router-dev/vite/route-chunks.ts index be1ce6689f..e9e5d4dfa1 100644 --- a/packages/react-router-dev/vite/route-chunks.ts +++ b/packages/react-router-dev/vite/route-chunks.ts @@ -1,4 +1,6 @@ import type { GeneratorOptions, GeneratorResult } from "@babel/generator"; +import invariant from "../invariant"; +import { type Cache, getOrSetFromCache } from "./cache"; import { type BabelTypes, type NodePath, @@ -7,35 +9,53 @@ import { generate, t, } from "./babel"; -import invariant from "../invariant"; type Statement = BabelTypes.Statement; type Identifier = BabelTypes.Identifier; +function codeToAst( + code: string, + cache: Cache, + cacheKey: string +): BabelTypes.File { + return getOrSetFromCache(cache, `${cacheKey}::codeToAst`, code, () => + parse(code, { sourceType: "module" }) + ); +} + function getTopLevelStatementsByExportName( - code: string + code: string, + cache: Cache, + cacheKey: string ): Map> { - let ast = parse(code, { sourceType: "module" }); - let topLevelStatementsByExportName = new Map>(); - - traverse(ast, { - ExportDeclaration(exportPath) { - let visited = new Set(); - let identifiers = new Set>(); - - collectIdentifiers(visited, identifiers, exportPath); - - let topLevelStatements = new Set([ - exportPath.node, - ...getTopLevelStatementsForPaths(identifiers), - ]); - for (let exportName of getExportNames(exportPath)) { - topLevelStatementsByExportName.set(exportName, topLevelStatements); - } - }, - }); + return getOrSetFromCache( + cache, + `${cacheKey}::getTopLevelStatementsByExportName`, + code, + () => { + let ast = codeToAst(code, cache, cacheKey); + let topLevelStatementsByExportName = new Map>(); + + traverse(ast, { + ExportDeclaration(exportPath) { + let visited = new Set(); + let identifiers = new Set>(); + + collectIdentifiers(visited, identifiers, exportPath); + + let topLevelStatements = new Set([ + exportPath.node, + ...getTopLevelStatementsForPaths(identifiers), + ]); + for (let exportName of getExportNames(exportPath)) { + topLevelStatementsByExportName.set(exportName, topLevelStatements); + } + }, + }); - return topLevelStatementsByExportName; + return topLevelStatementsByExportName; + } + ); } function collectIdentifiers( @@ -127,38 +147,54 @@ function areSetsDisjoint(set1: Set, set2: Set): boolean { return true; } -export function hasChunkableExport(code: string, exportName: string): boolean { - let topLevelStatementsByExportName = getTopLevelStatementsByExportName(code); - let topLevelStatements = topLevelStatementsByExportName.get(exportName); +export function hasChunkableExport( + code: string, + exportName: string, + cache: Cache, + cacheKey: string +): boolean { + return getOrSetFromCache( + cache, + `${cacheKey}::hasChunkableExport::${exportName}`, + code, + () => { + let topLevelStatementsByExportName = getTopLevelStatementsByExportName( + code, + cache, + cacheKey + ); + let topLevelStatements = topLevelStatementsByExportName.get(exportName); + + // Export wasn't found in the file + if (!topLevelStatements) { + return false; + } - // Export wasn't found in the file - if (!topLevelStatements) { - return false; - } + // Export had no identifiers to collect, so it's isolated + // e.g. export default function () { return "string" } + if (topLevelStatements.size === 0) { + return true; + } - // Export had no identifiers to collect, so it's isolated - // e.g. export default function () { return "string" } - if (topLevelStatements.size === 0) { - return true; - } + // Loop through all other exports to see if they have any top level statements + // in common with the export we're trying to create a chunk for + for (let [ + currentExportName, + currentTopLevelStatements, + ] of topLevelStatementsByExportName) { + if (currentExportName === exportName) { + continue; + } + // As soon as we find any top level statements in common with another export, + // we know this export cannot be placed in its own chunk + if (!areSetsDisjoint(currentTopLevelStatements, topLevelStatements)) { + return false; + } + } - // Loop through all other exports to see if they have any top level statements - // in common with the export we're trying to create a chunk for - for (let [ - currentExportName, - currentTopLevelStatements, - ] of topLevelStatementsByExportName) { - if (currentExportName === exportName) { - continue; + return true; } - // As soon as we find any top level statements in common with another export, - // we know this export cannot be placed in its own chunk - if (!areSetsDisjoint(currentTopLevelStatements, topLevelStatements)) { - return false; - } - } - - return true; + ); } function replaceBody( @@ -177,72 +213,121 @@ function replaceBody( export function getChunkedExport( code: string, exportName: string, - generateOptions: GeneratorOptions = {} + generateOptions: GeneratorOptions = {}, + cache: Cache, + cacheKey: string ): GeneratorResult | undefined { - let ast = parse(code, { sourceType: "module" }); - let topLevelStatementsByExportName = getTopLevelStatementsByExportName(code); - - if (!hasChunkableExport(code, exportName)) { - return undefined; - } - - let topLevelStatements = topLevelStatementsByExportName.get(exportName); - invariant(topLevelStatements, "Expected export to have top level statements"); + return getOrSetFromCache( + cache, + `${cacheKey}::getChunkedExport::${exportName}::${JSON.stringify( + generateOptions + )}`, + code, + () => { + if (!hasChunkableExport(code, exportName, cache, cacheKey)) { + return undefined; + } - let topLevelStatementsArray = Array.from(topLevelStatements); - let chunkAst = replaceBody(ast, (body) => - body.filter((node) => - topLevelStatementsArray.some((statement) => - t.isNodesEquivalent(node, statement) - ) - ) + let ast = codeToAst(code, cache, cacheKey); + let topLevelStatementsByExportName = getTopLevelStatementsByExportName( + code, + cache, + cacheKey + ); + let topLevelStatements = topLevelStatementsByExportName.get(exportName); + invariant( + topLevelStatements, + "Expected export to have top level statements" + ); + + let topLevelStatementsArray = Array.from(topLevelStatements); + let chunkAst = replaceBody(ast, (body) => + body.filter((node) => + topLevelStatementsArray.some((statement) => + t.isNodesEquivalent(node, statement) + ) + ) + ); + + return generate(chunkAst, generateOptions); + } ); - - return generate(chunkAst, generateOptions); } export function omitChunkedExports( code: string, exportNames: string[], - generateOptions: GeneratorOptions = {} + generateOptions: GeneratorOptions = {}, + cache: Cache, + cacheKey: string ): GeneratorResult | undefined { - let ast = parse(code, { sourceType: "module" }); - let topLevelStatementsByExportName = getTopLevelStatementsByExportName(code); - let omittedStatements = new Set(); - - for (let exportName of exportNames) { - let topLevelStatements = topLevelStatementsByExportName.get(exportName); - if (!topLevelStatements || !hasChunkableExport(code, exportName)) { - continue; - } - for (let statement of topLevelStatements) { - omittedStatements.add(statement); - } - } - - let omittedStatementsArray = Array.from(omittedStatements); - let astWithChunksOmitted = replaceBody(ast, (body) => - body.filter((node) => - omittedStatementsArray.every( - (statement) => !t.isNodesEquivalent(node, statement) - ) - ) - ); + return getOrSetFromCache( + cache, + `${cacheKey}::omitChunkedExports::${exportNames.join( + "," + )}::${JSON.stringify(generateOptions)}`, + code, + () => { + let topLevelStatementsByExportName = getTopLevelStatementsByExportName( + code, + cache, + cacheKey + ); + let omittedStatements = new Set(); + + for (let exportName of exportNames) { + let topLevelStatements = topLevelStatementsByExportName.get(exportName); + if ( + !topLevelStatements || + !hasChunkableExport(code, exportName, cache, cacheKey) + ) { + continue; + } + for (let statement of topLevelStatements) { + omittedStatements.add(statement); + } + } - if (astWithChunksOmitted.program.body.length === 0) { - return undefined; - } + let omittedStatementsArray = Array.from(omittedStatements); + let ast = codeToAst(code, cache, cacheKey); + let astWithChunksOmitted = replaceBody(ast, (body) => + body.filter((node) => + omittedStatementsArray.every( + (statement) => !t.isNodesEquivalent(node, statement) + ) + ) + ); + + if (astWithChunksOmitted.program.body.length === 0) { + return undefined; + } - return generate(astWithChunksOmitted, generateOptions); + return generate(astWithChunksOmitted, generateOptions); + } + ); } -export function detectRouteChunks({ code }: { code: string }): { +export function detectRouteChunks( + code: string, + cache: Cache, + cacheKey: string +): { hasClientActionChunk: boolean; hasClientLoaderChunk: boolean; hasRouteChunks: boolean; } { - let hasClientActionChunk = hasChunkableExport(code, "clientAction"); - let hasClientLoaderChunk = hasChunkableExport(code, "clientLoader"); + let hasClientActionChunk = hasChunkableExport( + code, + "clientAction", + cache, + cacheKey + ); + let hasClientLoaderChunk = hasChunkableExport( + code, + "clientLoader", + cache, + cacheKey + ); let hasRouteChunks = hasClientActionChunk || hasClientLoaderChunk; return { @@ -252,14 +337,24 @@ export function detectRouteChunks({ code }: { code: string }): { }; } -export function getRouteChunks({ code }: { code: string }): { +export function getRouteChunks( + code: string, + cache: Cache, + cacheKey: string +): { main: GeneratorResult | undefined; clientAction: GeneratorResult | undefined; clientLoader: GeneratorResult | undefined; } { return { - main: omitChunkedExports(code, ["clientAction", "clientLoader"]), - clientAction: getChunkedExport(code, "clientAction"), - clientLoader: getChunkedExport(code, "clientLoader"), + main: omitChunkedExports( + code, + ["clientAction", "clientLoader"], + {}, + cache, + cacheKey + ), + clientAction: getChunkedExport(code, "clientAction", {}, cache, cacheKey), + clientLoader: getChunkedExport(code, "clientLoader", {}, cache, cacheKey), }; }