From 32b4a67f5e243ba5e5a301b8f9e99a1995203290 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Nov 2023 12:30:40 -0500 Subject: [PATCH] Patch createRequire() and loadConfig() to handle builtin modules in the standalone CLI Fix code style --- src/lib/load-config.ts | 8 ++++++ standalone-cli/patch-require.js | 50 +++++++++++++++++++++++++++++++++ standalone-cli/standalone.js | 41 +++++++++++++++++++++------ 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 standalone-cli/patch-require.js diff --git a/src/lib/load-config.ts b/src/lib/load-config.ts index 645e8e1d55c4..e879412a3977 100644 --- a/src/lib/load-config.ts +++ b/src/lib/load-config.ts @@ -4,6 +4,14 @@ import { transform } from 'sucrase' import { Config } from '../../types/config' let jiti: ReturnType | null = null + +// @internal +// This WILL be removed in some future release +// If you rely on this your stuff WILL break +export function useCustomJiti(_jiti: ReturnType) { + jiti = _jiti +} + function lazyJiti() { return ( jiti ?? diff --git a/standalone-cli/patch-require.js b/standalone-cli/patch-require.js new file mode 100644 index 000000000000..c5857baf807a --- /dev/null +++ b/standalone-cli/patch-require.js @@ -0,0 +1,50 @@ +const Module = require('node:module') + +/** + * @param {Record} mods + */ +module.exports.patchRequire = function patchRequire(mods, parentCache) { + function wrapRequire(origRequire) { + return Object.assign( + function (id) { + // Patch require(…) to return the cached module + if (mods.hasOwnProperty(id)) { + return mods[id] + } + + return origRequire.apply(this, arguments) + }, + + // Make sure we carry over other properties of the original require(…) + origRequire, + + { + resolve(id) { + // Defer to the "parent" require cache when resolving the module + // This also requires that the module be provided as a "native module" to JITI + + // The path returned here is VERY important as it ensures that the `isNativeRe` in JITI + // passes which is required for the module to be loaded via the native require(…) function + // Thankfully, the regex just means that it needs to be in a node_modules folder which is true + // even when bundled using Vercel's `pkg` + if (parentCache.hasOwnProperty(id)) { + return parentCache[id].filename + } + + return origRequire.resolve.apply(this, arguments) + }, + } + ) + } + + let origRequire = Module.prototype.require + let origCreateRequire = Module.createRequire + + // We have to augment the default "require" in every module + Module.prototype.require = wrapRequire(origRequire) + + // And any "require" created by the "createRequire" method + Module.createRequire = function () { + return wrapRequire(origCreateRequire.apply(this, arguments)) + } +} diff --git a/standalone-cli/standalone.js b/standalone-cli/standalone.js index 28ddf69a960d..ce0bde1cb64e 100644 --- a/standalone-cli/standalone.js +++ b/standalone-cli/standalone.js @@ -1,5 +1,3 @@ -let Module = require('module') -let origRequire = Module.prototype.require let log = require('tailwindcss/lib/util/log').default let localModules = { @@ -25,11 +23,38 @@ let localModules = { tailwindcss: require('tailwindcss'), } -Module.prototype.require = function (id) { - if (localModules.hasOwnProperty(id)) { - return localModules[id] - } - return origRequire.apply(this, arguments) -} +// Swap out the default JITI implementation with one that has the built-in modules above preloaded as "native modules" +// NOTE: This uses a private, internal API of Tailwind CSS and is subject to change at any time +let { useCustomJiti } = require('tailwindcss/lib/lib/load-config') +let { transform } = require('sucrase') + +useCustomJiti(() => + require('jiti')(__filename, { + interopDefault: true, + nativeModules: Object.keys(localModules), + transform: (opts) => { + return transform(opts.source, { + transforms: ['typescript', 'imports'], + }) + }, + }) +) + +let { patchRequire } = require('./patch-require.js') +patchRequire( + // Patch require(…) to return the bundled modules above so they don't need to be installed + localModules, + + // Create a require cache that maps module IDs to module objects + // This MUST be done before require is patched to handle caching + Object.fromEntries( + Object.keys(localModules).map((id) => [ + id, + id === '@tailwindcss/line-clamp' + ? `node_modules/@tailwindcss/line-clamp/` + : require.cache[require.resolve(id)], + ]) + ) +) require('tailwindcss/lib/cli')