From 763fde3914e92d849d7176b96134425a1dcfac25 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Fri, 9 Oct 2026 12:35:35 +0300 Subject: [PATCH 1/6] update cli to support typescript 6 and 7 --- CHANGELOG.md | 8 ++++++++ commands/pack.js | 20 ++++++++++++++------ config/webpack.config.js | 34 +++++++++++++++++++++++++++++----- npm-shrinkwrap.json | 2 +- package.json | 2 +- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d041228..86ab5820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## unreleased + +### pack + +* Fixed `enact pack` to skip TypeScript type checking, with a warning, when the installed `typescript` package does not expose `createProgram` (TypeScript 7). Apps on TypeScript 5.x and 6.x still run `fork-ts-checker-webpack-plugin`. +* Fixed `enact pack` to exit with code 1 when the build fails, including when `chalk` has not loaded yet. +* Updated the optional `typescript` peer dependency to `^5.4.0 || ^6.0.0 || ^7.0.2`. + ## 7.3.4 (August 21, 2026) ### bootstrap diff --git a/commands/pack.js b/commands/pack.js index cb937de1..18cb5fca 100755 --- a/commands/pack.js +++ b/commands/pack.js @@ -325,17 +325,25 @@ function cli (args) { if (opts.help) displayHelp(); process.chdir(app.context); - import('chalk').then(({default: _chalk}) => { - chalk = _chalk; - import('strip-ansi').then(({default: _stripAnsi}) => { + import('chalk') + .then(({default: _chalk}) => { + chalk = _chalk; + return import('strip-ansi'); + }) + .then(({default: _stripAnsi}) => { stripAnsi = _stripAnsi; - api(opts).catch(err => { + return api(opts); + }) + .catch(err => { + if (chalk) { printErrorDetails(err, () => { process.exit(1); }); - }); + } else { + console.error(err); + process.exit(1); + } }); - }); } module.exports = {api, cli}; diff --git a/config/webpack.config.js b/config/webpack.config.js index 54be65f8..e651ff71 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -27,7 +27,6 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); -const resolve = require('resolve'); const TerserPlugin = require('terser-webpack-plugin'); const {DefinePlugin, EnvironmentPlugin} = require('webpack'); const { @@ -39,6 +38,27 @@ const { } = require('@enact/dev-utils'); const createEnvironmentHash = require('./createEnvironmentHash'); +// fork-ts-checker needs the classic TypeScript compiler API. TypeScript 7's +// package entry only exports a version, so resolution via `main` fails and the +// checker cannot run. Return null to skip it instead of failing the build. +function resolveLegacyTypeScript (context) { + let typescriptPath; + try { + typescriptPath = require.resolve('typescript', {paths: [context]}); + } catch (e) { + return null; + } + + try { + const typescript = require(typescriptPath); + if (typeof typescript.createProgram === 'function') return typescriptPath; + } catch (e) { + return null; + } + + return null; +} + // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. module.exports = function ( @@ -60,6 +80,12 @@ module.exports = function ( // Check if TypeScript is setup const useTypeScript = fs.existsSync('tsconfig.json'); + const typeScriptPath = useTypeScript ? resolveLegacyTypeScript(app.context) : null; + if (useTypeScript && !typeScriptPath) { + console.warn( + 'Skipping TypeScript type checking during the build. The installed "typescript" package does not provide the compiler API required by fork-ts-checker.' + ); + } // Check if Tailwind config exists const useTailwind = fs.existsSync(path.join(app.context, 'tailwind.config.js')); @@ -626,13 +652,11 @@ module.exports = function ( // and parses any to copy over any webOS meta assets at build time. new WebOSMetaPlugin({htmlPlugin: HtmlWebpackPlugin}), // TypeScript type checking - useTypeScript && + typeScriptPath && new ForkTsCheckerWebpackPlugin({ async: !isEnvProduction, typescript: { - typescriptPath: resolve.sync('typescript', { - basedir: 'node_modules' - }), + typescriptPath: typeScriptPath, configOverwrite: { compilerOptions: { sourceMap: shouldUseSourceMap, diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 644369fc..4f8b132a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -94,7 +94,7 @@ "fsevents": "^2.3.3" }, "peerDependencies": { - "typescript": "^5.4.0" + "typescript": "^5.4.0 || ^6.0.0 || ^7.0.2" }, "peerDependenciesMeta": { "typescript": { diff --git a/package.json b/package.json index 2f738c2c..318f3b9c 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "fsevents": "^2.3.3" }, "peerDependencies": { - "typescript": "^5.4.0" + "typescript": "^5.4.0 || ^6.0.0 || ^7.0.2" }, "peerDependenciesMeta": { "typescript": { From bc3baedeabfab07795d3419abff8d55df756b907 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Fri, 9 Oct 2026 13:55:53 +0300 Subject: [PATCH 2/6] added typecheck for typescript 7 --- CHANGELOG.md | 2 +- config/TypeScriptCheckPlugin.js | 132 ++++++++++++++++++++++++++++++++ config/webpack.config.js | 35 ++++++--- 3 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 config/TypeScriptCheckPlugin.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ab5820..9f58be16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### pack -* Fixed `enact pack` to skip TypeScript type checking, with a warning, when the installed `typescript` package does not expose `createProgram` (TypeScript 7). Apps on TypeScript 5.x and 6.x still run `fork-ts-checker-webpack-plugin`. +* Fixed `enact pack` and `enact serve` to type check TypeScript 7 apps by running the `tsc` binary, since TypeScript 7 does not expose the compiler API required by `fork-ts-checker-webpack-plugin`. Apps on TypeScript 5.x and 6.x still use `fork-ts-checker-webpack-plugin`. * Fixed `enact pack` to exit with code 1 when the build fails, including when `chalk` has not loaded yet. * Updated the optional `typescript` peer dependency to `^5.4.0 || ^6.0.0 || ^7.0.2`. diff --git a/config/TypeScriptCheckPlugin.js b/config/TypeScriptCheckPlugin.js new file mode 100644 index 00000000..b6cb1596 --- /dev/null +++ b/config/TypeScriptCheckPlugin.js @@ -0,0 +1,132 @@ +'use strict'; +const {spawn} = require('child_process'); +const path = require('path'); +const {WebpackError} = require('webpack'); + +const pluginName = 'EnactTypeScriptCheckPlugin'; + +// Diagnostics are not reported for test and setup files, matching the fork-ts-checker issue filters. +const excludeFile = /(^|[\\/])__tests__[\\/]|\.(spec|test)\.[^\\/]+$|[\\/]src[\\/]setup(Proxy|Tests)\.[^\\/]+$/; + +// Parses `tsc --pretty false` output. Each diagnostic starts with `file(line,col): error TSxxxx: message` +// (or `error TSxxxx: message` for global ones) and any following indented lines elaborate on it. +function parseDiagnostics (output) { + const diagnostics = []; + output.split(/\r?\n/).forEach(line => { + const located = /^(.+)\((\d+),(\d+)\): (error|warning) (TS\d+): (.*)$/.exec(line); + const unlocated = !located && /^(error|warning) (TS\d+): (.*)$/.exec(line); + if (located) { + const [, file, row, column, category, code, text] = located; + diagnostics.push({file, row, column, category, code, text}); + } else if (unlocated) { + const [, category, code, text] = unlocated; + diagnostics.push({category, code, text}); + } else if (line.trim() && diagnostics.length) { + diagnostics[diagnostics.length - 1].text += '\n' + line; + } + }); + return diagnostics; +} + +function formatDiagnostic ({file, row, column, code, text}) { + return (file ? `${file}(${row},${column})\n` : '') + `${code}: ${text}`; +} + +// Type checks the app with the `tsc` binary for TypeScript versions that do not expose the compiler API +// required by fork-ts-checker (TypeScript 7+). `tsc` runs alongside each webpack compilation and its +// diagnostics are added to the compilation once it finishes. +class TypeScriptCheckPlugin { + constructor ({tscPath, context, compileOnError = false}) { + this.tscPath = tscPath; + this.context = context; + this.compileOnError = compileOnError; + this.child = null; + this.check = null; + } + + runTsc () { + this.cancel(); + const args = [ + this.tscPath, + '--project', + path.join(this.context, 'tsconfig.json'), + '--noEmit', + '--skipLibCheck', + '--incremental', + '--tsBuildInfoFile', + path.join(this.context, 'node_modules', '.cache', 'tsconfig.tsbuildinfo'), + '--pretty', + 'false' + ]; + + return new Promise(resolve => { + let output = ''; + const child = spawn(process.execPath, args, {cwd: this.context, windowsHide: true}); + this.child = child; + child.stdout.on('data', data => (output += data)); + child.stderr.on('data', data => (output += data)); + child.on('error', err => { + resolve({failure: err.message}); + }); + child.on('close', (exitCode, signal) => { + if (this.child === child) this.child = null; + if (signal) { + resolve({cancelled: true}); + } else if (exitCode === 0) { + resolve({diagnostics: []}); + } else { + const diagnostics = parseDiagnostics(output); + // A non-zero exit without any parsable diagnostics means tsc itself failed + resolve( + diagnostics.length ? + {diagnostics} : + {failure: output.trim() || `tsc exited with code ${exitCode}`} + ); + } + }); + }); + } + + cancel () { + if (this.child) { + this.child.kill(); + this.child = null; + } + } + + apply (compiler) { + const start = () => { + this.check = this.runTsc(); + }; + compiler.hooks.run.tap(pluginName, start); + compiler.hooks.watchRun.tap(pluginName, start); + compiler.hooks.watchClose.tap(pluginName, () => this.cancel()); + compiler.hooks.shutdown.tap(pluginName, () => this.cancel()); + + compiler.hooks.afterCompile.tapPromise(pluginName, compilation => { + // Child compilers inherit this hook; only report on the main compilation + if (compilation.compiler !== compiler || !this.check) return Promise.resolve(); + + return this.check.then(({cancelled, failure, diagnostics}) => { + if (cancelled) return; + if (failure) { + compilation.warnings.push(new WebpackError(`TypeScript type checking could not run:\n${failure}`)); + return; + } + diagnostics + .filter(({file}) => !file || !excludeFile.test(file)) + .forEach(diagnostic => { + const issue = new WebpackError(formatDiagnostic(diagnostic)); + issue.hideStack = true; + if (diagnostic.category === 'error' && !this.compileOnError) { + compilation.errors.push(issue); + } else { + compilation.warnings.push(issue); + } + }); + }); + }); + } +} + +module.exports = TypeScriptCheckPlugin; diff --git a/config/webpack.config.js b/config/webpack.config.js index e651ff71..0886313b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -37,11 +37,12 @@ const { WebOSMetaPlugin } = require('@enact/dev-utils'); const createEnvironmentHash = require('./createEnvironmentHash'); +const TypeScriptCheckPlugin = require('./TypeScriptCheckPlugin'); // fork-ts-checker needs the classic TypeScript compiler API. TypeScript 7's -// package entry only exports a version, so resolution via `main` fails and the -// checker cannot run. Return null to skip it instead of failing the build. -function resolveLegacyTypeScript (context) { +// package entry only exports a version, so for it the `tsc` binary is resolved +// instead and run by TypeScriptCheckPlugin. Return null when neither is available. +function resolveTypeScript (context) { let typescriptPath; try { typescriptPath = require.resolve('typescript', {paths: [context]}); @@ -51,7 +52,16 @@ function resolveLegacyTypeScript (context) { try { const typescript = require(typescriptPath); - if (typeof typescript.createProgram === 'function') return typescriptPath; + if (typeof typescript.createProgram === 'function') return {typescriptPath}; + } catch (e) { + // Fall back to the tsc binary below + } + + try { + const packagePath = require.resolve('typescript/package.json', {paths: [context]}); + const {bin} = require(packagePath); + const tscPath = typeof bin === 'string' ? bin : bin && bin.tsc; + if (tscPath) return {tscPath: path.join(path.dirname(packagePath), tscPath)}; } catch (e) { return null; } @@ -80,10 +90,10 @@ module.exports = function ( // Check if TypeScript is setup const useTypeScript = fs.existsSync('tsconfig.json'); - const typeScriptPath = useTypeScript ? resolveLegacyTypeScript(app.context) : null; - if (useTypeScript && !typeScriptPath) { + const {typescriptPath, tscPath} = (useTypeScript && resolveTypeScript(app.context)) || {}; + if (useTypeScript && !typescriptPath && !tscPath) { console.warn( - 'Skipping TypeScript type checking during the build. The installed "typescript" package does not provide the compiler API required by fork-ts-checker.' + 'Skipping TypeScript type checking during the build. Unable to resolve the "typescript" package or its tsc binary.' ); } @@ -652,11 +662,11 @@ module.exports = function ( // and parses any to copy over any webOS meta assets at build time. new WebOSMetaPlugin({htmlPlugin: HtmlWebpackPlugin}), // TypeScript type checking - typeScriptPath && + typescriptPath && new ForkTsCheckerWebpackPlugin({ async: !isEnvProduction, typescript: { - typescriptPath: typeScriptPath, + typescriptPath, configOverwrite: { compilerOptions: { sourceMap: shouldUseSourceMap, @@ -692,6 +702,13 @@ module.exports = function ( infrastructure: 'silent' } }), + // TypeScript 7+ type checking via the tsc binary + tscPath && + new TypeScriptCheckPlugin({ + tscPath, + context: app.context, + compileOnError: process.env.TSC_COMPILE_ON_ERROR === 'true' + }), !noLinting && new ESLintPlugin({ // Plugin options From 63ab97a876d040b67088551ed111c18ed0ced41d Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 14 Oct 2026 14:52:28 +0300 Subject: [PATCH 3/6] minor fix --- config/webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 26e4a73c..0886313b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -27,7 +27,6 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); -const resolve = require('resolve'); const TerserPlugin = require('terser-webpack-plugin'); const {DefinePlugin, EnvironmentPlugin} = require('webpack'); const { From 4733215532424e3072e7c6a3f588569f92e8da9a Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Mon, 19 Oct 2026 17:04:12 +0300 Subject: [PATCH 4/6] fix for duplicate error --- commands/serve.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands/serve.js b/commands/serve.js index 84619dd7..96f62488 100755 --- a/commands/serve.js +++ b/commands/serve.js @@ -210,7 +210,9 @@ function devServerConfig (host, port, protocol, publicPath, proxy, allowedHost) // we specified in the webpack config. When homepage is '.', default to serving // from the root. // remove last slash so user can land on `/test` instead of `/test/` - publicPath: publicPath.slice(0, -1) + publicPath: publicPath.slice(0, -1), + // Errors and warnings are already printed by react-dev-utils' createCompiler + stats: 'none' }, historyApiFallback: { // ensure JSON file requests correctly 404 error when not found. From 7433117c29385071cada8f34852847b02fdd0842 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Mon, 19 Oct 2026 17:33:20 +0300 Subject: [PATCH 5/6] fix for duplicate error --- config/TypeScriptCheckPlugin.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/config/TypeScriptCheckPlugin.js b/config/TypeScriptCheckPlugin.js index b6cb1596..b8aea53a 100644 --- a/config/TypeScriptCheckPlugin.js +++ b/config/TypeScriptCheckPlugin.js @@ -113,17 +113,25 @@ class TypeScriptCheckPlugin { compilation.warnings.push(new WebpackError(`TypeScript type checking could not run:\n${failure}`)); return; } + const errors = []; + const warnings = []; diagnostics .filter(({file}) => !file || !excludeFile.test(file)) .forEach(diagnostic => { - const issue = new WebpackError(formatDiagnostic(diagnostic)); - issue.hideStack = true; - if (diagnostic.category === 'error' && !this.compileOnError) { - compilation.errors.push(issue); - } else { - compilation.warnings.push(issue); - } + const isError = diagnostic.category === 'error' && !this.compileOnError; + (isError ? errors : warnings).push(formatDiagnostic(diagnostic)); }); + + // All diagnostics are reported as a single issue, since react-dev-utils only + // prints the first error of a compilation in `enact serve` + const report = (list, issues) => { + if (!list.length) return; + const issue = new WebpackError(list.join('\n\n')); + issue.hideStack = true; + issues.push(issue); + }; + report(errors, compilation.errors); + report(warnings, compilation.warnings); }); }); } From eb3a265b0c42d4127a209034af27a861caba9363 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 21 Oct 2026 14:55:52 +0300 Subject: [PATCH 6/6] code review fixes --- CHANGELOG.md | 4 ++++ commands/serve.js | 12 ++++++++++++ config/TypeScriptCheckPlugin.js | 11 +++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f58be16..974110be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * Fixed `enact pack` to exit with code 1 when the build fails, including when `chalk` has not loaded yet. * Updated the optional `typescript` peer dependency to `^5.4.0 || ^6.0.0 || ^7.0.2`. +### serve + +* Fixed `enact serve` printing compilation errors twice. + ## 7.3.4 (August 21, 2026) ### bootstrap diff --git a/commands/serve.js b/commands/serve.js index 96f62488..e9747a86 100755 --- a/commands/serve.js +++ b/commands/serve.js @@ -17,6 +17,7 @@ const path = require('path'); const minimist = require('minimist'); const clearConsole = require('react-dev-utils/clearConsole'); const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware'); +const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); const openBrowser = require('react-dev-utils/openBrowser'); const redirectServedPathMiddleware = require('react-dev-utils/redirectServedPathMiddleware'); @@ -26,6 +27,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin' const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const {optionParser: app} = require('@enact/dev-utils'); +const TypeScriptCheckPlugin = require('../config/TypeScriptCheckPlugin'); let chalk; @@ -286,6 +288,16 @@ function serve (config, host, port, open) { }); callback(); }); + // createCompiler only prints the first error of a compilation. With TypeScript 7 the type errors are + // part of the compilation, so print them as well when another error (e.g. ESLint) was shown instead. + compiler.hooks.done.tap('EnactCLI', stats => { + const typeScriptError = stats.compilation.errors.find(TypeScriptCheckPlugin.isTypeScriptError); + if (!typeScriptError) return; + + const [shown] = formatWebpackMessages(stats.toJson({all: false, warnings: true, errors: true})).errors; + const [typeScriptMessage] = formatWebpackMessages({errors: [typeScriptError.message], warnings: []}).errors; + if (shown !== typeScriptMessage) console.log(typeScriptMessage + '\n'); + }); // Load proxy config const proxySetting = app.proxy; const proxyConfig = prepareProxy(proxySetting, './public', publicPath); diff --git a/config/TypeScriptCheckPlugin.js b/config/TypeScriptCheckPlugin.js index b8aea53a..15be9f9c 100644 --- a/config/TypeScriptCheckPlugin.js +++ b/config/TypeScriptCheckPlugin.js @@ -5,8 +5,11 @@ const {WebpackError} = require('webpack'); const pluginName = 'EnactTypeScriptCheckPlugin'; +// Errors reported by this plugin, so `enact serve` can tell them apart from other compilation errors +const reportedErrors = new WeakSet(); + // Diagnostics are not reported for test and setup files, matching the fork-ts-checker issue filters. -const excludeFile = /(^|[\\/])__tests__[\\/]|\.(spec|test)\.[^\\/]+$|[\\/]src[\\/]setup(Proxy|Tests)\.[^\\/]+$/; +const excludeFile = /(^|[\\/])__tests__[\\/]|\.(spec|test)\.[^\\/]+$|(^|[\\/])src[\\/]setup(Proxy|Tests)\.[^\\/]+$/; // Parses `tsc --pretty false` output. Each diagnostic starts with `file(line,col): error TSxxxx: message` // (or `error TSxxxx: message` for global ones) and any following indented lines elaborate on it. @@ -129,12 +132,16 @@ class TypeScriptCheckPlugin { const issue = new WebpackError(list.join('\n\n')); issue.hideStack = true; issues.push(issue); + return issue; }; - report(errors, compilation.errors); + const error = report(errors, compilation.errors); + if (error) reportedErrors.add(error); report(warnings, compilation.warnings); }); }); } } +TypeScriptCheckPlugin.isTypeScriptError = error => reportedErrors.has(error); + module.exports = TypeScriptCheckPlugin;