From 13df38cff06ab15bc3287934865fe7f7d64b08f7 Mon Sep 17 00:00:00 2001 From: "M. George Hansen" Date: Sun, 14 Jun 2020 12:06:59 -0700 Subject: [PATCH] fix: stack size exceed error on purging module cache --- src/build.ts | 4 +- src/config.ts | 4 +- src/utils/decache-module.ts | 69 ++++++++++++++++++++++++++++++++ src/{utils.ts => utils/index.ts} | 24 +---------- 4 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 src/utils/decache-module.ts rename src/{utils.ts => utils/index.ts} (68%) diff --git a/src/build.ts b/src/build.ts index aa676f6..beeaeb2 100644 --- a/src/build.ts +++ b/src/build.ts @@ -8,7 +8,7 @@ import * as path from "path"; import { Config, ConfigPaths } from "./config"; import { renderPage } from "./render"; -import { purgeModuleAndDepsFromCache, walkDir } from "./utils"; +import { decacheModule, walkDir } from "./utils"; const renderPagesToHtml = async ({ pagesDir, @@ -22,7 +22,7 @@ const renderPagesToHtml = async ({ } // Ensure that we don't cache page modules when running in dev server. - purgeModuleAndDepsFromCache(srcPath); + decacheModule(srcPath); // eslint-disable-next-line @typescript-eslint/no-var-requires const pageSrc = require(srcPath); if (!("page" in pageSrc)) { diff --git a/src/config.ts b/src/config.ts index d9c500f..2e91b13 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,7 @@ import * as path from "path"; -import { purgeModuleAndDepsFromCache } from "./utils"; +import { decacheModule } from "./utils"; /** * Paths used during configuration. @@ -63,7 +63,7 @@ export const loadConfig = async (rootDir: string): Promise => { // Attempt to load a websnacks.ts/js file in rootDir. try { configPath = require.resolve(path.resolve(rootDir, "websnacks")); - purgeModuleAndDepsFromCache(configPath); + decacheModule(configPath); // TODO: validate user config. userConfig = await import(configPath); } catch (error) { diff --git a/src/utils/decache-module.ts b/src/utils/decache-module.ts new file mode 100644 index 0000000..3cf1c45 --- /dev/null +++ b/src/utils/decache-module.ts @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +const resolveModulePath = (importPath: string): string | undefined => { + try { + return require.resolve(importPath); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + return; + } + throw error; + } +}; + +const removeParentModuleRef = (mod: NodeModule): void => { + const parent = mod.parent; + if (parent == null) { + return; + } + + const siblings = parent.children; + const nSiblings = siblings.length; + for (let i = nSiblings - 1; i >= 0; i--) { + const sibling = siblings[i]; + if (sibling.id === mod.id) { + siblings.splice(i, 1); + return; + } + } +}; + +/** + * Clear a module and its dependencies from node's module cache, ensuring that + * requiring the module again will reload the code from disk. + * + * @param importPath Path or name of the module to resolve (same as + * {@see require}). + * + * @throws Error if the module could not be resolved due to filesystem error. + */ +export const decacheModule = (importPath: string): void => { + const modulePath = resolveModulePath(importPath); + if (modulePath == null) { + return; + } + + // DFS the module dependency tree, using iteration to avoid stack size + // exceeded exceptions. + const modsToCheck: NodeModule[] = []; + const visited: Set = new Set(); + let currentMod: NodeModule | undefined = require.cache[modulePath]; + while (currentMod != null) { + if (visited.has(currentMod.id)) { + currentMod = modsToCheck.pop(); + continue; + } + + removeParentModuleRef(currentMod); + delete require.cache[currentMod.id]; + for (const childMod of currentMod.children) { + modsToCheck.push(childMod); + } + + visited.add(currentMod.id); + currentMod = modsToCheck.pop(); + } +}; diff --git a/src/utils.ts b/src/utils/index.ts similarity index 68% rename from src/utils.ts rename to src/utils/index.ts index 4c5fbdf..83f5f0a 100644 --- a/src/utils.ts +++ b/src/utils/index.ts @@ -6,6 +6,8 @@ import { promises as fs } from "fs"; import * as path from "path"; +export { decacheModule } from "./decache-module"; + /** * Recursively walk a directory, returning the files it finds. * @@ -27,28 +29,6 @@ export const walkDir = async function* ( } }; -/** - * Purge cached versions of a node module and all of its dependencies from the - * global require cache, ensuring that future imports reload the module from - * disk. - * - * @param modName Name of the module to purge from the require cache. - */ -export const purgeModuleAndDepsFromCache = (modName: string): void => { - const modPath = require.resolve(modName); - if (modPath == null) { - return; - } - const mod = require.cache[modPath]; - if (mod == null) { - return; - } - for (const child of mod.children) { - purgeModuleAndDepsFromCache(child.id); - } - delete require.cache[modPath]; -}; - export type Flattenable = Array>; /**