From 46848b9b99ab78a785dd167150d8d2761a675ff5 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Fri, 1 Mar 2024 21:31:03 -0800 Subject: [PATCH 01/12] Support .cjs and .cts configs and add explicit checks and errors for ESM --- ...-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json | 11 ++ packages/just-task/package.json | 1 + packages/just-task/src/cli.ts | 65 +++++----- packages/just-task/src/config.ts | 112 ++++++++++++++---- packages/just-task/src/index.ts | 12 +- 5 files changed, 141 insertions(+), 60 deletions(-) create mode 100644 change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json diff --git a/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json new file mode 100644 index 00000000..ee99770d --- /dev/null +++ b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "Support .cjs and .cts configs and add explicit checks and errors for ESM", + "packageName": "just-task", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} diff --git a/packages/just-task/package.json b/packages/just-task/package.json index 96066e03..43bf99bc 100644 --- a/packages/just-task/package.json +++ b/packages/just-task/package.json @@ -34,6 +34,7 @@ "chalk": "^4.0.0", "chokidar": "^3.5.2", "fs-extra": "^11.0.0", + "just-scripts-utils": "^2.0.1", "just-task-logger": ">=1.2.1 <2.0.0", "resolve": "^1.19.0", "undertaker": "^1.3.0", diff --git a/packages/just-task/src/cli.ts b/packages/just-task/src/cli.ts index f57bb56b..e60f2b03 100644 --- a/packages/just-task/src/cli.ts +++ b/packages/just-task/src/cli.ts @@ -39,41 +39,50 @@ function showHelp() { } } -// Define a built-in option of "config" so users can specify which path to choose for configurations -option('config', { - describe: 'path to a just configuration file (includes the file name, e.g. /path/to/just.config.ts)', -}); -option('defaultConfig', { - describe: - 'path to a default just configuration file that will be used when the current project does not have a just configuration file. (includes the file name, e.g. /path/to/just.config.ts)', -}); -option('esm', { - describe: - 'Configure ts-node to support imports of ESM package (changes TS module/moduleResolution settings to Node16)', -}); +async function run() { + // Define a built-in option of "config" so users can specify which path to choose for configurations + option('config', { + describe: 'path to a just configuration file, e.g. ./path/to/just.config.ts', + }); + option('defaultConfig', { + describe: + 'path to a default just configuration file that will be used when the current project does not have a just configuration file. ' + + '(includes the file name, e.g. /path/to/just.config.ts)', + }); + option('esm', { + describe: + 'Configure ts-node to support dynamic imports of ESM package (changes TS module/moduleResolution settings to Node16). ' + + 'Note that this does NOT enable full ES modules support.', + }); -const registry = undertaker.registry(); + const registry = undertaker.registry(); -const configModule = readConfig(); + const configModule = await readConfig(); -// Support named task function as exports of a config module -if (configModule && typeof configModule === 'object') { - for (const taskName of Object.keys(configModule)) { - if (typeof configModule[taskName] == 'function') { - task(taskName, configModule[taskName]); + // Support named task function as exports of a config module + if (configModule && typeof configModule === 'object') { + for (const taskName of Object.keys(configModule)) { + if (typeof configModule[taskName] == 'function') { + task(taskName, configModule[taskName]); + } } } -} -const command = parseCommand(); + const command = parseCommand(); -if (command) { - if (registry.get(command)) { - undertaker.series(registry.get(command))(() => undefined); + if (command) { + if (registry.get(command)) { + undertaker.series(registry.get(command))(() => undefined); + } else { + logger.error(`Command not defined: ${command}`); + process.exitCode = 1; + } } else { - logger.error(`Command not defined: ${command}`); - process.exitCode = 1; + showHelp(); } -} else { - showHelp(); } + +run().catch(e => { + logger.error(e.stack || e.message || e); + process.exit(1); +}); diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index ebc02dd9..b843ddfb 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -4,12 +4,23 @@ import * as path from 'path'; import { argv } from './option'; import { resolve } from './resolve'; import { mark, logger } from 'just-task-logger'; +import { readPackageJson } from 'just-scripts-utils'; import { enableTypeScript } from './enableTypeScript'; import yargsParser = require('yargs-parser'); import { TaskFunction } from './interfaces'; export function resolveConfigFile(args: yargsParser.Arguments): string | null { - for (const entry of [args.config, './just.config.js', './just-task.js', './just.config.ts', args.defaultConfig]) { + const paths = [ + args.config, + './just.config.js', + './just-task.js', + './just.config.ts', + './just.config.cjs', + './just.config.cts', + args.defaultConfig, + ].filter(Boolean); + + for (const entry of paths) { const configFile = resolve(entry); if (configFile) { return configFile; @@ -19,39 +30,88 @@ export function resolveConfigFile(args: yargsParser.Arguments): string | null { return null; } -export function readConfig(): { [key: string]: TaskFunction } | void { - // uses a separate instance of yargs to first parse the config (without the --help in the way) so we can parse the configFile first regardless +export async function readConfig(): Promise<{ [key: string]: TaskFunction } | void> { + // uses a separate instance of yargs to first parse the config (without the --help in the way) + // so we can parse the configFile first regardless const args = argv(); const configFile = resolveConfigFile(args); + const packageJson = readPackageJson(process.cwd()); + const packageIsESM = packageJson?.type === 'module'; - if (configFile && fs.existsSync(configFile)) { - const ext = path.extname(configFile); - if (ext === '.ts' || ext === '.tsx') { - // TODO: add option to do typechecking as well - enableTypeScript({ transpileOnly: true, esm: args.esm }); - } + if (!configFile) { + const newConfigName = packageIsESM ? 'just.config.cjs' : 'just.config.js'; + logger.error( + `Config file not found! Please create a file called "${newConfigName}" ` + + `in the root of the project next to "package.json".`, + ); + process.exit(1); + } - try { - const configModule = require(configFile); + if (!fs.existsSync(configFile)) { + logger.error(`The specified config file "${configFile}" doesn't exit or couldn't be resolved.`); + process.exit(1); + } + + const esmMessage = ` +Just currently does not support ESM for the config file (${configFile}). +Ensure the file has a .cjs or .cts extension and change any top-level imports to require. - mark('registry:configModule'); +If you need to load an ES module in the config, use dynamic import() within an async function${ + // this new mode is automatically enabled for ESM packages + packageIsESM ? '' : `\nand pass the --esm flag to just-scripts` + }. +(Task functions may be async, and the config file may export an async function as module.exports.) +`; - if (typeof configModule === 'function') { - configModule(); - } + const configContents = fs.readFileSync(configFile, 'utf8'); + const ext = path.extname(configFile).toLowerCase(); + const isTS = /^\.[cm]?tsx?$/.test(ext); - logger.perf('registry:configModule'); + // Check for common ESM patterns before requiring the file + if ( + // Explicit ESM extension + ext.startsWith('.m') || + // ESM package: .js or .ts files will be implicitly handled as ESM + (packageIsESM && !ext.startsWith('.c')) || + // JS or explicit .cts file with top-level import + ((!isTS || ext.startsWith('.cts')) && /^import /m.test(configContents)) + ) { + logger.error(esmMessage); + process.exit(1); + } + + if (isTS) { + enableTypeScript({ transpileOnly: true, esm: args.esm || packageIsESM }); + } - return configModule; - } catch (e) { - logger.error(`Invalid configuration file: ${configFile}`); - logger.error(`Error: ${e.stack || e.message || e}`); - process.exit(1); + try { + const configModule = require(configFile); + + mark('registry:configModule'); + + if (typeof configModule === 'function') { + await configModule(); } - } else { - logger.error( - `Cannot find config file "${configFile}".`, - `Please create a file called "just.config.js" in the root of the project next to "package.json".`, - ); + + logger.perf('registry:configModule'); + + return configModule; + } catch (e) { + logger.error(`Invalid configuration file: ${configFile}`); + logger.error(`Error: ${e.stack || e.message || e}`); + + if ( + // config or something it required was an ES module + // (or it used a dynamic import() in a non-ESM package without the --esm flag) + e.code === 'ERR_REQUIRE_ESM' || + // import in a CJS module + (e.name === 'SyntaxError' && /\bimport\b/.test(e.message || '')) || + // require in an ES module + (e.name === 'ReferenceError' && /\brequire\b/.test(e.message || '')) + ) { + logger.error(esmMessage); + } + + process.exit(1); } } diff --git a/packages/just-task/src/index.ts b/packages/just-task/src/index.ts index 9319f38d..cd6da75a 100644 --- a/packages/just-task/src/index.ts +++ b/packages/just-task/src/index.ts @@ -1,10 +1,10 @@ -export * from './undertaker'; -export * from './task'; -export * from './interfaces'; +export { parallel, series, undertaker } from './undertaker'; +export { task } from './task'; +export { Task, TaskContext, TaskFunction } from './interfaces'; export { condition } from './condition'; export { addResolvePath, resetResolvePaths, resolve, resolveCwd } from './resolve'; export { option, argv } from './option'; export { clearCache } from './cache'; -export * from './logger'; -export * from './chain'; -export * from './watch'; +export { Logger, logger, mark } from './logger'; +export { chain } from './chain'; +export { watch } from './watch'; From 3d6bd9c6200f4e988573ec41fc5c71d22005f86d Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 12:30:40 -0700 Subject: [PATCH 02/12] Support ESM configs and explicit .cjs and .mjs config extensions --- .vscode/launch.json | 11 ++ ...-48ef8505-5b31-4b60-bfe2-53e1433985cd.json | 11 ++ ...-9ea30187-bf7e-4cd9-ba0a-e1af7e7b3bdc.json | 11 ++ ...-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json | 2 +- ...-e400c7a0-47ba-4cf5-b712-508ad7481673.json | 11 ++ package.json | 2 +- .../src/tasks/webpackCliInitTask.ts | 6 +- .../just-scripts/src/tasks/webpackTask.ts | 6 +- packages/just-task/etc/just-task.api.md | 11 +- packages/just-task/package.json | 1 + .../just-task/src/__tests__/resolve.spec.ts | 22 ++-- packages/just-task/src/cli.ts | 4 +- packages/just-task/src/config.ts | 121 ++++++++---------- packages/just-task/src/enableTypeScript.ts | 17 ++- packages/just-task/src/resolve.ts | 24 ++++ scripts/jest.config.js | 5 +- tsconfig.json | 4 +- yarn.lock | 13 +- 18 files changed, 178 insertions(+), 104 deletions(-) create mode 100644 change/change-48ef8505-5b31-4b60-bfe2-53e1433985cd.json create mode 100644 change/change-9ea30187-bf7e-4cd9-ba0a-e1af7e7b3bdc.json create mode 100644 change/change-e400c7a0-47ba-4cf5-b712-508ad7481673.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c1e3e198..b94572f5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,17 @@ "sourceMaps": true, "outputCapture": "std", "console": "integratedTerminal" + }, + { + "name": "Debug example-lib build", + "request": "launch", + "type": "node", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "build"], + "cwd": "${workspaceFolder}/packages/example-lib", + "console": "integratedTerminal", + "outputCapture": "std", + "sourceMaps": true } ] } diff --git a/change/change-48ef8505-5b31-4b60-bfe2-53e1433985cd.json b/change/change-48ef8505-5b31-4b60-bfe2-53e1433985cd.json new file mode 100644 index 00000000..cb0f9059 --- /dev/null +++ b/change/change-48ef8505-5b31-4b60-bfe2-53e1433985cd.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "Compile just with TS 4.7 and `module`/`moduleResolution` `\"Node16\"`", + "packageName": "just-scripts", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/change/change-9ea30187-bf7e-4cd9-ba0a-e1af7e7b3bdc.json b/change/change-9ea30187-bf7e-4cd9-ba0a-e1af7e7b3bdc.json new file mode 100644 index 00000000..b52245f3 --- /dev/null +++ b/change/change-9ea30187-bf7e-4cd9-ba0a-e1af7e7b3bdc.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "For ts-node, use `module`/`moduleResolution` `\"Node16\"` if the local TS version supports it", + "packageName": "just-task", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json index ee99770d..2b7c4e18 100644 --- a/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json +++ b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json @@ -2,7 +2,7 @@ "changes": [ { "type": "minor", - "comment": "Support .cjs and .cts configs and add explicit checks and errors for ESM", + "comment": "Support ESM configs and explicit .cjs and .mjs config extensions", "packageName": "just-task", "email": "elcraig@microsoft.com", "dependentChangeType": "patch" diff --git a/change/change-e400c7a0-47ba-4cf5-b712-508ad7481673.json b/change/change-e400c7a0-47ba-4cf5-b712-508ad7481673.json new file mode 100644 index 00000000..4a6da678 --- /dev/null +++ b/change/change-e400c7a0-47ba-4cf5-b712-508ad7481673.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "Compile just with TS 4.7 and `module`/`moduleResolution` `\"Node16\"`", + "packageName": "just-task", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 9c498854..fcc25db8 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "prettier": "^2.8.4", "syncpack": "^9.0.0", "ts-jest": "^29.0.5", - "typescript": "~4.3.5", + "typescript": "~4.7.0", "vuepress": "^1.9.9", "vuepress-plugin-mermaidjs": "^1.9.1", "workspace-tools": "^0.35.2" diff --git a/packages/just-scripts/src/tasks/webpackCliInitTask.ts b/packages/just-scripts/src/tasks/webpackCliInitTask.ts index 7c818606..54bed257 100644 --- a/packages/just-scripts/src/tasks/webpackCliInitTask.ts +++ b/packages/just-scripts/src/tasks/webpackCliInitTask.ts @@ -20,13 +20,13 @@ export function webpackCliInitTask(customScaffold?: string, auto = false): TaskF try { init(null, null, null, '--auto'); } catch (error) { - throw `Webpack-cli init failed with ${error.length} error(s).`; + throw `Webpack-cli init failed with ${(error as any).length} error(s).`; } } else { try { init(); } catch (error) { - throw `Webpack-cli init failed with ${error.length} error(s).`; + throw `Webpack-cli init failed with ${(error as any).length} error(s).`; } } } else { @@ -34,7 +34,7 @@ export function webpackCliInitTask(customScaffold?: string, auto = false): TaskF try { init(null, null, customScaffold); } catch (error) { - throw `Webpack-cli init failed with ${error.length} error(s).`; + throw `Webpack-cli init failed with ${(error as any).length} error(s).`; } } }; diff --git a/packages/just-scripts/src/tasks/webpackTask.ts b/packages/just-scripts/src/tasks/webpackTask.ts index 8cd7ddea..2bb70cba 100644 --- a/packages/just-scripts/src/tasks/webpackTask.ts +++ b/packages/just-scripts/src/tasks/webpackTask.ts @@ -82,11 +82,7 @@ export function webpackTask(options?: WebpackTaskOptions): TaskFunction { return new Promise((resolve, reject) => { wp(webpackConfigs, async (err: Error, stats: any) => { if (options && options.onCompile) { - const results = options.onCompile(err, stats); - - if (typeof results === 'object' && results.then) { - await results; - } + await options.onCompile(err, stats); } if (options && options.outputStats) { diff --git a/packages/just-task/etc/just-task.api.md b/packages/just-task/etc/just-task.api.md index ea25a174..02f4f463 100644 --- a/packages/just-task/etc/just-task.api.md +++ b/packages/just-task/etc/just-task.api.md @@ -10,6 +10,8 @@ import { Arguments } from 'yargs-parser'; import { Duplex } from 'stream'; import type { FSWatcher } from 'chokidar'; import { Logger } from 'just-task-logger'; +import { logger } from 'just-task-logger'; +import { mark } from 'just-task-logger'; import type { Stats } from 'fs'; import { TaskFunction as TaskFunction_2 } from 'undertaker'; import { TaskFunctionParams } from 'undertaker'; @@ -34,6 +36,12 @@ export function clearCache(): void; // @public (undocumented) export function condition(taskName: string, conditional: () => boolean): TaskFunction_2; +export { Logger } + +export { logger } + +export { mark } + // Warning: (ae-forgotten-export) The symbol "OptionConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -118,9 +126,6 @@ export function watch(globs: string | string[], optionsOrListener?: WatchListene // @public (undocumented) type WatchListener = (path: string, stats?: Stats) => void; - -export * from "just-task-logger"; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/just-task/package.json b/packages/just-task/package.json index 43bf99bc..5e4ed6ff 100644 --- a/packages/just-task/package.json +++ b/packages/just-task/package.json @@ -34,6 +34,7 @@ "chalk": "^4.0.0", "chokidar": "^3.5.2", "fs-extra": "^11.0.0", + "import-meta-resolve": "^4.0.0", "just-scripts-utils": "^2.0.1", "just-task-logger": ">=1.2.1 <2.0.0", "resolve": "^1.19.0", diff --git a/packages/just-task/src/__tests__/resolve.spec.ts b/packages/just-task/src/__tests__/resolve.spec.ts index cc8e94c3..f5ab716c 100644 --- a/packages/just-task/src/__tests__/resolve.spec.ts +++ b/packages/just-task/src/__tests__/resolve.spec.ts @@ -202,7 +202,7 @@ describe('resolveConfigFile', () => { resetResolvePaths(); }); - it('default chooses local config', () => { + it('default chooses local config', async () => { mockfs({ config: { 'configArgument.ts': 'formConfig', @@ -210,11 +210,11 @@ describe('resolveConfigFile', () => { }, 'just.config.ts': 'localConfig', }); - const resolvedConfig = config.resolveConfigFile({ config: undefined, defaultConfig: undefined } as any); + const resolvedConfig = await config.resolveConfigFile({}); expect(resolvedConfig).toContain('just.config.ts'); }); - it('config argument wins over local config and defaultConfig', () => { + it('config argument wins over local config and defaultConfig', async () => { mockfs({ config: { 'configArgument.ts': 'formConfig', @@ -222,14 +222,14 @@ describe('resolveConfigFile', () => { }, 'just.config.ts': 'localConfig', }); - const resolvedConfig = config.resolveConfigFile({ + const resolvedConfig = await config.resolveConfigFile({ config: './config/configArgument.ts', defaultConfig: './config/defaultConfigArgument.ts', - } as any); + }); expect(resolvedConfig).toContain('configArgument.ts'); }); - it('local config file wins over defaultConfig', () => { + it('local config file wins over defaultConfig', async () => { mockfs({ config: { 'configArgument.ts': 'formConfig', @@ -237,24 +237,24 @@ describe('resolveConfigFile', () => { }, 'just.config.ts': 'localConfig', }); - const resolvedConfig = config.resolveConfigFile({ + const resolvedConfig = await config.resolveConfigFile({ config: undefined, defaultConfig: './config/defaultConfigArgument.ts', - } as any); + }); expect(resolvedConfig).toContain('just.config.ts'); }); - it('default config is used as last fallback', () => { + it('default config is used as last fallback', async () => { mockfs({ config: { 'configArgument.ts': 'formConfig', 'defaultConfigArgument.ts': 'formDefaultConfig', }, }); - const resolvedConfig = config.resolveConfigFile({ + const resolvedConfig = await config.resolveConfigFile({ config: undefined, defaultConfig: './config/defaultConfigArgument.ts', - } as any); + }); expect(resolvedConfig).toContain('defaultConfigArgument.ts'); }); }); diff --git a/packages/just-task/src/cli.ts b/packages/just-task/src/cli.ts index e60f2b03..516d4062 100644 --- a/packages/just-task/src/cli.ts +++ b/packages/just-task/src/cli.ts @@ -50,9 +50,7 @@ async function run() { '(includes the file name, e.g. /path/to/just.config.ts)', }); option('esm', { - describe: - 'Configure ts-node to support dynamic imports of ESM package (changes TS module/moduleResolution settings to Node16). ' + - 'Note that this does NOT enable full ES modules support.', + describe: 'No longer needed', }); const registry = undertaker.registry(); diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index b843ddfb..64eba6d0 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -2,28 +2,28 @@ import * as fs from 'fs'; import * as path from 'path'; import { argv } from './option'; -import { resolve } from './resolve'; +import { resolve, resolveModern } from './resolve'; import { mark, logger } from 'just-task-logger'; -import { readPackageJson } from 'just-scripts-utils'; import { enableTypeScript } from './enableTypeScript'; -import yargsParser = require('yargs-parser'); import { TaskFunction } from './interfaces'; -export function resolveConfigFile(args: yargsParser.Arguments): string | null { +export async function resolveConfigFile(args: { config?: string; defaultConfig?: string }): Promise { + // Check for the old config paths/extensions first const paths = [ args.config, './just.config.js', './just-task.js', './just.config.ts', + // Add .cjs and .mjs (.cts and .mts don't seem to work with ts-node) './just.config.cjs', - './just.config.cts', + './just.config.mjs', args.defaultConfig, - ].filter(Boolean); + ].filter((p): p is string => !!p); for (const entry of paths) { - const configFile = resolve(entry); - if (configFile) { - return configFile; + const resolved = resolve(entry) || (await resolveModern(entry)); + if (resolved) { + return resolved; } } @@ -33,85 +33,72 @@ export function resolveConfigFile(args: yargsParser.Arguments): string | null { export async function readConfig(): Promise<{ [key: string]: TaskFunction } | void> { // uses a separate instance of yargs to first parse the config (without the --help in the way) // so we can parse the configFile first regardless - const args = argv(); - const configFile = resolveConfigFile(args); - const packageJson = readPackageJson(process.cwd()); - const packageIsESM = packageJson?.type === 'module'; + const args = argv() as { config?: string; defaultConfig?: string }; + const configFile = await resolveConfigFile(args); if (!configFile) { - const newConfigName = packageIsESM ? 'just.config.cjs' : 'just.config.js'; logger.error( - `Config file not found! Please create a file called "${newConfigName}" ` + - `in the root of the project next to "package.json".`, + `Config file not found! Please create a file called "just.config.js" ` + + `in the root of the package next to "package.json".`, ); process.exit(1); } if (!fs.existsSync(configFile)) { - logger.error(`The specified config file "${configFile}" doesn't exit or couldn't be resolved.`); + logger.error(`The specified config file "${configFile}" doesn't exist or couldn't be resolved.`); process.exit(1); } - const esmMessage = ` -Just currently does not support ESM for the config file (${configFile}). -Ensure the file has a .cjs or .cts extension and change any top-level imports to require. - -If you need to load an ES module in the config, use dynamic import() within an async function${ - // this new mode is automatically enabled for ESM packages - packageIsESM ? '' : `\nand pass the --esm flag to just-scripts` - }. -(Task functions may be async, and the config file may export an async function as module.exports.) -`; - - const configContents = fs.readFileSync(configFile, 'utf8'); const ext = path.extname(configFile).toLowerCase(); - const isTS = /^\.[cm]?tsx?$/.test(ext); - - // Check for common ESM patterns before requiring the file - if ( - // Explicit ESM extension - ext.startsWith('.m') || - // ESM package: .js or .ts files will be implicitly handled as ESM - (packageIsESM && !ext.startsWith('.c')) || - // JS or explicit .cts file with top-level import - ((!isTS || ext.startsWith('.cts')) && /^import /m.test(configContents)) - ) { - logger.error(esmMessage); - process.exit(1); - } - if (isTS) { - enableTypeScript({ transpileOnly: true, esm: args.esm || packageIsESM }); + if (ext === '.ts') { + enableTypeScript({ transpileOnly: true }); } + let configModule = undefined; + let importError: unknown; try { - const configModule = require(configFile); - - mark('registry:configModule'); - - if (typeof configModule === 'function') { - await configModule(); + try { + if (ext !== '.cjs') { + // Rather than trying to infer the correct type in all cases, try import first. + configModule = await import(configFile); + } + } catch (e) { + importError = e; } - - logger.perf('registry:configModule'); - - return configModule; + // Fall back to require + configModule ||= require(configFile); } catch (e) { logger.error(`Invalid configuration file: ${configFile}`); - logger.error(`Error: ${e.stack || e.message || e}`); - - if ( - // config or something it required was an ES module - // (or it used a dynamic import() in a non-ESM package without the --esm flag) - e.code === 'ERR_REQUIRE_ESM' || - // import in a CJS module - (e.name === 'SyntaxError' && /\bimport\b/.test(e.message || '')) || - // require in an ES module - (e.name === 'ReferenceError' && /\brequire\b/.test(e.message || '')) - ) { - logger.error(esmMessage); + if (importError) { + logger.error( + `Initially got this error trying to import() the file: ${ + (importError as Error)?.stack || (importError as Error)?.message || importError + }`, + ); + logger.error( + `Then tried to require() the file and got this error: ${(e as Error).stack || (e as Error).message || e}`, + ); + } else { + logger.error((e as Error).stack || (e as Error).message || e); } process.exit(1); } + + mark('registry:configModule'); + + if (typeof configModule === 'function') { + try { + await configModule(); + } catch (e) { + logger.error(`Invalid configuration file: ${configFile}`); + logger.error(`Error running config function: ${(e as Error).stack || (e as Error).message || e}`); + process.exit(1); + } + } + + logger.perf('registry:configModule'); + + return configModule; } diff --git a/packages/just-task/src/enableTypeScript.ts b/packages/just-task/src/enableTypeScript.ts index 92b6cd21..9f33e5cb 100644 --- a/packages/just-task/src/enableTypeScript.ts +++ b/packages/just-task/src/enableTypeScript.ts @@ -1,21 +1,32 @@ +import * as fse from 'fs-extra'; import { resolve } from './resolve'; import { logger } from 'just-task-logger'; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function enableTypeScript({ transpileOnly = true, esm = false }): void { +export function enableTypeScript({ transpileOnly = true }): void { const tsNodeModule = resolve('ts-node'); + if (tsNodeModule) { + // Use module/moduleResolution "node16" if supported for broadest compatibility + let supportsNode16Setting = false; + const typescriptPackageJson = resolve('typescript/package.json'); + if (typescriptPackageJson) { + const typescriptVersion = fse.readJsonSync(typescriptPackageJson).version as string; + const [major, minor] = typescriptVersion.split('.').map(Number); + supportsNode16Setting = major > 4 || (major === 4 && minor >= 7); + } + const tsNode = require(tsNodeModule); tsNode.register({ transpileOnly, skipProject: true, compilerOptions: { target: 'es2017', - module: esm ? 'node16' : 'commonjs', + module: supportsNode16Setting ? 'node16' : 'commonjs', strict: false, skipLibCheck: true, skipDefaultLibCheck: true, - moduleResolution: esm ? 'node16' : 'node', + moduleResolution: supportsNode16Setting ? 'node16' : 'node', allowJs: true, esModuleInterop: true, }, diff --git a/packages/just-task/src/resolve.ts b/packages/just-task/src/resolve.ts index b0ee73e7..4a1bde6f 100644 --- a/packages/just-task/src/resolve.ts +++ b/packages/just-task/src/resolve.ts @@ -1,6 +1,8 @@ import { sync as resolveSync } from 'resolve'; +import * as fs from 'fs'; import * as path from 'path'; import { argv } from './option'; +import { fileURLToPath, pathToFileURL } from 'url'; export interface ResolveOptions { /** Directory to start resolution from. Defaults to `process.cwd()`. */ @@ -138,3 +140,25 @@ export function resolveCwd(moduleName: string, cwdOrOptions?: string | ResolveOp } return _tryResolve(moduleName, options); } + +let importMetaResolve: ((specifier: string, parent: string) => string) | undefined; +/** + * Resolve a module relative to `cwd` (default `process.cwd()`) using `import-meta-resolve`. + * Returns the resolved module if it exists, or null otherwise. + */ +export async function resolveModern(moduleName: string, cwd?: string): Promise { + importMetaResolve ||= (await import('import-meta-resolve')).resolve; + // import-meta-resolve needs a parent path ending in a filename in the correct directory + // (doesn't matter whether the file exists) + const parent = pathToFileURL(path.join(cwd || process.cwd(), 'package.json')).href; + + try { + const resolved = fileURLToPath(importMetaResolve(moduleName, parent)); + if (fs.existsSync(resolved)) { + return resolved; + } + } catch { + // ignore + } + return null; +} diff --git a/scripts/jest.config.js b/scripts/jest.config.js index c6eb0229..17ba0cb3 100644 --- a/scripts/jest.config.js +++ b/scripts/jest.config.js @@ -5,7 +5,7 @@ const path = require('path'); * Jest config for packages within the just monorepo * @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { +const config = { roots: ['/src'], testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).[jt]s'], @@ -16,7 +16,10 @@ module.exports = { { tsconfig: path.join(process.cwd(), 'tsconfig.json'), packageJson: path.join(process.cwd(), 'package.json'), + // badly-named option means skip type checking (it's done as part of the build) + isolatedModules: true, }, ], }, }; +module.exports = config; diff --git a/tsconfig.json b/tsconfig.json index d311d550..9f8553d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "es2019", - "module": "commonjs", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "Node16", "pretty": true, "declaration": true, "declarationMap": true, diff --git a/yarn.lock b/yarn.lock index f9b5dbd2..e37a4074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6737,6 +6737,11 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +import-meta-resolve@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz#0b1195915689f60ab00f830af0f15cc841e8919e" + integrity sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -11121,10 +11126,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@~4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@~4.7.0: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typescript@~5.0.4: version "5.0.4" From 11199b5cf70f67264d48e1943043a4edab140dcc Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 12:35:14 -0700 Subject: [PATCH 03/12] work around crash --- packages/just-task/src/resolve.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/just-task/src/resolve.ts b/packages/just-task/src/resolve.ts index 4a1bde6f..cd404fc7 100644 --- a/packages/just-task/src/resolve.ts +++ b/packages/just-task/src/resolve.ts @@ -147,6 +147,13 @@ let importMetaResolve: ((specifier: string, parent: string) => string) | undefin * Returns the resolved module if it exists, or null otherwise. */ export async function resolveModern(moduleName: string, cwd?: string): Promise { + if (process.env.JEST_WORKER_ID) { + // For some reason, the dynamic import below causes a segfault in Jest. + // This issue appears to be specific to Jest (doesn't repro when building `example-lib` in the + // just repo), so just fall back to the old resolver in that case. + return null; + } + importMetaResolve ||= (await import('import-meta-resolve')).resolve; // import-meta-resolve needs a parent path ending in a filename in the correct directory // (doesn't matter whether the file exists) From d12bb8124d8621c69493eba5bd473b717468882c Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 12:37:31 -0700 Subject: [PATCH 04/12] api --- .prettierignore | 1 + packages/just-scripts/etc/just-scripts.api.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index c2c0d9b1..2d09ad4d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,4 @@ temp LICENSE CHANGELOG.* SECURITY.md +*.api.md \ No newline at end of file diff --git a/packages/just-scripts/etc/just-scripts.api.md b/packages/just-scripts/etc/just-scripts.api.md index 01f0d4c0..d2603b4a 100644 --- a/packages/just-scripts/etc/just-scripts.api.md +++ b/packages/just-scripts/etc/just-scripts.api.md @@ -445,7 +445,7 @@ export interface TsLoaderOptions { } // @public (undocumented) -export const tsOverlay: (overlayOptions?: TsOverlayOptions | undefined) => Partial; +export const tsOverlay: (overlayOptions?: TsOverlayOptions) => Partial; // @public (undocumented) export interface TsOverlayOptions { From 0d445940e5fc5006c70f49732a23d0fccad774eb Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 12:40:22 -0700 Subject: [PATCH 05/12] remove import-meta-resolve --- packages/just-task/package.json | 1 - .../just-task/src/__tests__/resolve.spec.ts | 16 ++++++++++ packages/just-task/src/config.ts | 4 +-- packages/just-task/src/resolve.ts | 31 ------------------- yarn.lock | 5 --- 5 files changed, 18 insertions(+), 39 deletions(-) diff --git a/packages/just-task/package.json b/packages/just-task/package.json index 5e4ed6ff..43bf99bc 100644 --- a/packages/just-task/package.json +++ b/packages/just-task/package.json @@ -34,7 +34,6 @@ "chalk": "^4.0.0", "chokidar": "^3.5.2", "fs-extra": "^11.0.0", - "import-meta-resolve": "^4.0.0", "just-scripts-utils": "^2.0.1", "just-task-logger": ">=1.2.1 <2.0.0", "resolve": "^1.19.0", diff --git a/packages/just-task/src/__tests__/resolve.spec.ts b/packages/just-task/src/__tests__/resolve.spec.ts index f5ab716c..7ca2db34 100644 --- a/packages/just-task/src/__tests__/resolve.spec.ts +++ b/packages/just-task/src/__tests__/resolve.spec.ts @@ -257,4 +257,20 @@ describe('resolveConfigFile', () => { }); expect(resolvedConfig).toContain('defaultConfigArgument.ts'); }); + + it('resolves .mjs config', async () => { + mockfs({ + 'just.config.mjs': 'localConfig', + }); + const resolvedConfig = await config.resolveConfigFile({}); + expect(resolvedConfig).toContain('just.config.mjs'); + }); + + it('resolves .cjs config', async () => { + mockfs({ + 'just.config.cjs': 'localConfig', + }); + const resolvedConfig = await config.resolveConfigFile({}); + expect(resolvedConfig).toContain('just.config.cjs'); + }); }); diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index 64eba6d0..6917581a 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { argv } from './option'; -import { resolve, resolveModern } from './resolve'; +import { resolve } from './resolve'; import { mark, logger } from 'just-task-logger'; import { enableTypeScript } from './enableTypeScript'; import { TaskFunction } from './interfaces'; @@ -21,7 +21,7 @@ export async function resolveConfigFile(args: { config?: string; defaultConfig?: ].filter((p): p is string => !!p); for (const entry of paths) { - const resolved = resolve(entry) || (await resolveModern(entry)); + const resolved = resolve(entry); if (resolved) { return resolved; } diff --git a/packages/just-task/src/resolve.ts b/packages/just-task/src/resolve.ts index cd404fc7..b0ee73e7 100644 --- a/packages/just-task/src/resolve.ts +++ b/packages/just-task/src/resolve.ts @@ -1,8 +1,6 @@ import { sync as resolveSync } from 'resolve'; -import * as fs from 'fs'; import * as path from 'path'; import { argv } from './option'; -import { fileURLToPath, pathToFileURL } from 'url'; export interface ResolveOptions { /** Directory to start resolution from. Defaults to `process.cwd()`. */ @@ -140,32 +138,3 @@ export function resolveCwd(moduleName: string, cwdOrOptions?: string | ResolveOp } return _tryResolve(moduleName, options); } - -let importMetaResolve: ((specifier: string, parent: string) => string) | undefined; -/** - * Resolve a module relative to `cwd` (default `process.cwd()`) using `import-meta-resolve`. - * Returns the resolved module if it exists, or null otherwise. - */ -export async function resolveModern(moduleName: string, cwd?: string): Promise { - if (process.env.JEST_WORKER_ID) { - // For some reason, the dynamic import below causes a segfault in Jest. - // This issue appears to be specific to Jest (doesn't repro when building `example-lib` in the - // just repo), so just fall back to the old resolver in that case. - return null; - } - - importMetaResolve ||= (await import('import-meta-resolve')).resolve; - // import-meta-resolve needs a parent path ending in a filename in the correct directory - // (doesn't matter whether the file exists) - const parent = pathToFileURL(path.join(cwd || process.cwd(), 'package.json')).href; - - try { - const resolved = fileURLToPath(importMetaResolve(moduleName, parent)); - if (fs.existsSync(resolved)) { - return resolved; - } - } catch { - // ignore - } - return null; -} diff --git a/yarn.lock b/yarn.lock index e37a4074..8c10544b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6737,11 +6737,6 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -import-meta-resolve@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz#0b1195915689f60ab00f830af0f15cc841e8919e" - integrity sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA== - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" From a1d5a47cc670edf2827798752e8c6ffed6bc0967 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 13:09:53 -0700 Subject: [PATCH 06/12] remove unnecessary check --- packages/just-task/src/config.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index 6917581a..fcc216f6 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -1,6 +1,4 @@ -import * as fs from 'fs'; import * as path from 'path'; - import { argv } from './option'; import { resolve } from './resolve'; import { mark, logger } from 'just-task-logger'; @@ -44,11 +42,6 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo process.exit(1); } - if (!fs.existsSync(configFile)) { - logger.error(`The specified config file "${configFile}" doesn't exist or couldn't be resolved.`); - process.exit(1); - } - const ext = path.extname(configFile).toLowerCase(); if (ext === '.ts') { From cbf0a24049e2422ee2789744c25a0ac94093a120 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 16:43:30 -0700 Subject: [PATCH 07/12] update messages --- packages/just-task/src/config.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index fcc216f6..3c6bcf6f 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -35,10 +35,7 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo const configFile = await resolveConfigFile(args); if (!configFile) { - logger.error( - `Config file not found! Please create a file called "just.config.js" ` + - `in the root of the package next to "package.json".`, - ); + logger.error('Config file not found. Please create a file "just.config.js" at the package root.'); process.exit(1); } @@ -65,12 +62,12 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo logger.error(`Invalid configuration file: ${configFile}`); if (importError) { logger.error( - `Initially got this error trying to import() the file: ${ + `Initially got this error trying to import() the file:\n${ (importError as Error)?.stack || (importError as Error)?.message || importError }`, ); logger.error( - `Then tried to require() the file and got this error: ${(e as Error).stack || (e as Error).message || e}`, + `Then tried to require() the file and got this error:\n${(e as Error).stack || (e as Error).message || e}`, ); } else { logger.error((e as Error).stack || (e as Error).message || e); From 132a2683b458e55ad9fa29082c473540a625f1b8 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 21:01:48 -0700 Subject: [PATCH 08/12] detect esm or cjs --- ...-0c7dafd4-b632-47f2-bacf-b22077ce3c51.json | 11 +++ ...-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json | 2 +- packages/example-lib-esm-ts/README.md | 3 + packages/example-lib-esm-ts/just.config.cts | 7 ++ packages/example-lib-esm-ts/package.json | 14 ++++ packages/example-lib-esm-ts/src/index.ts | 1 + .../example-lib-esm-ts/tasks/customTask.ts | 7 ++ packages/example-lib-esm-ts/tsconfig.json | 15 ++++ packages/example-lib-esm/README.md | 3 + packages/example-lib-esm/just.config.js | 7 ++ packages/example-lib-esm/package.json | 14 ++++ packages/example-lib-esm/src/index.ts | 1 + packages/example-lib-esm/tasks/customTask.js | 7 ++ packages/example-lib-esm/tsconfig.json | 15 ++++ packages/example-lib/package.json | 7 -- .../just-scripts/src/tasks/nodeExecTask.ts | 24 +++--- .../src/typescript/getTsNodeEnv.ts | 12 ++- packages/just-task/src/config.ts | 59 +++++++------- packages/just-task/src/enableTypeScript.ts | 77 ++++++++++--------- yarn.lock | 74 ++++++++++++++++++ 20 files changed, 271 insertions(+), 89 deletions(-) create mode 100644 change/change-0c7dafd4-b632-47f2-bacf-b22077ce3c51.json create mode 100644 packages/example-lib-esm-ts/README.md create mode 100644 packages/example-lib-esm-ts/just.config.cts create mode 100644 packages/example-lib-esm-ts/package.json create mode 100644 packages/example-lib-esm-ts/src/index.ts create mode 100644 packages/example-lib-esm-ts/tasks/customTask.ts create mode 100644 packages/example-lib-esm-ts/tsconfig.json create mode 100644 packages/example-lib-esm/README.md create mode 100644 packages/example-lib-esm/just.config.js create mode 100644 packages/example-lib-esm/package.json create mode 100644 packages/example-lib-esm/src/index.ts create mode 100644 packages/example-lib-esm/tasks/customTask.js create mode 100644 packages/example-lib-esm/tsconfig.json diff --git a/change/change-0c7dafd4-b632-47f2-bacf-b22077ce3c51.json b/change/change-0c7dafd4-b632-47f2-bacf-b22077ce3c51.json new file mode 100644 index 00000000..928c2f17 --- /dev/null +++ b/change/change-0c7dafd4-b632-47f2-bacf-b22077ce3c51.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "`nodeExecTask`: add `enableTypeScript: esm` setting to run the task in `ts-node` with ESM support", + "packageName": "just-scripts", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json index 2b7c4e18..9c67c9d8 100644 --- a/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json +++ b/change/change-9eb2d5fb-3a3f-4714-8fd7-5bac2166d20d.json @@ -2,7 +2,7 @@ "changes": [ { "type": "minor", - "comment": "Support ESM configs and explicit .cjs and .mjs config extensions", + "comment": "Partial support for ESM packages: load `just.config.js` as ESM if package has `type: \"module\"`, and support explicit `.cjs`, `.mjs`, and `.cts` extensions. (`.ts` configs in packages with `type: \"module\"` are not supported due to limitations with `ts-node` `register()`.)", "packageName": "just-task", "email": "elcraig@microsoft.com", "dependentChangeType": "patch" diff --git a/packages/example-lib-esm-ts/README.md b/packages/example-lib-esm-ts/README.md new file mode 100644 index 00000000..4942bcb3 --- /dev/null +++ b/packages/example-lib-esm-ts/README.md @@ -0,0 +1,3 @@ +This package has `"type": "module"` in its `package.json`. If the just config was `just.config.ts`, this would be implicitly considered as ESM, and loading TS files as ESM isn't supported with `ts-node` `register()` (it must be configured with a `--loader` option when the Node process is created). So the package has to use `just.config.cts` instead. + +The just config also defines a `nodeExecTask` which must be handled as ESM. The custom task file can be a normal `.ts` which is treated as ESM because `enableTypeScript: 'esm'` is set, and the task is run in a separate Node process where the `--loader` can be used. diff --git a/packages/example-lib-esm-ts/just.config.cts b/packages/example-lib-esm-ts/just.config.cts new file mode 100644 index 00000000..c60b428a --- /dev/null +++ b/packages/example-lib-esm-ts/just.config.cts @@ -0,0 +1,7 @@ +import { nodeExecTask, tscTask, task, parallel } from 'just-scripts'; + +task('typescript', tscTask({})); + +task('customNodeTask', nodeExecTask({ enableTypeScript: 'esm', args: ['./tasks/customTask.ts'] })); + +task('build', parallel('customNodeTask', 'typescript')); diff --git a/packages/example-lib-esm-ts/package.json b/packages/example-lib-esm-ts/package.json new file mode 100644 index 00000000..7d763baa --- /dev/null +++ b/packages/example-lib-esm-ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "example-lib-esm-ts", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "just-scripts build" + }, + "license": "MIT", + "devDependencies": { + "just-scripts": ">=2.2.3 <3.0.0", + "ts-node": "^10.0.0" + } +} diff --git a/packages/example-lib-esm-ts/src/index.ts b/packages/example-lib-esm-ts/src/index.ts new file mode 100644 index 00000000..ef74d34b --- /dev/null +++ b/packages/example-lib-esm-ts/src/index.ts @@ -0,0 +1 @@ +const a = 5; diff --git a/packages/example-lib-esm-ts/tasks/customTask.ts b/packages/example-lib-esm-ts/tasks/customTask.ts new file mode 100644 index 00000000..39731f3b --- /dev/null +++ b/packages/example-lib-esm-ts/tasks/customTask.ts @@ -0,0 +1,7 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const packageJson = fs.readFileSync(path.resolve(dirname, '../package.json'), 'utf-8'); diff --git a/packages/example-lib-esm-ts/tsconfig.json b/packages/example-lib-esm-ts/tsconfig.json new file mode 100644 index 00000000..6ce85e60 --- /dev/null +++ b/packages/example-lib-esm-ts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [] + }, + "include": ["src"] +} diff --git a/packages/example-lib-esm/README.md b/packages/example-lib-esm/README.md new file mode 100644 index 00000000..5c7889bb --- /dev/null +++ b/packages/example-lib-esm/README.md @@ -0,0 +1,3 @@ +This package has `"type": "module"` in its `package.json`, so its `just.config.js` must be loaded as ESM. + +The just config also defines a `nodeExecTask` which must be handled as ESM. diff --git a/packages/example-lib-esm/just.config.js b/packages/example-lib-esm/just.config.js new file mode 100644 index 00000000..d174515a --- /dev/null +++ b/packages/example-lib-esm/just.config.js @@ -0,0 +1,7 @@ +import { nodeExecTask, tscTask, task, parallel } from 'just-scripts'; + +task('typescript', tscTask({})); + +task('customNodeTask', nodeExecTask({ args: ['./tasks/customTask.js'] })); + +task('build', parallel('customNodeTask', 'typescript')); diff --git a/packages/example-lib-esm/package.json b/packages/example-lib-esm/package.json new file mode 100644 index 00000000..13f72fe4 --- /dev/null +++ b/packages/example-lib-esm/package.json @@ -0,0 +1,14 @@ +{ + "name": "example-lib-esm", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "just-scripts build" + }, + "license": "MIT", + "devDependencies": { + "just-scripts": ">=2.2.3 <3.0.0", + "ts-node": "^9.1.1" + } +} diff --git a/packages/example-lib-esm/src/index.ts b/packages/example-lib-esm/src/index.ts new file mode 100644 index 00000000..ef74d34b --- /dev/null +++ b/packages/example-lib-esm/src/index.ts @@ -0,0 +1 @@ +const a = 5; diff --git a/packages/example-lib-esm/tasks/customTask.js b/packages/example-lib-esm/tasks/customTask.js new file mode 100644 index 00000000..39731f3b --- /dev/null +++ b/packages/example-lib-esm/tasks/customTask.js @@ -0,0 +1,7 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const packageJson = fs.readFileSync(path.resolve(dirname, '../package.json'), 'utf-8'); diff --git a/packages/example-lib-esm/tsconfig.json b/packages/example-lib-esm/tsconfig.json new file mode 100644 index 00000000..6ce85e60 --- /dev/null +++ b/packages/example-lib-esm/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [] + }, + "include": ["src"] +} diff --git a/packages/example-lib/package.json b/packages/example-lib/package.json index 4cd11e88..a8865d7c 100644 --- a/packages/example-lib/package.json +++ b/packages/example-lib/package.json @@ -2,17 +2,10 @@ "name": "example-lib", "private": true, "version": "1.0.0", - "description": "", - "main": "index.js", "scripts": { "build": "just-scripts build" }, - "keywords": [], - "author": "", "license": "MIT", - "engines": { - "node": ">=14" - }, "devDependencies": { "just-scripts": ">=2.2.3 <3.0.0", "ts-node": "^9.1.1" diff --git a/packages/just-scripts/src/tasks/nodeExecTask.ts b/packages/just-scripts/src/tasks/nodeExecTask.ts index 21fbe3ac..0f3a44d2 100644 --- a/packages/just-scripts/src/tasks/nodeExecTask.ts +++ b/packages/just-scripts/src/tasks/nodeExecTask.ts @@ -17,9 +17,10 @@ export interface NodeExecTaskOptions { env?: NodeJS.ProcessEnv; /** - * Should this nodeExec task be using something like ts-node to execute the binary + * Whether this nodeExec task should use ts-node to execute the binary. + * If set to `esm`, it will use `ts-node/esm` instead of `ts-node/register`. */ - enableTypeScript?: boolean; + enableTypeScript?: boolean | 'esm'; /** * The tsconfig file to pass to ts-node for Typescript config @@ -40,21 +41,22 @@ export interface NodeExecTaskOptions { export function nodeExecTask(options: NodeExecTaskOptions): TaskFunction { return function () { const { spawnOptions, enableTypeScript, tsconfig, transpileOnly } = options; + const args = [...(options.args || [])]; + const env = { ...options.env }; - const tsNodeRegister = resolveCwd('ts-node/register'); + const esm = enableTypeScript === 'esm'; + const tsNodeHelper = resolveCwd(esm ? 'ts-node/esm.mjs' : 'ts-node/register'); const nodeExecPath = process.execPath; - if (enableTypeScript && tsNodeRegister) { - options.args = options.args || []; - options.args.unshift(tsNodeRegister); - options.args.unshift('-r'); + if (enableTypeScript && tsNodeHelper) { + args.unshift(esm ? '--loader' : '-r', tsNodeHelper); + Object.assign(env, getTsNodeEnv(tsconfig, transpileOnly, esm)); - options.env = { ...options.env, ...getTsNodeEnv(tsconfig, transpileOnly) }; - logger.info('Executing [TS]: ' + [nodeExecPath, ...(options.args || [])].join(' ')); + logger.info('Executing [TS]: ' + [nodeExecPath, ...args].join(' ')); } else { - logger.info('Executing: ' + [nodeExecPath, ...(options.args || [])].join(' ')); + logger.info('Executing: ' + [nodeExecPath, ...args].join(' ')); } - return spawn(nodeExecPath, options.args, { stdio: 'inherit', env: options.env, ...spawnOptions }); + return spawn(nodeExecPath, args, { stdio: 'inherit', env, ...spawnOptions }); }; } diff --git a/packages/just-scripts/src/typescript/getTsNodeEnv.ts b/packages/just-scripts/src/typescript/getTsNodeEnv.ts index 4861dfa0..daee4671 100644 --- a/packages/just-scripts/src/typescript/getTsNodeEnv.ts +++ b/packages/just-scripts/src/typescript/getTsNodeEnv.ts @@ -1,13 +1,21 @@ import { logger } from 'just-task'; -export function getTsNodeEnv(tsconfig?: string, transpileOnly?: boolean): { [key: string]: string | undefined } { +export function getTsNodeEnv( + tsconfig?: string, + transpileOnly?: boolean, + esm?: boolean, +): { [key: string]: string | undefined } { const env: { [key: string]: string | undefined } = {}; if (tsconfig) { logger.info(`[TS] Using ${tsconfig}`); env.TS_NODE_PROJECT = tsconfig; } else { - const compilerOptions = JSON.stringify({ module: 'commonjs', target: 'es2017', moduleResolution: 'node' }); + const compilerOptions = JSON.stringify({ + target: 'es2017', + moduleResolution: esm ? 'NodeNext' : 'node', + module: esm ? 'NodeNext' : 'commonjs', + }); logger.info(`[TS] Using these compilerOptions: ${compilerOptions}`); env.TS_NODE_COMPILER_OPTIONS = compilerOptions; } diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index 3c6bcf6f..be1c8c93 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -4,6 +4,7 @@ import { resolve } from './resolve'; import { mark, logger } from 'just-task-logger'; import { enableTypeScript } from './enableTypeScript'; import { TaskFunction } from './interfaces'; +import { readPackageJson } from 'just-scripts-utils'; export async function resolveConfigFile(args: { config?: string; defaultConfig?: string }): Promise { // Check for the old config paths/extensions first @@ -11,15 +12,12 @@ export async function resolveConfigFile(args: { config?: string; defaultConfig?: args.config, './just.config.js', './just-task.js', - './just.config.ts', - // Add .cjs and .mjs (.cts and .mts don't seem to work with ts-node) - './just.config.cjs', - './just.config.mjs', + ...['.ts', '.cts', '.mts', '.cjs', '.mjs'].map(ext => `./just.config${ext}`), args.defaultConfig, - ].filter((p): p is string => !!p); + ]; for (const entry of paths) { - const resolved = resolve(entry); + const resolved = entry && resolve(entry); if (resolved) { return resolved; } @@ -39,40 +37,37 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo process.exit(1); } + const packageJson = readPackageJson(process.cwd()); + const packageIsESM = packageJson?.type === 'module'; + const ext = path.extname(configFile).toLowerCase(); - if (ext === '.ts') { - enableTypeScript({ transpileOnly: true }); + if (ext === '.mts' || (packageIsESM && ext === '.ts')) { + // We can't support these with ts-node because we're calling register() rather than creating + // a child process with the custom --loader, and it appears that it's not possible to change + // the loader (needed for ESM) after the fact. The same limitation applies for tsx. + // https://typestrong.org/ts-node/docs/imports/#native-ecmascript-modules + logger.error('Just does not currently support ESM TypeScript configuration files. Please use a .cts file.'); + process.exit(1); + } + + if (/^\.[cm]?ts$/.test(ext)) { + const tsSuccess = enableTypeScript({ transpileOnly: true, configFile }); + if (!tsSuccess) { + process.exit(1); // enableTypeScript will log the error + } } let configModule = undefined; - let importError: unknown; try { - try { - if (ext !== '.cjs') { - // Rather than trying to infer the correct type in all cases, try import first. - configModule = await import(configFile); - } - } catch (e) { - importError = e; - } - // Fall back to require - configModule ||= require(configFile); - } catch (e) { - logger.error(`Invalid configuration file: ${configFile}`); - if (importError) { - logger.error( - `Initially got this error trying to import() the file:\n${ - (importError as Error)?.stack || (importError as Error)?.message || importError - }`, - ); - logger.error( - `Then tried to require() the file and got this error:\n${(e as Error).stack || (e as Error).message || e}`, - ); + if (ext.startsWith('.m') || (packageIsESM && !ext.startsWith('.c'))) { + configModule = await import(configFile); } else { - logger.error((e as Error).stack || (e as Error).message || e); + configModule = require(configFile); } - + } catch (e) { + logger.error(`Error loading configuration file: ${configFile}`); + logger.error((e as Error).stack || (e as Error).message || e); process.exit(1); } diff --git a/packages/just-task/src/enableTypeScript.ts b/packages/just-task/src/enableTypeScript.ts index 9f33e5cb..d723b2f6 100644 --- a/packages/just-task/src/enableTypeScript.ts +++ b/packages/just-task/src/enableTypeScript.ts @@ -1,46 +1,51 @@ import * as fse from 'fs-extra'; +import * as path from 'path'; import { resolve } from './resolve'; import { logger } from 'just-task-logger'; -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function enableTypeScript({ transpileOnly = true }): void { +/** + * Enable typescript support with ts-node. + * Returns true if successful. + */ +export function enableTypeScript(params: { transpileOnly?: boolean; configFile?: string }): boolean { + const { transpileOnly = true, configFile = '' } = params; const tsNodeModule = resolve('ts-node'); - if (tsNodeModule) { - // Use module/moduleResolution "node16" if supported for broadest compatibility - let supportsNode16Setting = false; - const typescriptPackageJson = resolve('typescript/package.json'); - if (typescriptPackageJson) { - const typescriptVersion = fse.readJsonSync(typescriptPackageJson).version as string; - const [major, minor] = typescriptVersion.split('.').map(Number); - supportsNode16Setting = major > 4 || (major === 4 && minor >= 7); - } - - const tsNode = require(tsNodeModule); - tsNode.register({ - transpileOnly, - skipProject: true, - compilerOptions: { - target: 'es2017', - module: supportsNode16Setting ? 'node16' : 'commonjs', - strict: false, - skipLibCheck: true, - skipDefaultLibCheck: true, - moduleResolution: supportsNode16Setting ? 'node16' : 'node', - allowJs: true, - esModuleInterop: true, - }, - files: ['just.config.ts'], - }); - } else { - logger.error(`In order to use TypeScript with just.config.ts, you need to install "ts-node" module: - - npm install -D ts-node - -or + if (!tsNodeModule) { + logger.error(`In order to use TypeScript with just.config.ts, you need to install the "ts-node" package.`); + return false; + } - yarn add -D ts-node + // Use module/moduleResolution "node16" if supported for broadest compatibility + let supportsNode16Setting = false; + const typescriptPackageJson = resolve('typescript/package.json'); + if (typescriptPackageJson) { + const typescriptVersion = fse.readJsonSync(typescriptPackageJson).version as string; + const [major, minor] = typescriptVersion.split('.').map(Number); + supportsNode16Setting = major > 4 || (major === 4 && minor >= 7); + } -`); + const tsNode = require(tsNodeModule) as typeof import('ts-node'); + const tsNodeMajor = Number(String(tsNode.VERSION || '0').split('.')[0]); + const ext = path.extname(configFile); + if (tsNodeMajor < 10 && ext !== '.ts') { + logger.error(`ts-node >= 10 is required for ${ext} extension support.`); + return false; } + + tsNode.register({ + transpileOnly, + skipProject: true, + compilerOptions: { + target: 'es2017', + module: supportsNode16Setting ? 'node16' : 'commonjs', + strict: false, + skipLibCheck: true, + skipDefaultLibCheck: true, + moduleResolution: supportsNode16Setting ? 'node16' : 'node', + allowJs: true, + esModuleInterop: true, + }, + }); + return true; } diff --git a/yarn.lock b/yarn.lock index 8c10544b..96d6fdd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1008,6 +1008,13 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz#8ff71d51053cd5ee4981e5a501d80a536244f7fd" integrity sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1276,6 +1283,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -1291,6 +1303,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": version "0.3.19" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" @@ -1444,6 +1464,26 @@ dependencies: defer-to-connect "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/argparse@1.0.38": version "1.0.38" resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9" @@ -2365,11 +2405,21 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -10984,6 +11034,25 @@ ts-jest@^29.0.5: semver "^7.5.3" yargs-parser "^21.0.1" +ts-node@^10.0.0: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + ts-node@^9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -11410,6 +11479,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-to-istanbul@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" From 06ed223f03f2cb084791e224c871d7026c10ee40 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 21:38:59 -0700 Subject: [PATCH 09/12] allow all extensions and detect tsx or ts-node --- ...-3e7ef03c-6140-4b48-b824-f7303ca53d62.json | 11 ++++++++++ packages/just-task/src/config.ts | 22 +++++++++++-------- packages/just-task/src/enableTypeScript.ts | 16 ++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 change/change-3e7ef03c-6140-4b48-b824-f7303ca53d62.json diff --git a/change/change-3e7ef03c-6140-4b48-b824-f7303ca53d62.json b/change/change-3e7ef03c-6140-4b48-b824-f7303ca53d62.json new file mode 100644 index 00000000..18f56b40 --- /dev/null +++ b/change/change-3e7ef03c-6140-4b48-b824-f7303ca53d62.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "patch", + "comment": "Detect if already running in ts-node or tsx and skip call to ts-node register", + "packageName": "just-task", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/packages/just-task/src/config.ts b/packages/just-task/src/config.ts index be1c8c93..eedabcbe 100644 --- a/packages/just-task/src/config.ts +++ b/packages/just-task/src/config.ts @@ -42,15 +42,6 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo const ext = path.extname(configFile).toLowerCase(); - if (ext === '.mts' || (packageIsESM && ext === '.ts')) { - // We can't support these with ts-node because we're calling register() rather than creating - // a child process with the custom --loader, and it appears that it's not possible to change - // the loader (needed for ESM) after the fact. The same limitation applies for tsx. - // https://typestrong.org/ts-node/docs/imports/#native-ecmascript-modules - logger.error('Just does not currently support ESM TypeScript configuration files. Please use a .cts file.'); - process.exit(1); - } - if (/^\.[cm]?ts$/.test(ext)) { const tsSuccess = enableTypeScript({ transpileOnly: true, configFile }); if (!tsSuccess) { @@ -68,6 +59,19 @@ export async function readConfig(): Promise<{ [key: string]: TaskFunction } | vo } catch (e) { logger.error(`Error loading configuration file: ${configFile}`); logger.error((e as Error).stack || (e as Error).message || e); + + if (ext === '.mts' || (packageIsESM && ext === '.ts')) { + // We can't directly support these with ts-node because we're calling register() rather than + // creating a child process with the custom --loader. + // (Related: https://typestrong.org/ts-node/docs/imports/) + const binPath = path.relative(process.cwd(), process.argv[1]); + logger.error(''); + logger.error( + 'Just does not directly support ESM TypeScript configuration files. You must either ' + + `use a .cts file, or call the just binary (${binPath}) via ts-node or tsx.`, + ); + } + process.exit(1); } diff --git a/packages/just-task/src/enableTypeScript.ts b/packages/just-task/src/enableTypeScript.ts index d723b2f6..da3b9697 100644 --- a/packages/just-task/src/enableTypeScript.ts +++ b/packages/just-task/src/enableTypeScript.ts @@ -9,6 +9,22 @@ import { logger } from 'just-task-logger'; */ export function enableTypeScript(params: { transpileOnly?: boolean; configFile?: string }): boolean { const { transpileOnly = true, configFile = '' } = params; + + // Try to determine if the user is already running with a known transpiler. + // ts-node makes this easy by setting process.env.TS_NODE. + // tsx doesn't set a variable, so check a few places it might show up. + const contextVals = [ + ...process.argv, + ...process.execArgv, + process.env._, + process.env.npm_lifecycle_event, + process.env.npm_config_argv, + ]; + if (process.env.TS_NODE || contextVals.some(val => /[^.]tsx\b/.test(val || ''))) { + // It appears the user ran the just CLI with tsx or ts-node, so allow this. + return true; + } + const tsNodeModule = resolve('ts-node'); if (!tsNodeModule) { From c4763e24ed22cc3dcea7b8a7886a1ece4c745dbf Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 21:49:30 -0700 Subject: [PATCH 10/12] add ts-node-esm wrapped example lib --- packages/example-lib-esm-ts/just.config.cts | 9 ++++++++- packages/example-lib-esm-tsnode/README.md | 1 + packages/example-lib-esm-tsnode/just.config.ts | 14 ++++++++++++++ packages/example-lib-esm-tsnode/package.json | 14 ++++++++++++++ packages/example-lib-esm-tsnode/src/index.ts | 1 + .../example-lib-esm-tsnode/tasks/customTask.ts | 7 +++++++ packages/example-lib-esm-tsnode/tsconfig.json | 15 +++++++++++++++ packages/example-lib/just.config.ts | 9 ++++++++- .../just-scripts/src/typescript/getTsNodeEnv.ts | 1 + 9 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/example-lib-esm-tsnode/README.md create mode 100644 packages/example-lib-esm-tsnode/just.config.ts create mode 100644 packages/example-lib-esm-tsnode/package.json create mode 100644 packages/example-lib-esm-tsnode/src/index.ts create mode 100644 packages/example-lib-esm-tsnode/tasks/customTask.ts create mode 100644 packages/example-lib-esm-tsnode/tsconfig.json diff --git a/packages/example-lib-esm-ts/just.config.cts b/packages/example-lib-esm-ts/just.config.cts index c60b428a..10bbd7dc 100644 --- a/packages/example-lib-esm-ts/just.config.cts +++ b/packages/example-lib-esm-ts/just.config.cts @@ -2,6 +2,13 @@ import { nodeExecTask, tscTask, task, parallel } from 'just-scripts'; task('typescript', tscTask({})); -task('customNodeTask', nodeExecTask({ enableTypeScript: 'esm', args: ['./tasks/customTask.ts'] })); +task( + 'customNodeTask', + nodeExecTask({ + enableTypeScript: 'esm', + transpileOnly: true, + args: ['./tasks/customTask.ts'], + }), +); task('build', parallel('customNodeTask', 'typescript')); diff --git a/packages/example-lib-esm-tsnode/README.md b/packages/example-lib-esm-tsnode/README.md new file mode 100644 index 00000000..1c57c67b --- /dev/null +++ b/packages/example-lib-esm-tsnode/README.md @@ -0,0 +1 @@ +This package has `"type": "module"` in its `package.json`. The config is a `.ts` which will be treated as ESM, and Just can't handle that directly (see [`example-lib-esm-ts` readme](../example-lib-esm-ts/README.md)). So the package's `"build"` script wraps the `just-scripts` binary with the transpiler: `ts-node-esm node_modules/.bin/just-scripts build`. diff --git a/packages/example-lib-esm-tsnode/just.config.ts b/packages/example-lib-esm-tsnode/just.config.ts new file mode 100644 index 00000000..10bbd7dc --- /dev/null +++ b/packages/example-lib-esm-tsnode/just.config.ts @@ -0,0 +1,14 @@ +import { nodeExecTask, tscTask, task, parallel } from 'just-scripts'; + +task('typescript', tscTask({})); + +task( + 'customNodeTask', + nodeExecTask({ + enableTypeScript: 'esm', + transpileOnly: true, + args: ['./tasks/customTask.ts'], + }), +); + +task('build', parallel('customNodeTask', 'typescript')); diff --git a/packages/example-lib-esm-tsnode/package.json b/packages/example-lib-esm-tsnode/package.json new file mode 100644 index 00000000..3966ddd0 --- /dev/null +++ b/packages/example-lib-esm-tsnode/package.json @@ -0,0 +1,14 @@ +{ + "name": "example-lib-esm-tsnode", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "ts-node-esm node_modules/.bin/just-scripts build" + }, + "license": "MIT", + "devDependencies": { + "just-scripts": ">=2.2.3 <3.0.0", + "ts-node": "^10.0.0" + } +} diff --git a/packages/example-lib-esm-tsnode/src/index.ts b/packages/example-lib-esm-tsnode/src/index.ts new file mode 100644 index 00000000..ef74d34b --- /dev/null +++ b/packages/example-lib-esm-tsnode/src/index.ts @@ -0,0 +1 @@ +const a = 5; diff --git a/packages/example-lib-esm-tsnode/tasks/customTask.ts b/packages/example-lib-esm-tsnode/tasks/customTask.ts new file mode 100644 index 00000000..39731f3b --- /dev/null +++ b/packages/example-lib-esm-tsnode/tasks/customTask.ts @@ -0,0 +1,7 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const packageJson = fs.readFileSync(path.resolve(dirname, '../package.json'), 'utf-8'); diff --git a/packages/example-lib-esm-tsnode/tsconfig.json b/packages/example-lib-esm-tsnode/tsconfig.json new file mode 100644 index 00000000..6ce85e60 --- /dev/null +++ b/packages/example-lib-esm-tsnode/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [] + }, + "include": ["src"] +} diff --git a/packages/example-lib/just.config.ts b/packages/example-lib/just.config.ts index 02b8363f..54fda037 100644 --- a/packages/example-lib/just.config.ts +++ b/packages/example-lib/just.config.ts @@ -3,7 +3,14 @@ import { nodeExecTask, tscTask, task, parallel, watch } from 'just-scripts'; task('typescript', tscTask({})); task('typescript:watch', tscTask({ watch: true })); -task('customNodeTask', nodeExecTask({ enableTypeScript: true, args: ['./tasks/customTask.ts'] })); +task( + 'customNodeTask', + nodeExecTask({ + enableTypeScript: true, + transpileOnly: true, + args: ['./tasks/customTask.ts'], + }), +); task('build', parallel('customNodeTask', 'typescript')); task('watch', parallel('typescript:watch')); diff --git a/packages/just-scripts/src/typescript/getTsNodeEnv.ts b/packages/just-scripts/src/typescript/getTsNodeEnv.ts index daee4671..1e567300 100644 --- a/packages/just-scripts/src/typescript/getTsNodeEnv.ts +++ b/packages/just-scripts/src/typescript/getTsNodeEnv.ts @@ -15,6 +15,7 @@ export function getTsNodeEnv( target: 'es2017', moduleResolution: esm ? 'NodeNext' : 'node', module: esm ? 'NodeNext' : 'commonjs', + skipLibCheck: true, }); logger.info(`[TS] Using these compilerOptions: ${compilerOptions}`); env.TS_NODE_COMPILER_OPTIONS = compilerOptions; From 4f87ce1c685d4db9b394753637b4e624d5fa4ddc Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 14 Mar 2024 21:50:25 -0700 Subject: [PATCH 11/12] api --- packages/just-scripts/etc/just-scripts.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/just-scripts/etc/just-scripts.api.md b/packages/just-scripts/etc/just-scripts.api.md index d2603b4a..5e228d9a 100644 --- a/packages/just-scripts/etc/just-scripts.api.md +++ b/packages/just-scripts/etc/just-scripts.api.md @@ -293,7 +293,7 @@ export function nodeExecTask(options: NodeExecTaskOptions): TaskFunction; // @public (undocumented) export interface NodeExecTaskOptions { args?: string[]; - enableTypeScript?: boolean; + enableTypeScript?: boolean | 'esm'; env?: NodeJS.ProcessEnv; spawnOptions?: SpawnOptions; transpileOnly?: boolean; From 9d38a18f5872399a928348c261bb52d4fb6af9a7 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Fri, 15 Mar 2024 10:41:19 -0700 Subject: [PATCH 12/12] syncpack --- package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package.json b/package.json index fcc25db8..87a3a9c8 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,18 @@ "dependencyTypes": [ "dev", "prod" + ], + "versionGroups": [ + { + "label": "ts-node (testing with multiple versions)", + "dependencies": [ + "ts-node" + ], + "packages": [ + "**" + ], + "isIgnored": true + } ] }, "workspaces": {