diff --git a/packages/@sanity/core/_internal.js b/packages/@sanity/core/_internal.js new file mode 100644 index 00000000000..050685d8ff7 --- /dev/null +++ b/packages/@sanity/core/_internal.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('./_interopReexport')(module.exports, require('./lib/_exports/_internal')) diff --git a/packages/@sanity/core/_interopReexport.js b/packages/@sanity/core/_interopReexport.js new file mode 100644 index 00000000000..e1c9eb78d1f --- /dev/null +++ b/packages/@sanity/core/_interopReexport.js @@ -0,0 +1,30 @@ +/** + * esm/commonjs interop reexport helper + * this should be used instead of doing `module.exports = require('./some/file') + * since that doesn't work properly with esm imports from the consuming end + * @param moduleExports export object from the exporting module + * @param importedModule the imported module to be reexported + */ +module.exports = function _interopReexport(moduleExports, importedModule) { + Object.defineProperty(moduleExports, '__esModule', { + value: true, + }) + + Object.defineProperty(moduleExports, 'default', { + enumerable: true, + get: function get() { + return importedModule.default + }, + }) + + Object.keys(importedModule).forEach(function (key) { + if (key === 'default' || key === '__esModule') return + if (key in moduleExports && moduleExports[key] === importedModule[key]) return + Object.defineProperty(moduleExports, key, { + enumerable: true, + get: function get() { + return importedModule[key] + }, + }) + }) +} diff --git a/packages/@sanity/core/package.json b/packages/@sanity/core/package.json index f00d58176cb..c043f425892 100644 --- a/packages/@sanity/core/package.json +++ b/packages/@sanity/core/package.json @@ -5,6 +5,14 @@ "main": "lib/index.js", "author": "Sanity.io ", "license": "MIT", + "//": "the typesVersion config below is a workaround for TypeScript's lack of support for package exports", + "typesVersions": { + "*": { + "*": [ + "dist/dts/_exports/*" + ] + } + }, "scripts": { "build": "babel src --copy-files --out-dir lib", "clean": "rimraf lib dest", @@ -74,6 +82,7 @@ "@sanity/client": "2.21.7", "@types/fs-extra": "^7.0.0", "@types/inquirer": "^6.0.2", + "@types/klaw-sync": "^6.0.1", "@types/resolve-from": "^4.0.0", "@types/semver": "^6.2.3", "chalk": "^2.4.2", diff --git a/packages/@sanity/core/src/_exports/_internal.ts b/packages/@sanity/core/src/_exports/_internal.ts new file mode 100644 index 00000000000..caeba03a5cc --- /dev/null +++ b/packages/@sanity/core/src/_exports/_internal.ts @@ -0,0 +1 @@ +export {createRequireContext} from '../util/createRequireContext' diff --git a/packages/@sanity/core/src/_exports/index.ts b/packages/@sanity/core/src/_exports/index.ts new file mode 100644 index 00000000000..f2066c0e6ab --- /dev/null +++ b/packages/@sanity/core/src/_exports/index.ts @@ -0,0 +1,2 @@ +// intentionally left blank for now since this package have never had any official package exports before +export {} diff --git a/packages/@sanity/core/src/util/createRequireContext.ts b/packages/@sanity/core/src/util/createRequireContext.ts new file mode 100644 index 00000000000..708e1c0fb65 --- /dev/null +++ b/packages/@sanity/core/src/util/createRequireContext.ts @@ -0,0 +1,98 @@ +import path from 'path' +import klaw from 'klaw-sync' + +/** + * https://webpack.js.org/guides/dependency-management/#requirecontext + */ +interface WebpackRequireContextFactory { + ( + directory: string, + useSubdirectories?: boolean, + regExp?: RegExp, + mode?: 'sync' + ): WebpackRequireContext +} + +/** + * https://webpack.js.org/guides/dependency-management/#context-module-api + */ +interface WebpackRequireContext { + (request: string): unknown + + id: string + keys(): string[] + resolve(request: string): unknown +} + +/** + * note, options are primarily for testing + */ +interface CreateRequireContextOptions { + require?: (request: string) => unknown + resolve?: (request: string) => string +} + +const globalRequire = require + +export function createRequireContext( + dirname: string, + { + require = globalRequire, + resolve = globalRequire.resolve.bind(globalRequire), + }: CreateRequireContextOptions = {} +): WebpackRequireContextFactory { + function requireContext( + directory: string, + recursive = true, + regExp = /.*/ + ): WebpackRequireContext { + console.warn('Usage of `require.context` is deprecated and will break in a future release.') + + try { + const basedir = path.resolve(dirname, directory) + + const keys = klaw(basedir, { + depthLimit: recursive ? 30 : 0, + }) + .filter((item) => !item.stats.isDirectory() && regExp.test(item.path)) + // use relative paths for the keys + .map((item) => path.relative(basedir, item.path)) + // if the path.resolve doesn't prefixed with `./` then add it. + // note it could be upward `../` so we need the conditional + .map((filename) => (filename.startsWith('.') ? filename : `./${filename}`)) + + // eslint-disable-next-line no-inner-declarations + function context(request: string) { + // eslint-disable-next-line import/no-dynamic-require + return require(path.join(basedir, request)) + } + + Object.defineProperty(context, 'id', { + get: () => { + console.warn('`require.context` `context.id` is not supported.') + return '' + }, + }) + + Object.defineProperty(context, 'keys', { + // NOTE: this keys method does not match the behavior of webpack's + // require.context context because it does not return all possible keys + // + // e.g. `./module-a/index.js` + // would return `['./module-a', './module-a/index', './module-a/index.js']` + value: () => keys, + }) + + Object.defineProperty(context, 'resolve', { + value: (request: string) => resolve(path.join(basedir, request)), + }) + + return context as WebpackRequireContext + } catch (contextErr) { + contextErr.message = `Error running require.context():\n\n${contextErr.message}` + throw contextErr + } + } + + return requireContext +} diff --git a/packages/@sanity/core/src/util/mockBrowserEnvironment.js b/packages/@sanity/core/src/util/mockBrowserEnvironment.js index 2b3f6cc2e63..87d8ed42216 100644 --- a/packages/@sanity/core/src/util/mockBrowserEnvironment.js +++ b/packages/@sanity/core/src/util/mockBrowserEnvironment.js @@ -38,7 +38,6 @@ function mockBrowserEnvironment(basePath) { const domCleanup = jsdomGlobal(jsdomDefaultHtml, {url: 'http://localhost:3333/'}) const windowCleanup = () => global.window.close() const globalCleanup = provideFakeGlobals() - const contextCleanup = requireContext.register() const cleanupFileLoader = pirates.addHook( (code, filename) => `module.exports = ${JSON.stringify(filename)}`, { @@ -49,13 +48,14 @@ function mockBrowserEnvironment(basePath) { registerBabelLoader(basePath) pluginLoader({basePath, stubCss: true}) + const contextCleanup = requireContext.register() return function cleanupBrowserEnvironment() { cleanupFileLoader() - contextCleanup() globalCleanup() windowCleanup() domCleanup() + contextCleanup() } } diff --git a/packages/@sanity/core/src/util/requireContext.js b/packages/@sanity/core/src/util/requireContext.js deleted file mode 100644 index 6413759234b..00000000000 --- a/packages/@sanity/core/src/util/requireContext.js +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable import/no-dynamic-require, max-depth, no-inner-declarations */ - -/** - * This file exposes a "register" function which hacks in a `require.context` function, - * mirroring webpacks version (https://webpack.js.org/guides/dependency-management/#requirecontext) - * - * Basically allows you to do `require.context('./types', true, /\.js$/)` to import multiple - * files at the same time. While generally not advised (given it's a webpack-only thing), - * people are already using it in the wild, so it breaks when trying to deploy GraphQL APIs, - * or when running scripts using `sanity exec`. - * - * We have to inject the `require.context` function to each required file, which is done by - * overriding the `module.constructor.wrap` method, with a stringified version of `requireContext` - * injected to the top of the file, assigned to `require.context`. - * - * To make things worse, knowing where the require call was performed from is harder than it - * ought to be, so we have to use an approach where we get the calling path name from an - * error stacktrace. This is required to resolve `./types` to `/some/studio/path/schemas/types`, - * for instance. - */ - -const klawPath = require.resolve('klaw-sync') -const resolvePath = require.resolve('resolve-from') -const hookPath = __dirname - -let registered = false - -const requireContext = (directory, recursive, regExp) => { - // This whole thing is kind of sketchy, so let's wrap it to prevent any breakage - // if node should change it's APIs in any way - try { - // Has to be required from within function because of wrapping - const path = require('path') - const klaw = require(klawPath) - const resolveFrom = require(resolvePath) - - // We need to resolve `./foo` from where the _caller_ is situated - function getCallerDirName() { - const originalFunc = Error.prepareStackTrace - - let callerFile - let currentFile - try { - const err = new Error() - - Error.prepareStackTrace = function prepareStackTrace(error, stack) { - return stack - } - - currentFile = err.stack.shift().getFileName() - if (currentFile === hookPath) { - while (err.stack.length) { - callerFile = err.stack.shift().getFileName() - - if (currentFile !== callerFile) { - break - } - } - } else { - callerFile = currentFile - } - } catch (err) { - // noop - } - - Error.prepareStackTrace = originalFunc - - return path.dirname(callerFile) - } - - // Assume absolute path by default - let basePath = directory - - if (directory[0] === '.') { - // Relative path - basePath = path.join(getCallerDirName(), directory) - } else if (!path.isAbsolute(directory)) { - // Module path, resolve the module from the caller - basePath = resolveFrom(getCallerDirName(), directory) - } - - // Fix any trailing slashes and similar - basePath = path.resolve(basePath) - - const keys = klaw(basePath, {depthLimit: recursive ? 30 : 0, nodir: false}) - // Make sure we only match the provided regexp (or use the default (.json/.js)) - .filter((file) => file.path.match(regExp || /\.(json|js)$/)) - // Use relative paths for the keys - .map((file) => path.join('.', file.path.slice(basePath.length + 1))) - - const context = (key) => require(context.resolve(key)) - context.resolve = (key) => path.resolve(basePath, key) - context.keys = () => keys - - return context - } catch (contextErr) { - contextErr.message = `Error running require.context():\n\n${contextErr.message}` - throw contextErr - } -} - -const wrap = module.constructor.wrap -const restore = () => { - module.constructor.wrap = wrap -} - -function register() { - if (registered) { - return restore - } - - try { - module.constructor.wrap = (script) => { - const requireContextScript = requireContext - .toString() - .replace(/hookPath/g, JSON.stringify(hookPath)) - .replace(/klawPath/g, JSON.stringify(klawPath)) - .replace(/resolvePath/g, JSON.stringify(resolvePath)) - - // Assigning the function may very well crash in an upcoming version, - // so try to catch and warn if this is the case - return wrap(`try { - require.context = ${requireContextScript} - } catch (err) { - console.warn('Error assigning require.context function:\\n' + err.message) - }\n\n${script}`) - } - } catch (err) { - // eslint-disable-next-line no-console - console.warn(`Error assigning module wrapper for require.context hook:\n${err.message}`) - } - - registered = true - - // There isn't a good way to clean this up - return restore -} - -module.exports = { - register, -} diff --git a/packages/@sanity/core/src/util/requireContext.ts b/packages/@sanity/core/src/util/requireContext.ts new file mode 100644 index 00000000000..395e59fcf3e --- /dev/null +++ b/packages/@sanity/core/src/util/requireContext.ts @@ -0,0 +1,34 @@ +/** + * This file exposes a "register" function which hacks in a `require.context`function, + * mirroring webpacks version (https://webpack.js.org/guides/dependency-management/#requirecontext) + * + * Basically allows you to do `require.context('./types', true, /\.js$/)` to import multiple + * files at the same time. While generally not advised (given it's a webpack-only thing), + * people are already using it in the wild, so it breaks when trying to deploy GraphQL APIs, + * or when running scripts using `sanity exec`. + * + * We have to inject the `require.context` function to each required file, which is done by + * overriding injecting a small script that runs before the start of each file. + */ + +import path from 'path' +import {addHook} from 'pirates' + +type Revert = ReturnType +let revert: Revert | undefined + +const augmentRequire = (dirname: string) => + `if (typeof require !== 'undefined') {const {createRequireContext} = require('@sanity/core/_internal');require.context = createRequireContext(${JSON.stringify( + dirname + )});};` + +export function register(): Revert { + if (revert) return revert + + revert = addHook((code, filename) => `${augmentRequire(path.dirname(filename))}${code}`, { + exts: ['.js', '.ts', '.tsx'], + ignoreNodeModules: true, + }) + + return revert +}