Skip to content

Commit

Permalink
Feat: support Node's --experimental-default-type flag
Browse files Browse the repository at this point in the history
  • Loading branch information
Septh committed Feb 7, 2024
1 parent a604446 commit c4d170e
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 44 deletions.
26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Program",
"type": "node",
"request": "launch",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"runtimeArgs": [
"--import=./lib/index.js",
"--experimental-default-type module"
],
"program": "zz_test.js",
"outFiles": [
"${workspaceFolder}/lib/*.js"
],
"sourceMaps": true
}
]
}
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default defineConfig([
dir: 'lib',
format: 'esm',
generatedCode: 'es2015',
sourcemap: "hidden",
sourcemap: true,
sourcemapExcludeSources: true
},
plugins: [
Expand Down
32 changes: 18 additions & 14 deletions source/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
declare global {
namespace NodeJS {
interface Module {
_compile(code: string, filename: string): string
}
type ModuleType = 'commonjs' | 'module'

interface NodeError {
code: string
}
// In package.json.
interface PkgType {
type?: ModuleType
}

type ModuleType = 'commonjs' | 'module'
interface PkgType {
type?: ModuleType
}
}
// The data passed to the initialize() hook.
interface InitializeHookData {
self: string
defaultModuleType: ModuleType
}

namespace NodeJS {
interface Module {
_compile(code: string, filename: string): string
}
}
}

declare module 'module' {
export const _extensions: NodeJS.RequireExtensions;
export const _extensions: NodeJS.RequireExtensions
}

// This file needs to be a module
export {}
export { }
17 changes: 8 additions & 9 deletions source/cjs-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs'

const require = createRequire(import.meta.url)

function transpile(m: Module, format: NodeJS.ModuleType, filePath: string) {
function transpile(m: Module, format: ModuleType, filePath: string) {
// Notes:
// - This function is called by the CJS loader so it must be sync.
// - We lazy-load Sucrase as the CJS loader may well never be used
Expand All @@ -18,8 +18,8 @@ function transpile(m: Module, format: NodeJS.ModuleType, filePath: string) {
}

const unknownType = Symbol()
const pkgTypeCache = new Map<string, NodeJS.ModuleType | Symbol>()
function nearestPackageType(file: string): NodeJS.ModuleType {
const pkgTypeCache = new Map<string, ModuleType | Symbol>()
function nearestPackageType(file: string, defaultType: ModuleType): ModuleType {
for (
let current = path.dirname(file), previous: string | undefined = undefined;
previous !== current;
Expand All @@ -30,13 +30,13 @@ function nearestPackageType(file: string): NodeJS.ModuleType {
if (!format) {
try {
const data = readFileSync(pkgFile, 'utf-8')
const { type } = JSON.parse(data) as NodeJS.PkgType
const { type } = JSON.parse(data) as PkgType
format = type === 'module' || type ==='commonjs'
? type
: unknownType
}
catch(err) {
const { code } = err as NodeJS.NodeError
const { code } = err as NodeJS.ErrnoException
if (code !== 'ENOENT')
console.error(err)
format = unknownType
Expand All @@ -49,12 +49,11 @@ function nearestPackageType(file: string): NodeJS.ModuleType {
return format
}

// TODO: decide default format based on --experimental-default-type
return 'commonjs'
return defaultType
}

export function install_cjs_hooks() {
Module._extensions['.ts'] = (m, filename) => transpile(m, nearestPackageType(filename), filename)
export function install_cjs_hooks(defaultType: ModuleType) {
Module._extensions['.ts'] = (m, filename) => transpile(m, nearestPackageType(filename, defaultType), filename)
Module._extensions['.cts'] = (m, filename) => transpile(m, 'commonjs', filename)
Module._extensions['.mts'] = (m, filename) => transpile(m, 'module', filename)
}
12 changes: 6 additions & 6 deletions source/cjs-transform.cts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { transform as sucrase, type Transform } from 'sucrase'
import { transform as sucrase, type Transform, type TransformResult } from 'sucrase'

const transforms: Record<NodeJS.ModuleType, Transform[]> = {
const transforms: Record<ModuleType, Transform[]> = {
commonjs: [ 'typescript', 'imports' ],
module: [ 'typescript' ]
}

export function transform(source: string, format: NodeJS.ModuleType, filePath: string) {
export function transform(source: string, format: ModuleType, filePath: string) {
const { code, sourceMap } = sucrase(source, {
filePath,
transforms: transforms[format],
Expand All @@ -16,10 +16,10 @@ export function transform(source: string, format: NodeJS.ModuleType, filePath: s
sourceMapOptions: {
compiledFilename: filePath
}
})
}) as Required<TransformResult>

sourceMap!.sourceRoot = ''
sourceMap!.sources = [ filePath ]
sourceMap.sourceRoot = ''
sourceMap.sources = [ filePath ]
// sourceMap.sourcesContent = [ source ]

return code + '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,'
Expand Down
21 changes: 11 additions & 10 deletions source/esm-hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { readFile } from 'node:fs/promises'
import { type InitializeHook, type ResolveHook, type LoadHook, type ModuleSource, createRequire } from 'node:module'
import { createRequire, type InitializeHook, type ResolveHook, type LoadHook } from 'node:module'

const { transform } = createRequire(import.meta.url)('./cjs-transform.cjs') as typeof import('./cjs-transform.cjs')

let self: string
export const initialize: InitializeHook<string> = data => {
self = data
let defaultModuleType: ModuleType
export const initialize: InitializeHook<InitializeHookData> = data => {
self = data.self
defaultModuleType = data.defaultModuleType
}

export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
Expand Down Expand Up @@ -37,8 +39,8 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
}

const unknownType = Symbol()
const pkgTypeCache = new Map<string, NodeJS.ModuleType | Symbol>()
async function nearestPackageType(file: string): Promise<NodeJS.ModuleType> {
const pkgTypeCache = new Map<string, ModuleType | Symbol>()
async function nearestPackageType(file: string): Promise<ModuleType> {
for (
let current = path.dirname(file), previous: string | undefined = undefined;
previous !== current;
Expand All @@ -48,9 +50,9 @@ async function nearestPackageType(file: string): Promise<NodeJS.ModuleType> {
let format = pkgTypeCache.get(pkgFile)
if (!format) {
format = await readFile(pkgFile, 'utf-8')
.then(data => (JSON.parse(data) as NodeJS.PkgType).type ?? unknownType)
.then(data => (JSON.parse(data) as PkgType).type ?? unknownType)
.catch(err => {
const { code } = err as NodeJS.NodeError
const { code } = err as NodeJS.ErrnoException
if (code !== 'ENOENT')
console.error(err)
return unknownType
Expand All @@ -62,8 +64,7 @@ async function nearestPackageType(file: string): Promise<NodeJS.ModuleType> {
return format
}

// TODO: decide default format based on --experimental-default-type
return 'commonjs'
return defaultModuleType
}

export const load: LoadHook = async (url, context, nextLoad) => {
Expand All @@ -77,7 +78,7 @@ export const load: LoadHook = async (url, context, nextLoad) => {
// Determine the output format based on the file's extension
// or the nearest package.json's `type` field.
const filePath = fileURLToPath(url)
const format: NodeJS.ModuleType = (
const format: ModuleType = (
ext[1] === '.ts'
? await nearestPackageType(filePath)
: ext[1] === '.mts'
Expand Down
23 changes: 19 additions & 4 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,31 @@ if (
|| (major === 20 && minor >= 6)
|| (major === 18 && minor >= 19)
) {
const self = import.meta.url

// Determine the default module type.
let defaultModuleType: ModuleType = 'commonjs'
const argc = process.execArgv.findIndex(arg => arg.startsWith('--experimental-default-type'))
if (argc >= 0) {
const argv = process.execArgv[argc].split('=')
const type = argv.length === 1
? process.execArgv[argc + 1]
: argv[1]
if (type === 'module' || type === 'commonjs')
defaultModuleType = type
}

// Install the esm hooks -- those are run in a worker thread.
Module.register('./esm-hooks.js', {
const self = import.meta.url
Module.register<InitializeHookData>('./esm-hooks.js', {
parentURL: self,
data: self
data: {
self,
defaultModuleType
}
})

// Install the cjs hooks -- those are run synchronously in the main thread.
install_cjs_hooks()
install_cjs_hooks(defaultModuleType)

// Enable source map support.
process.setSourceMapsEnabled(true)
Expand Down

0 comments on commit c4d170e

Please sign in to comment.