Last active
December 26, 2021 08:43
-
-
Save unicornware/6d8f8f7050d5c364ea85d6ba5d43e8cd to your computer and use it in GitHub Desktop.
Custom ESM Configuration ([email protected])
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Environment Variables - NODE_OPTIONS - Use ESM syntax | |
| if [ -f "$PWD/node_modules/ts-node/esm.mjs" ]; then | |
| # Ignore warnings | |
| NODE_NO_WARNINGS=1 | |
| # Import JSON modules | |
| JSON_MODULES='--experimental-json-modules' | |
| # Use custom ESM loader | |
| LOADER="--loader $PWD/tools/loaders/esm.mjs" | |
| # Don't require imported modules to include extensions | |
| SPECIFIER_RESOLUTION='--es-module-specifier-resolution=node' | |
| # Set options | |
| export NODE_OPTIONS="$JSON_MODULES $LOADER $SPECIFIER_RESOLUTION" | |
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| echo "NODE_NO_WARNINGS=1" >> $GITHUB_ENV | |
| echo "NODE_OPTIONS=--experimental-modules --experimental-json-modules --loader=$GITHUB_WORKSPACE/tools/loaders/esm.mjs --es-module-specifier-resolution=node" >> $GITHUB_ENV |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ENVIRONMENT VARIABLES | |
| # See: https://zsh.sourceforge.io/Intro/intro_3.html | |
| # Load ESM environment variables | |
| [[ -f "$PWD/.env.esm" ]] && . $PWD/.env.esm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| declare global { | |
| namespace EsmLoader { | |
| type Format = | |
| | 'builtin' | |
| | 'commonjs' | |
| | 'dynamic' | |
| | 'json' | |
| | 'module' | |
| | 'wasm' | |
| type Source = string | Buffer | |
| const hooks: { | |
| getFormat: Hooks.GetFormat | |
| resolve: Hooks.Resolve | |
| transformSource: Hooks.TransformSource | |
| } | |
| namespace Hooks { | |
| type GetFormat = ( | |
| url: string, | |
| context: HookContext.GetFormat, | |
| defaultGetFormat: GetFormat | |
| ) => Promise<HookResult.GetFormat> | |
| type Resolve = ( | |
| specifier: string, | |
| context: HookContext.Resolve, | |
| defaultResolve: Resolve | |
| ) => Promise<HookResult.Resolve> | |
| type TransformSource = ( | |
| source: Source, | |
| context: HookContext.TransformSource, | |
| defaultTransformSource: TransformSource | |
| ) => Promise<HookResult.TransformSource> | |
| } | |
| namespace HookContext { | |
| type GetFormat = Record<never, never> | |
| type Resolve = { parentURL: string } | |
| type TransformSource = { format: Format; url: string } | |
| } | |
| namespace HookResult { | |
| type GetFormat = { format: Format } | |
| type Resolve = { url: string } | |
| type TransformSource = { source: Source } | |
| } | |
| } | |
| } | |
| export {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as istanbul from '@istanbuljs/esm-loader-hook' | |
| import path from 'node:path' | |
| import pkg from '../../package.json' | |
| import matchSpecifier from '../helpers/match-specifier.cjs' | |
| /** @type {EsmLoader.hooks} */ const hooks = await import('ts-node/esm') | |
| /** | |
| * @file Helpers - Custom ESM Loader Hooks | |
| * @module tools/loaders/esm | |
| * @see https://github.com/TypeStrong/ts-node/issues/1007 | |
| * @see https://nodejs.org/docs/latest-v12.x/api/all.html#esm_hooks | |
| */ | |
| const TEST = process.env.NODE_ENV === 'test' | |
| /** | |
| * Determines if `url` should be interpreted as a CommonJS or ES module. | |
| * | |
| * @async | |
| * @param {string} url - File URL | |
| * @param {EsmLoader.HookContext.GetFormat} ctx - Resolver context | |
| * @param {EsmLoader.Hooks.GetFormat} fn - Default format fn | |
| * @return {Promise<EsmLoader.HookResult.GetFormat>} Module format | |
| */ | |
| export const getFormat = async (url, ctx, fn) => { | |
| // Get file extension | |
| const ext = path.extname(url) | |
| // Force extensionless files in `bin` directories to load as commonjs | |
| if (/^file:\/\/\/.*\/bin\//.test(url) && !ext) return { format: 'commonjs' } | |
| // ! Fixes `TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module | |
| // ! "file:///$HOME/node_modules/typescript-esm/dist/tsc-esm"` | |
| if (url.includes('typescript-esm/dist/tsc-esm')) return { format: 'commonjs' } | |
| // Load TypeScript files as ESM | |
| // See `tsconfig.json#ts-node.moduleTypes` for file-specific overrides | |
| if (pkg.type === 'module' && ext === '.ts') return { format: 'module' } | |
| // Defer to ts-node for all other URLs | |
| return await hooks.getFormat(url, ctx, fn) | |
| } | |
| /** | |
| * Returns the resolved file URL for a given module `specifier` and parent URL. | |
| * | |
| * @see https://github.com/TypeStrong/ts-node/discussions/1450 | |
| * @see https://github.com/dividab/tsconfig-paths | |
| * | |
| * @async | |
| * @param {string} specifier - Module specifier | |
| * @param {EsmLoader.HookContext.Resolve} ctx - Function context | |
| * @param {string} [ctx.parentURL] - URL of module that imported `specifier` | |
| * @param {EsmLoader.Hooks.Resolve} fn - Default fn | |
| * @return {Promise<EsmLoader.HookResult.Resolve>} Resolved file URL | |
| */ | |
| export const resolve = async (specifier, ctx, fn) => { | |
| // Attempt to resolve specifier using path mappings | |
| const match = matchSpecifier(specifier, ctx) | |
| // Update specifier if match was found | |
| if (match !== specifier) specifier = match | |
| // Defer to ts-node | |
| return await hooks.resolve(specifier, ctx, fn) | |
| } | |
| /** | |
| * Applies transformations to `source`. | |
| * | |
| * @async | |
| * @param {EsmLoader.Source} source - Source code to transform: ; | |
| * @param {EsmLoader.HookContext.TransformSource} ctx - Function context | |
| * @param {EsmLoader.Format} [ctx.format] - Module format of source code | |
| * @param {string} [ctx.url] - Source code file URL | |
| * @param {EsmLoader.Hooks.TransformSource} fn - Default transform fn | |
| * @return {Promise<EsmLoader.HookResult.TransformSource>} Source code | |
| */ | |
| export const transformSource = async (source, ctx, fn) => { | |
| source = (await hooks.transformSource(source, ctx, fn)).source | |
| /** @see https://github.com/istanbuljs/esm-loader-hook */ | |
| if (TEST) source = (await istanbul.transformSource(source, ctx, fn)).source | |
| return { source } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { createMatchPath } = require('tsconfig-paths') | |
| const { | |
| ExplicitParams, | |
| configLoader | |
| } = require('tsconfig-paths/lib/config-loader') | |
| /** | |
| * @file Helpers - matchSpecifier | |
| * @module tools/helpers/matchSpecifier | |
| */ | |
| /** | |
| * Attempts to match `specifier` to a path alias defined in a tsconfig file. | |
| * | |
| * @see https://github.com/TypeStrong/ts-node/discussions/1450 | |
| * @see https://github.com/dividab/tsconfig-paths | |
| * | |
| * @param {string} specifier - Module specifier | |
| * @param {EsmLoader.HookContext.Resolve} ctx - Function context | |
| * @param {string} [ctx.parentURL] - Parent module specifier | |
| * @param {ExplicitParams} [explicitParams] - Tsconfig path mapping options | |
| * @return {string} Resolved file URL | |
| * @throws {Error} | |
| */ | |
| const matchSpecifier = (specifier, ctx, explicitParams) => { | |
| // Load TypeScript config to get path mappings | |
| const result = configLoader({ cwd: process.cwd(), explicitParams }) | |
| // Handle possible error | |
| if (result.resultType === 'failed') throw new Error(result.message) | |
| // Get base URL and path aliases | |
| const { absoluteBaseUrl, addMatchAll, mainFields, paths } = result | |
| // Create path matching function | |
| const matchPath = createMatchPath( | |
| absoluteBaseUrl, | |
| paths, | |
| mainFields, | |
| addMatchAll | |
| ) | |
| // Attempt to match specifier using path mappings | |
| const match = matchPath(specifier) | |
| // Return patch match if found. Otherwise return original specifier | |
| return match || specifier | |
| } | |
| module.exports = matchSpecifier |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const path = require('path') | |
| const { ExplicitParams } = require('tsconfig-paths/lib/config-loader') | |
| const matchSpecifier = require('./match-specifier.cjs') | |
| const { loadSync: tsconfig } = require('tsconfig/dist/tsconfig') | |
| /** | |
| * @file Helpers - tsconfigPaths | |
| * @module tools/helpers/tsconfigPaths | |
| */ | |
| /** | |
| * Installs a custom module load function that can adhere to paths in tsconfig. | |
| * | |
| * @param {ExplicitParams} [explicitParams] - Tsconfig path mapping options | |
| * @return {() => void} Function to undo paths registration | |
| */ | |
| function register(explicitParams) { | |
| // Resolve cjs directory index files | |
| require.extensions['.cjs'] = require.extensions['.js'] | |
| // Get original node module and resolveFilename function | |
| const Module = require('module') | |
| const resolveFilename = Module._resolveFilename | |
| /** | |
| * Patches node's module loading. | |
| * | |
| * @param {string} specifier - Module specifier | |
| * @param {NodeJS.Module | null} parent - Module that imported `specifier` | |
| * @return {string} Resolved file URL | |
| */ | |
| Module._resolveFilename = function (specifier, parent) { | |
| const CORE_MODULE = !!register.coreModules(Module.builtinModules)[specifier] | |
| let args = arguments | |
| if (!CORE_MODULE) { | |
| try { | |
| // Get match context | |
| const ctx = { parentURL: parent ? parent.id : undefined } | |
| // Attempt to resolve specifier using path mappings | |
| const match = matchSpecifier(specifier, ctx, explicitParams) | |
| // Update specifier if match was found | |
| if (match !== specifier) { | |
| args = [match, ...Array.prototype.slice.call(args, 1)] | |
| } | |
| } catch (error) { | |
| console.error(`${error.message}. tsconfig-paths skipped`) | |
| return () => void 0 | |
| } | |
| } | |
| return resolveFilename.apply(this, args) | |
| } | |
| // Restore node's module loading to original state | |
| return () => { | |
| Module._resolveFilename = resolveFilename | |
| } | |
| } | |
| /** | |
| * Returns a map containing defined core modules. | |
| * | |
| * @param {string[]} [builtins] - Names of builtin core modules | |
| * @return {Record<string, boolean>} Core module map | |
| */ | |
| register.coreModules = builtins => { | |
| builtins = builtins || [ | |
| 'assert', | |
| 'buffer', | |
| 'child_process', | |
| 'cluster', | |
| 'crypto', | |
| 'dgram', | |
| 'dns', | |
| 'domain', | |
| 'events', | |
| 'fs', | |
| 'http', | |
| 'https', | |
| 'net', | |
| 'os', | |
| 'path', | |
| 'punycode', | |
| 'querystring', | |
| 'readline', | |
| 'stream', | |
| 'string_decoder', | |
| 'tls', | |
| 'tty', | |
| 'url', | |
| 'util', | |
| 'v8', | |
| 'vm', | |
| 'zlib' | |
| ] | |
| const core = {} | |
| for (let module of builtins) core[module] = true | |
| return core | |
| } | |
| const baseUrl = path.resolve(__dirname, '..', '..') | |
| register({ | |
| addMatchAll: true, | |
| baseUrl, | |
| mainFields: ['main'], | |
| paths: tsconfig(baseUrl, 'tsconfig.json').config.compilerOptions.paths | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment