diff --git a/.gitignore b/.gitignore index 81659fe018a0a5..d4aa8ee28d0b79 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,7 @@ tools/*/*.i.tmp /tools/clang-format/node_modules /tools/eslint/node_modules /tools/lint-md/node_modules +/tools/typescript/node_modules # === Rules for test artifacts === /*.tap diff --git a/Makefile b/Makefile index bccbac90bca748..b807af4d137401 100644 --- a/Makefile +++ b/Makefile @@ -1468,6 +1468,29 @@ lint-js-ci: tools/eslint/node_modules/eslint/bin/eslint.js jslint-ci: lint-js-ci $(warning Please use lint-js-ci instead of jslint-ci) +# TypeScript type checking +tools/typescript/node_modules/typescript/bin/tsc: tools/typescript/package.json + -cd tools/typescript && $(call available-node,$(run-npm-ci)) + +.PHONY: typecheck-build +typecheck-build: ## Install TypeScript type checker dependencies + $(info Installing TypeScript for type checking...) + cd tools/typescript && $(call available-node,$(run-npm-ci)) + +.PHONY: typecheck +typecheck: ## Type-check TypeScript files in lib/ + @if [ -f "tools/typescript/node_modules/typescript/bin/tsc" ]; then \ + $(MAKE) run-typecheck ; \ + else \ + echo 'TypeScript type checking is not available'; \ + echo "Run 'make typecheck-build'"; \ + fi + +.PHONY: run-typecheck +run-typecheck: + $(info Running TypeScript type checker...) + @$(call available-node,tools/typescript/node_modules/typescript/bin/tsc) + LINT_CPP_ADDON_DOC_FILES_GLOB = test/addons/??_*/*.cc test/addons/??_*/*.h LINT_CPP_ADDON_DOC_FILES = $(wildcard $(LINT_CPP_ADDON_DOC_FILES_GLOB)) LINT_CPP_EXCLUDE ?= @@ -1636,11 +1659,12 @@ lint: ## Run JS, C++, MD and doc linters. $(MAKE) lint-addon-docs || EXIT_STATUS=$$? ; \ $(MAKE) lint-md || EXIT_STATUS=$$? ; \ $(MAKE) lint-yaml || EXIT_STATUS=$$? ; \ + $(MAKE) typecheck || EXIT_STATUS=$$? ; \ exit $$EXIT_STATUS CONFLICT_RE=^>>>>>>> [[:xdigit:]]+|^<<<<<<< [[:alpha:]]+ # Related CI job: node-test-linter -lint-ci: lint-js-ci lint-cpp lint-py lint-md lint-addon-docs lint-yaml-build lint-yaml +lint-ci: lint-js-ci lint-cpp lint-py lint-md lint-addon-docs lint-yaml-build lint-yaml typecheck @if ! ( grep -IEqrs "$(CONFLICT_RE)" --exclude="error-message.js" --exclude="merge-conflict.json" benchmark deps doc lib src test tools ) \ && ! ( $(FIND) . -maxdepth 1 -type f | xargs grep -IEqs "$(CONFLICT_RE)" ); then \ exit 0 ; \ @@ -1661,6 +1685,7 @@ lint-clean: ## Remove linting artifacts. $(RM) .eslintcache $(RM) -r tools/eslint/node_modules $(RM) -r tools/lint-md/node_modules + $(RM) -r tools/typescript/node_modules $(RM) tools/pip/site_packages HAS_DOCKER ?= $(shell command -v docker > /dev/null 2>&1; [ $$? -eq 0 ] && echo 1 || echo 0) diff --git a/deps/amaro/lib/wasm.d.ts b/deps/amaro/lib/wasm.d.ts index 3f7ffbacc3fda3..8a3cf470d0ffc9 100644 --- a/deps/amaro/lib/wasm.d.ts +++ b/deps/amaro/lib/wasm.d.ts @@ -61,7 +61,7 @@ interface JsxConfig { -type Mode = "strip-only" | "transform"; +export type Mode = "strip-only" | "transform"; diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.ts similarity index 71% rename from lib/internal/modules/typescript.js rename to lib/internal/modules/typescript.ts index eeda58b3a10097..381463131517bc 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.ts @@ -1,5 +1,9 @@ 'use strict'; +type Options = import('../../../deps/amaro/lib/wasm').Options; +type TransformOutput = import('../../../deps/amaro/lib/wasm').TransformOutput; +type Mode = 'strip-only' | 'transform'; + const { ObjectPrototypeHasOwnProperty, } = primordials; @@ -9,11 +13,13 @@ const { validateObject, validateString, } = require('internal/validators'); -const { assertTypeScript, - emitExperimentalWarning, - getLazy, - isUnderNodeModules, - kEmptyObject } = require('internal/util'); +const { + assertTypeScript, + emitExperimentalWarning, + getLazy, + isUnderNodeModules, + kEmptyObject +} = require('internal/util'); const { ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, @@ -30,31 +36,26 @@ const { /** * The TypeScript parsing mode, either 'strip-only' or 'transform'. - * @type {function(): TypeScriptMode} */ -const getTypeScriptParsingMode = getLazy(() => - (getOptionValue('--experimental-transform-types') ? - (emitExperimentalWarning('Transform Types'), 'transform') : 'strip-only'), +const getTypeScriptParsingMode: () => Mode = getLazy(() => +(getOptionValue('--experimental-transform-types') ? + (emitExperimentalWarning('Transform Types'), 'transform') : 'strip-only'), ); /** * Load the TypeScript parser. * and returns an object with a `code` property. - * @returns {Function} The TypeScript parser function. */ -const loadTypeScriptParser = getLazy(() => { +const loadTypeScriptParser: () => (source: string, options: Options) => TransformOutput = getLazy(() => { assertTypeScript(); - const amaro = require('internal/deps/amaro/dist/index'); + const amaro: { transformSync: (source: string, options: Options) => TransformOutput } = require('internal/deps/amaro/dist/index'); return amaro.transformSync; }); /** - * - * @param {string} source the source code - * @param {object} options the options to pass to the parser - * @returns {TransformOutput} an object with a `code` property. + * Parse TypeScript source code. */ -function parseTypeScript(source, options) { +function parseTypeScript(source: string, options: Options): TransformOutput { const parse = loadTypeScriptParser(); try { return parse(source, options); @@ -82,12 +83,9 @@ function parseTypeScript(source, options) { } /** - * - * @param {Error} error the error to decorate: ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX - * @param {object} amaroError the error object from amaro - * @returns {Error} the decorated error + * Decorate an error with a code snippet from the Amaro error. */ -function decorateErrorWithSnippet(error, amaroError) { +function decorateErrorWithSnippet(error: Error, amaroError: any): Error { const errorHints = `${amaroError.filename}:${amaroError.startLine}\n${amaroError.snippet}`; error.stack = `${errorHints}\n${error.stack}`; return error; @@ -95,11 +93,8 @@ function decorateErrorWithSnippet(error, amaroError) { /** * Performs type-stripping to TypeScript source code. - * @param {string} code TypeScript code to parse. - * @param {TransformOptions} options The configuration for type stripping. - * @returns {string} The stripped TypeScript code. */ -function stripTypeScriptTypes(code, options = kEmptyObject) { +function stripTypeScriptTypes(code: string, options = kEmptyObject): string { emitExperimentalWarning('stripTypeScriptTypes'); validateString(code, 'code'); validateObject(options, 'options'); @@ -127,22 +122,11 @@ function stripTypeScriptTypes(code, options = kEmptyObject) { }); } -/** - * @typedef {'strip-only' | 'transform'} TypeScriptMode - * @typedef {object} TypeScriptOptions - * @property {TypeScriptMode} mode Mode. - * @property {boolean} sourceMap Whether to generate source maps. - * @property {string|undefined} filename Filename. - */ - /** * Processes TypeScript code by stripping types or transforming. * Handles source maps if needed. - * @param {string} code TypeScript code to process. - * @param {TypeScriptOptions} options The configuration object. - * @returns {string} The processed code. */ -function processTypeScriptCode(code, options) { +function processTypeScriptCode(code: string, options: Options): string { const { code: transformedCode, map } = parseTypeScript(code, options); if (map) { @@ -158,11 +142,8 @@ function processTypeScriptCode(code, options) { /** * Get the type enum used for compile cache. - * @param {TypeScriptMode} mode Mode of transpilation. - * @param {boolean} sourceMap Whether source maps are enabled. - * @returns {number} */ -function getCachedCodeType(mode, sourceMap) { +function getCachedCodeType(mode: Mode, sourceMap: boolean): number { if (mode === 'transform') { if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; } return kTransformedTypeScript; @@ -173,11 +154,8 @@ function getCachedCodeType(mode, sourceMap) { /** * Performs type-stripping to TypeScript source code internally. * It is used by internal loaders. - * @param {string} source TypeScript code to parse. - * @param {string} filename The filename of the source code. - * @returns {TransformOutput} The stripped TypeScript code. */ -function stripTypeScriptModuleTypes(source, filename) { +function stripTypeScriptModuleTypes(source: string, filename: string): string { assert(typeof source === 'string'); if (isUnderNodeModules(filename)) { throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename); @@ -200,7 +178,7 @@ function stripTypeScriptModuleTypes(source, filename) { return cached.transpiled; } - const options = { + const options: Options = { mode, sourceMap, filename, @@ -219,12 +197,9 @@ function stripTypeScriptModuleTypes(source, filename) { } /** - * - * @param {string} code The compiled code. - * @param {string} sourceMap The source map. - * @returns {string} The code with the source map attached. + * Add a source map to compiled code. */ -function addSourceMap(code, sourceMap) { +function addSourceMap(code: string, sourceMap: string): string { // The base64 encoding should be https://datatracker.ietf.org/doc/html/rfc4648#section-4, // not base64url https://datatracker.ietf.org/doc/html/rfc4648#section-5. See data url // spec https://tools.ietf.org/html/rfc2397#section-2. diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 7e962e7ecdabb2..8c5c3b5711a81a 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -178,19 +178,99 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { } #ifdef NODE_BUILTIN_MODULES_PATH -static std::string OnDiskFileName(const char* id) { - std::string filename = NODE_BUILTIN_MODULES_PATH; - filename += "/"; +static std::string OnDiskFileName(const char* id, + bool* is_typescript = nullptr) { + std::string base_path = NODE_BUILTIN_MODULES_PATH; + base_path += "/"; if (strncmp(id, "internal/deps", strlen("internal/deps")) == 0) { - id += strlen("internal/"); + base_path += id + strlen("internal/"); } else { - filename += "lib/"; + base_path += "lib/"; + base_path += id; } - filename += id; - filename += ".js"; - return filename; + // Try .ts file first + std::string ts_filename = base_path + ".ts"; + uv_fs_t req; + int r = uv_fs_stat(nullptr, &req, ts_filename.c_str(), nullptr); + uv_fs_req_cleanup(&req); + + if (r == 0) { + if (is_typescript != nullptr) *is_typescript = true; + return ts_filename; + } + + // Fall back to .js file + if (is_typescript != nullptr) *is_typescript = false; + return base_path + ".js"; +} + +// Strip TypeScript types by calling the JavaScript stripTypeScriptModuleTypes +// function +static MaybeLocal StripTypeScriptTypes(Local context, + const std::string& source, + const std::string& filename) { + Isolate* isolate = context->GetIsolate(); + EscapableHandleScope scope(isolate); + + // Get the current environment + Environment* env = Environment::GetCurrent(context); + if (env == nullptr) { + // Runtime not initialized yet, cannot strip types + // Return the source as-is (this shouldn't happen in practice) + return String::NewFromUtf8( + isolate, source.c_str(), NewStringType::kNormal, source.length()); + } + + // Require the typescript module + Local typescript_module; + if (!env->builtin_module_require() + ->Call(context, + Undefined(isolate), + 1, + &FIXED_ONE_BYTE_STRING(isolate, "internal/modules/typescript") + .As()) + .ToLocal(&typescript_module) || + !typescript_module->IsObject()) { + // Cannot load typescript module, return source as-is + return String::NewFromUtf8( + isolate, source.c_str(), NewStringType::kNormal, source.length()); + } + + // Get the stripTypeScriptModuleTypes function + Local typescript_obj = typescript_module.As(); + Local strip_fn; + if (!typescript_obj + ->Get(context, + FIXED_ONE_BYTE_STRING(isolate, "stripTypeScriptModuleTypes")) + .ToLocal(&strip_fn) || + !strip_fn->IsFunction()) { + // Function not found, return source as-is + return String::NewFromUtf8( + isolate, source.c_str(), NewStringType::kNormal, source.length()); + } + + // Call stripTypeScriptModuleTypes(source, filename) + Local args[2] = { + String::NewFromUtf8( + isolate, source.c_str(), NewStringType::kNormal, source.length()) + .ToLocalChecked(), + String::NewFromUtf8( + isolate, filename.c_str(), NewStringType::kNormal, filename.length()) + .ToLocalChecked()}; + + Local result; + if (!strip_fn.As() + ->Call(context, Undefined(isolate), 2, args) + .ToLocal(&result) || + !result->IsString()) { + // Stripping failed, return original source + return String::NewFromUtf8( + isolate, source.c_str(), NewStringType::kNormal, source.length()); + } + + return scope.Escape(result.As()); } #endif // NODE_BUILTIN_MODULES_PATH @@ -283,6 +363,25 @@ MaybeLocal BuiltinLoader::LookupAndCompileInternal( return {}; } +#ifdef NODE_BUILTIN_MODULES_PATH + // Check if this is a TypeScript file and strip types if needed + bool is_typescript = false; + std::string filename_for_check = OnDiskFileName(id, &is_typescript); + + if (is_typescript) { + // Convert source to std::string + node::Utf8Value utf8_source(isolate, source); + std::string source_str(*utf8_source, utf8_source.length()); + + // Strip TypeScript types + if (!StripTypeScriptTypes(context, source_str, filename_for_check) + .ToLocal(&source)) { + // If stripping fails, use the original source + // (this will likely cause a syntax error, but better than crashing) + } + } +#endif // NODE_BUILTIN_MODULES_PATH + std::string filename_s = std::string("node:") + id; Local filename = OneByteString(isolate, filename_s); ScriptOrigin origin(filename, 0, 0, true); diff --git a/tools/typescript/package-lock.json b/tools/typescript/package-lock.json new file mode 100644 index 00000000000000..8d56b08ca2f963 --- /dev/null +++ b/tools/typescript/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "node-type-checker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-type-checker", + "version": "1.0.0", + "dependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/tools/typescript/package.json b/tools/typescript/package.json new file mode 100644 index 00000000000000..0f6d31c3cf74c7 --- /dev/null +++ b/tools/typescript/package.json @@ -0,0 +1,9 @@ +{ + "name": "node-type-checker", + "version": "1.0.0", + "private": true, + "description": "TypeScript type checker for Node.js core", + "dependencies": { + "typescript": "^5.9.3" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3f5a3067ced063..be13161ec07ca4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,10 @@ { - "include": ["lib", "doc"], + "include": ["lib/**/*.ts", "doc"], "exclude": ["src", "tools", "out"], "files": [ "./typings/globals.d.ts", - "./typings/primordials.d.ts" + "./typings/primordials.d.ts", + "./deps/amaro/lib/wasm.d.ts" ], "compilerOptions": { "allowJs": true, @@ -12,6 +13,11 @@ "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "CommonJS", + "moduleResolution": "node", + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "noResolve": true, "baseUrl": ".", "paths": { "_http_agent": ["./lib/_http_agent.js"], diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 9b5a38db71a061..ceebaf3b24925e 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -66,9 +66,11 @@ interface InternalBindingMap { type InternalBindingKeys = keyof InternalBindingMap; -declare function internalBinding(binding: T): InternalBindingMap[T] - declare global { + function internalBinding(binding: T): InternalBindingMap[T]; + function require(id: string): any; + var module: { exports: any }; + var exports: any; type TypedArray = | Uint8Array | Uint8ClampedArray diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts index c5a79ba9c2957e..234820bdd65d6e 100644 --- a/typings/internalBinding/modules.d.ts +++ b/typings/internalBinding/modules.d.ts @@ -28,4 +28,11 @@ export interface ModulesBinding { enableCompileCache(path?: string): { status: number, message?: string, directory?: string } getCompileCacheDir(): string | undefined flushCompileCache(keepDeserializedCache?: boolean): void + getCompileCacheEntry(source: string, filename: string, type: number): any + saveCompileCacheEntry(filename: string, data: any): void + cachedCodeTypes: { + kStrippedTypeScript: number + kTransformedTypeScript: number + kTransformedTypeScriptWithSourceMaps: number + } }