Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support native type stripping for Node >= v23 #442

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading