Skip to content

Commit

Permalink
feat: support native type stripping for Node >= v23
Browse files Browse the repository at this point in the history
  • Loading branch information
jean-michelet committed Jan 25, 2025
1 parent 69d42a5 commit 4b319e5
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 88 deletions.
103 changes: 82 additions & 21 deletions lib/find-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async function findPlugins (dir, options) {
const { opts, prefix } = options

const pluginTree = {
[prefix || '/']: { hooks: [], plugins: [] }
[prefix || '/']: { hooks: [], plugins: [] },
}

await buildTree(pluginTree, dir, { prefix, opts, depth: 0, hooks: [] })
Expand All @@ -24,24 +24,48 @@ async function buildTree (pluginTree, dir, { prefix, opts, depth, hooks }) {

const dirEntries = await readdir(dir, { withFileTypes: true })

const currentDirHooks = findCurrentDirHooks(pluginTree, { dir, dirEntries, hooks, opts, prefix })

const { indexDirEntry, hasNoDirectory } = processIndexDirEntryIfExists(pluginTree, { dirEntries, opts, dir, prefix })
const currentDirHooks = findCurrentDirHooks(pluginTree, {
dir,
dirEntries,
hooks,
opts,
prefix,
})

const { indexDirEntry, hasNoDirectory } = processIndexDirEntryIfExists(
pluginTree,
{ dirEntries, opts, dir, prefix }
)
if (hasNoDirectory) {
return
}

// Contains package.json but no index.js file?
const packageDirEntry = dirEntries.find((dirEntry) => dirEntry.name === 'package.json')
const packageDirEntry = dirEntries.find(
(dirEntry) => dirEntry.name === 'package.json'
)
if (packageDirEntry && !indexDirEntry) {
throw new Error(`@fastify/autoload cannot import plugin at '${dir}'. To fix this error rename the main entry file to 'index.js' (or .cjs, .mjs, .ts).`)
throw new Error(
`@fastify/autoload cannot import plugin at '${dir}'. To fix this error rename the main entry file to 'index.js' (or .cjs, .mjs, .ts).`
)
}

// Otherwise treat each script file as a plugin
await processDirContents(pluginTree, { dirEntries, opts, indexDirEntry, prefix, dir, depth, currentDirHooks })
await processDirContents(pluginTree, {
dirEntries,
opts,
indexDirEntry,
prefix,
dir,
depth,
currentDirHooks,
})
}

function findCurrentDirHooks (pluginTree, { dir, dirEntries, hooks, opts, prefix }) {
function findCurrentDirHooks (
pluginTree,
{ dir, dirEntries, hooks, opts, prefix }
) {
if (!opts.autoHooks) return []

let currentDirHooks = []
Expand All @@ -51,7 +75,9 @@ function findCurrentDirHooks (pluginTree, { dir, dirEntries, hooks, opts, prefix
}

// Contains autohooks file?
const autoHooks = dirEntries.find((dirEntry) => opts.autoHooksPattern.test(dirEntry.name))
const autoHooks = dirEntries.find((dirEntry) =>
opts.autoHooksPattern.test(dirEntry.name)
)
if (autoHooks) {
const file = join(dir, autoHooks.name)
const { type } = getScriptType(file, opts.packageType)
Expand All @@ -70,22 +96,32 @@ function findCurrentDirHooks (pluginTree, { dir, dirEntries, hooks, opts, prefix
return currentDirHooks
}

function processIndexDirEntryIfExists (pluginTree, { opts, dirEntries, dir, prefix }) {
function processIndexDirEntryIfExists (
pluginTree,
{ opts, dirEntries, dir, prefix }
) {
// Contains index file?
const indexDirEntry = dirEntries.find((dirEntry) => opts.indexPattern.test(dirEntry.name))
const indexDirEntry = dirEntries.find((dirEntry) =>
opts.indexPattern.test(dirEntry.name)
)
if (!indexDirEntry) return { indexDirEntry }

const file = join(dir, indexDirEntry.name)
const { language, type } = getScriptType(file, opts.packageType)
handleTypeScriptSupport(file, language, true)
accumulatePlugin({ file, type, opts, pluginTree, prefix })

const hasNoDirectory = dirEntries.every((dirEntry) => !dirEntry.isDirectory())
const hasNoDirectory = dirEntries.every(
(dirEntry) => !dirEntry.isDirectory()
)

return { indexDirEntry, hasNoDirectory }
}

async function processDirContents (pluginTree, { dirEntries, opts, indexDirEntry, prefix, dir, depth, currentDirHooks }) {
async function processDirContents (
pluginTree,
{ dirEntries, opts, indexDirEntry, prefix, dir, depth, currentDirHooks }
) {
for (const dirEntry of dirEntries) {
if (opts.ignorePattern && RegExp(opts.ignorePattern).test(dirEntry.name)) {
continue
Expand All @@ -94,7 +130,15 @@ async function processDirContents (pluginTree, { dirEntries, opts, indexDirEntry
const atMaxDepth = Number.isFinite(opts.maxDepth) && opts.maxDepth <= depth
const file = join(dir, dirEntry.name)
if (dirEntry.isDirectory() && !atMaxDepth) {
await processDirectory(pluginTree, { prefix, opts, dirEntry, dir, file, depth, currentDirHooks })
await processDirectory(pluginTree, {
prefix,
opts,
dirEntry,
dir,
file,
depth,
currentDirHooks,
})
} else if (indexDirEntry) {
// An index.js file is present in the directory so we ignore the others modules (but not the subdirectories)
} else if (dirEntry.isFile() && opts.scriptPattern.test(dirEntry.name)) {
Expand All @@ -103,8 +147,11 @@ async function processDirContents (pluginTree, { dirEntries, opts, indexDirEntry
}
}

async function processDirectory (pluginTree, { prefix, opts, dirEntry, dir, file, depth, currentDirHooks }) {
let prefixBreadCrumb = (prefix ? `${prefix}/` : '/')
async function processDirectory (
pluginTree,
{ prefix, opts, dirEntry, dir, file, depth, currentDirHooks }
) {
let prefixBreadCrumb = prefix ? `${prefix}/` : '/'
if (opts.dirNameRoutePrefix === true) {
prefixBreadCrumb += dirEntry.name
} else if (typeof opts.dirNameRoutePrefix === 'function') {
Expand All @@ -116,7 +163,12 @@ async function processDirectory (pluginTree, { prefix, opts, dirEntry, dir, file

// Pass hooks forward to next level
const hooks = opts.autoHooks && opts.cascadeHooks ? currentDirHooks : []
await buildTree(pluginTree, file, { opts, prefix: prefixBreadCrumb, depth: depth + 1, hooks })
await buildTree(pluginTree, file, {
opts,
prefix: prefixBreadCrumb,
depth: depth + 1,
hooks,
})
}

function processFile (pluginTree, { file, opts, dirEntry, prefix }) {
Expand Down Expand Up @@ -144,9 +196,18 @@ function accumulatePlugin ({ file, type, opts, pluginTree, prefix }) {
}

function handleTypeScriptSupport (file, language, isHook = false) {
if (language === 'typescript' && !runtime.supportTypeScript) {
throw new Error(`@fastify/autoload cannot import ${isHook ? 'hooks ' : ''}plugin at '${file}'. To fix this error compile TypeScript to JavaScript or use 'ts-node' to run your app.`)
}
if (
language === 'typescript' &&
/* c8 ignore start - This cannot be reached from Node 23 native type-stripping */
!runtime.supportTypeScript
) {
throw new Error(
`@fastify/autoload cannot import ${
isHook ? 'hooks ' : ''
}plugin at '${file}'. To fix this error compile TypeScript to JavaScript or use 'ts-node' to run your app.`
)
}
/* c8 ignore end */
}

function filterPath (path, filter) {
Expand All @@ -165,7 +226,7 @@ const typescriptPattern = /\.(?:ts|mts|cts)$/iu
function getScriptType (fname, packageType) {
return {
language: typescriptPattern.test(fname) ? 'typescript' : 'javascript',
type: determineModuleType(fname, packageType)
type: determineModuleType(fname, packageType),
}
}

Expand Down
61 changes: 30 additions & 31 deletions lib/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let preloadModules
function checkPreloadModules (moduleName) {
/* c8 ignore start */
// nullish needed for non Node.js runtime
preloadModules ??= (process._preload_modules ?? [])
preloadModules ??= process._preload_modules ?? []
/* c8 ignore stop */
return preloadModules.includes(moduleName)
}
Expand All @@ -28,85 +28,80 @@ function checkPreloadModulesString (moduleName) {
}

function checkEnvVariable (name, value) {
return value
? process.env[name] === value
: process.env[name] !== undefined
return value ? process.env[name] === value : process.env[name] !== undefined
}

const runtime = {}
// use Object.defineProperties to provide lazy load
Object.defineProperties(runtime, {
tsNode: {
get () {
cache.tsNode ??= (
cache.tsNode ??=
// --require tsnode/register
(Symbol.for('ts-node.register.instance') in process) ||
Symbol.for('ts-node.register.instance') in process ||
// --loader ts-node/esm
checkProcessArgv('ts-node/esm') ||
// ts-node-dev
!!process.env.TS_NODE_DEV
)
return cache.tsNode
}
},
},
babelNode: {
get () {
cache.babelNode ??= checkProcessArgv('babel-node')
return cache.babelNode
}
},
},
vitest: {
get () {
cache.vitest ??= (
cache.vitest ??=
checkEnvVariable('VITEST', 'true') ||
checkEnvVariable('VITEST_WORKER_ID')
)
return cache.vitest
}
},
},
jest: {
get () {
cache.jest ??= checkEnvVariable('JEST_WORKER_ID')
return cache.jest
}
},
},
swc: {
get () {
cache.swc ??= (
cache.swc ??=
checkPreloadModules('@swc/register') ||
checkPreloadModules('@swc-node/register') ||
checkProcessArgv('.bin/swc-node')
)
return cache.swc
}
},
},
tsm: {
get () {
cache.tsm ??= checkPreloadModules('tsm')
return cache.tsm
}
},
},
esbuild: {
get () {
cache.esbuild ??= checkPreloadModules('esbuild-register')
return cache.esbuild
}
},
},
tsx: {
get () {
cache.tsx ??= checkPreloadModulesString('tsx')
return cache.tsx
}
},
},
tsimp: {
get () {
cache.tsimp ??= checkProcessArgv('tsimp/import')
return cache.tsimp
}
},
},
supportTypeScript: {
get () {
cache.supportTypeScript ??= (
cache.supportTypeScript ??=
checkEnvVariable('FASTIFY_AUTOLOAD_TYPESCRIPT') ||
runtime.tsNode ||
runtime.vitest ||
Expand All @@ -116,21 +111,25 @@ Object.defineProperties(runtime, {
runtime.tsm ||
runtime.tsx ||
runtime.esbuild ||
runtime.tsimp
)
runtime.tsimp ||
runtime.nodeVersion >= 23

return cache.supportTypeScript
}
},
},
forceESM: {
get () {
cache.forceESM ??= (
checkProcessArgv('ts-node/esm') ||
runtime.vitest ||
false
)
cache.forceESM ??=
checkProcessArgv('ts-node/esm') || runtime.vitest || false
return cache.forceESM
}
}
},
},
nodeVersion: {
get () {
cache.nodeVersion ??= Number(process.version.split('.')[0].slice(1))
return cache.nodeVersion
},
},
})

module.exports = runtime
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run typescript && npm run typescript:jest && npm run typescript:swc-node-register && npm run typescript:tsm && npm run typescript:tsx && npm run typescript:vitest && npm run typescript:esbuild && npm run unit",
"test": "npm run typescript && npm run typescript:native npm run typescript:jest && npm run typescript:swc-node-register && npm run typescript:tsm && npm run typescript:tsx && npm run typescript:vitest && npm run typescript:esbuild && npm run unit",
"typescript": "tsd",
"typescript:jest": "jest",
"typescript:esm": "node scripts/unit-typescript-esm.js",
Expand All @@ -17,6 +17,7 @@
"typescript:tsx": "node scripts/unit-typescript-tsx.js",
"typescript:tsimp": "node scripts/unit-typescript-tsimp.js",
"typescript:esbuild": "node scripts/unit-typescript-esbuild.js",
"typescript:native": "node scripts/unit-typescript-native-type-stripping.js",
"typescript:vitest": "vitest run",
"typescript:vitest:dev": "vitest",
"unit": "node scripts/unit.js",
Expand Down
30 changes: 30 additions & 0 deletions scripts/unit-typescript-native-type-stripping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict'

const { exec } = require('node:child_process')
const runtime = require('../lib/runtime')

function common () {
const args = ['node', 'test/typescript-common/index.ts']
const child = exec(args.join(' '), {
shell: true,
})
child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)
child.once('close', () => esm())
}

function esm () {
const args = ['node', 'test/typescript-esm/forceESM.ts']

const child = exec(args.join(' '), {
shell: true,
})

child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)
child.once('close', process.exit)
}

if (runtime.nodeVersion >= 23) {
common()
}
5 changes: 2 additions & 3 deletions scripts/unit-typescript-tsx.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
'use strict'

const { exec } = require('node:child_process')

const version = Number(process.version.split('.')[0].slice(1))
const runtime = require('../lib/runtime')

const args = [
'npx',
version >= 18 ? '--node-options=--import=tsx' : '',
runtime.nodeVersion >= 18 ? '--node-options=--import=tsx' : '',
'tsnd',
'test/typescript/basic.ts'
]
Expand Down
Loading

0 comments on commit 4b319e5

Please sign in to comment.