File
Blob: src/node/internal/internal_process.ts
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | // Our implementation of process.nextTick is just queueMicrotask. The timing |
| 5 | // of when the queue is drained is different, as is the error handling so this |
| 6 | // is only an approximation of process.nextTick semantics. Hopefully it's good |
| 7 | // enough because we really do not want to implement Node.js' idea of a nextTick |
| 8 | // queue. |
| 9 | |
| 10 | import { validateObject } from 'node-internal:validators'; |
| 11 | import { |
| 12 | ERR_INVALID_ARG_TYPE, |
| 13 | ERR_INVALID_ARG_VALUE, |
| 14 | NodeError, |
| 15 | } from 'node-internal:internal_errors'; |
| 16 | import { |
| 17 | type EmitWarningOptions, |
| 18 | type ErrorWithDetail, |
| 19 | default as processImpl, |
| 20 | } from 'node-internal:process'; |
| 21 | import type publicProcessType from 'node-internal:public_process'; |
| 22 | import type legacyProcessType from 'node-internal:legacy_process'; |
| 23 | |
| 24 | export const platform = processImpl.platform; |
| 25 | |
| 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type |
| 27 | export function nextTick(cb: Function, ...args: unknown[]): void { |
| 28 | queueMicrotask(() => { |
| 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call |
| 30 | cb(...args); |
| 31 | }); |
| 32 | } |
| 33 | |
| 34 | // Decide if a value can round-trip to JSON without losing any information. |
| 35 | function isJsonSerializable( |
| 36 | value: unknown, |
| 37 | seen: Set<unknown> = new Set() |
| 38 | ): boolean { |
| 39 | switch (typeof value) { |
| 40 | case 'boolean': |
| 41 | case 'number': |
| 42 | case 'string': |
| 43 | return true; |
| 44 | |
| 45 | case 'object': { |
| 46 | if (value === null) { |
| 47 | return true; |
| 48 | } |
| 49 | |
| 50 | if (seen.has(value)) { |
| 51 | // Don't allow cycles or aliases. (Non-cyclic aliases technically could be OK, but a |
| 52 | // round trip to JSON would lose the fact that they are aliases.) |
| 53 | return false; |
| 54 | } |
| 55 | seen.add(value); |
| 56 | |
| 57 | // TODO(revisit): While any object that implements the toJSON function is |
| 58 | // generally expected to be JSON serializable, when working with jsrpc |
| 59 | // targets, the `toJSON` property ends up being Proxied and appears to be |
| 60 | // a legit property on some object types when it really shouldn't be, causing |
| 61 | // issues with certain types of bindings. Fun! |
| 62 | // Commenting this out instead of removing it because it would be great if |
| 63 | // we could find a way to support this reliably. |
| 64 | // |
| 65 | // if (typeof value.toJSON === 'function') { |
| 66 | // // This type is explicitly designed to be JSON-serialized so we'll accept it. |
| 67 | // return true; |
| 68 | // } |
| 69 | |
| 70 | // We only consider objects to be serializable if they are plain objects or plain arrays. |
| 71 | // Technically, JSON can serialize any subclass of Object (as well as objects with null |
| 72 | // prototypes), but the round trip would lose information about the original type. Hence, |
| 73 | // we assume that any env var containing such things is not intended to be appear as JSON |
| 74 | // in process.env. For example, we wouldn't want a KV namespace to show up in process.env as |
| 75 | // "{}" -- this would be weird. |
| 76 | switch (Object.getPrototypeOf(value)) { |
| 77 | case Object.prototype: |
| 78 | // Note that Object.values() only returns string-keyed values, not symbol-keyed. |
| 79 | return Object.values(value).every((prop) => |
| 80 | isJsonSerializable(prop, seen) |
| 81 | ); |
| 82 | case Array.prototype: |
| 83 | return (value as Array<unknown>).every((elem: unknown) => |
| 84 | isJsonSerializable(elem, seen) |
| 85 | ); |
| 86 | default: |
| 87 | return false; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | default: |
| 92 | return false; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | function getInitialEnv(): Record<string, string> { |
| 97 | const env: Record<string, string> = {}; |
| 98 | for (const [key, value] of Object.entries(processImpl.getEnvObject())) { |
| 99 | // Workers environment variables can have a variety of types, but process.env vars are |
| 100 | // strictly strings. We want to convert our workers env into process.env, but allowing |
| 101 | // process.env to contain non-strings would probably break Node apps. |
| 102 | // |
| 103 | // As a compromise, we say: |
| 104 | // - Workers env vars that are plain strings are unchanged in process.env. |
| 105 | // - Workers env vars that can be represented as JSON will be JSON-stringified in process.env. |
| 106 | // - Anything else will be omitted. |
| 107 | // |
| 108 | // Note that you might argue that, at the config layer, it's possible to differentiate between |
| 109 | // plain strings and JSON values that evaluated to strings. Wouldn't it be nice if we could |
| 110 | // check which way the binding was originally configured in order to decide whether to |
| 111 | // represent it plain or as JSON here. However, there is no way to tell just by looking at |
| 112 | // the `env` object inside a Worker whether a particular var was originally configured as |
| 113 | // plain text, or as JSON that evaluated to a string. Either way, you just get a string. And |
| 114 | // indeed, the Workers Runtime itself does not necessarily know this. In many cases it does |
| 115 | // know, but in general the abstraction the Runtime intends to provide is that `env` is just |
| 116 | // a JavaScript object, and how exactly the contents were originally represented is not |
| 117 | // intended to be conveyed. This is important because, for example, we could extend dynamic |
| 118 | // dispatch bindings in the future such that the caller can specify `env` directly, and in |
| 119 | // that case the caller would simply specify a JS object, without JSON or any other |
| 120 | // serialization involved. In this case, there would be no way to know if a string var was |
| 121 | // "supposed to be" raw text vs. JSON. |
| 122 | // |
| 123 | // So, we have to do the best we can given just what we know -- the JavaScript object that is |
| 124 | // `env`. |
| 125 | // |
| 126 | // As a consolation, this is consistent with how variables are defined in wrangler.toml: you |
| 127 | // do not explicitly specify whether a variable is text or JSON. If you define a variable with |
| 128 | // a simple string value, it gets configured as a text var. If you specify an object, then it's |
| 129 | // configured as JSON. |
| 130 | |
| 131 | if (typeof value === 'string') { |
| 132 | env[key] = value; |
| 133 | } else if (isJsonSerializable(value)) { |
| 134 | env[key] = JSON.stringify(value); |
| 135 | } |
| 136 | } |
| 137 | return env; |
| 138 | } |
| 139 | |
| 140 | export const env = new Proxy(getInitialEnv(), { |
| 141 | // Per Node.js rules. process.env values must be coerced to strings. |
| 142 | // When defined using defineProperty, the property descriptor must be writable, |
| 143 | // configurable, and enumerable using just a falsy check. Getters and setters |
| 144 | // are not permitted. |
| 145 | set(obj: object, prop: PropertyKey, value: unknown): boolean { |
| 146 | if (typeof prop === 'symbol' || typeof value === 'symbol') |
| 147 | throw new TypeError(`Cannot convert a symbol value to a string`); |
| 148 | return Reflect.set(obj, prop, `${value}`); |
| 149 | }, |
| 150 | defineProperty( |
| 151 | obj: object, |
| 152 | prop: PropertyKey, |
| 153 | descriptor: PropertyDescriptor |
| 154 | ): boolean { |
| 155 | validateObject(descriptor, 'descriptor'); |
| 156 | if (Reflect.has(descriptor, 'get') || Reflect.has(descriptor, 'set')) { |
| 157 | throw new ERR_INVALID_ARG_VALUE( |
| 158 | 'descriptor', |
| 159 | descriptor, |
| 160 | 'process.env value must not have getter/setter' |
| 161 | ); |
| 162 | } |
| 163 | if (!descriptor.configurable) { |
| 164 | throw new ERR_INVALID_ARG_VALUE( |
| 165 | 'descriptor.configurable', |
| 166 | descriptor, |
| 167 | 'process.env value must be configurable' |
| 168 | ); |
| 169 | } |
| 170 | if (!descriptor.enumerable) { |
| 171 | throw new ERR_INVALID_ARG_VALUE( |
| 172 | 'descriptor.enumerable', |
| 173 | descriptor, |
| 174 | 'process.env value must be enumerable' |
| 175 | ); |
| 176 | } |
| 177 | if (!descriptor.writable) { |
| 178 | throw new ERR_INVALID_ARG_VALUE( |
| 179 | 'descriptor.writable', |
| 180 | descriptor, |
| 181 | 'process.env value must be writable' |
| 182 | ); |
| 183 | } |
| 184 | if (Reflect.has(descriptor, 'value')) { |
| 185 | if (typeof prop === 'symbol' || typeof descriptor.value === 'symbol') |
| 186 | throw new TypeError(`Cannot convert a symbol value to a string`); |
| 187 | Reflect.set(descriptor, 'value', `${descriptor.value}`); |
| 188 | } else { |
| 189 | throw new ERR_INVALID_ARG_VALUE( |
| 190 | 'descriptor.value', |
| 191 | descriptor, |
| 192 | 'process.env value must be specified explicitly' |
| 193 | ); |
| 194 | } |
| 195 | if (typeof prop === 'symbol') |
| 196 | throw new TypeError(`Cannot convert a symbol value to a string`); |
| 197 | return Reflect.defineProperty(obj, prop, descriptor); |
| 198 | }, |
| 199 | }); |
| 200 | |
| 201 | // The following features does not include deprecated or experimental flags mentioned in |
| 202 | // https://nodejs.org/docs/latest/api/process.html |
| 203 | export const features = Object.freeze({ |
| 204 | // A boolean value that is true if the current Node.js build is caching builtin modules. |
| 205 | cached_builtins: true, |
| 206 | // A boolean value that is true if the current Node.js build is a debug build. |
| 207 | debug: false, |
| 208 | // A boolean value that is true if the current Node.js build includes the inspector. |
| 209 | inspector: false, |
| 210 | // A boolean value that is true if the current Node.js build supports IPv6. |
| 211 | ipv6: true, |
| 212 | // A boolean value that is true if the current Node.js build uses BoringSSL instead of OpenSSL. |
| 213 | openssl_is_boringssl: true, |
| 214 | // A boolean value that is true if the current Node.js build supports loading ECMAScript modules using require(). |
| 215 | // TODO(soon): Update this when we support ESM modules through require(). |
| 216 | require_module: false, |
| 217 | // A boolean value that is true if the current Node.js build includes support for TLS. |
| 218 | tls: true, |
| 219 | // A boolean value that is true if the current Node.js build supports TLS ALPN. |
| 220 | tls_alpn: true, |
| 221 | // A boolean value that is true if the current Node.js build supports TLS OCSP. |
| 222 | tls_ocsp: true, |
| 223 | // A boolean value that is true if the current Node.js build supports TLS SNI. |
| 224 | tls_sni: true, |
| 225 | // A string indicating the level of TypeScript support. 'strip' means type stripping only. |
| 226 | typescript: false as string | boolean, |
| 227 | // A boolean value that is true if the current Node.js build includes libuv. |
| 228 | uv: true, |
| 229 | }); |
| 230 | |
| 231 | // process.config is a frozen object containing the build configuration options used to compile |
| 232 | // the current Node.js executable. We stub it with representative values for compatibility. |
| 233 | export const config = Object.freeze({ |
| 234 | target_defaults: Object.freeze({ |
| 235 | default_configuration: 'Release', |
| 236 | }), |
| 237 | variables: Object.freeze({ |
| 238 | asan: 0, |
| 239 | clang: 1, |
| 240 | control_flow_guard: false, |
| 241 | coverage: false, |
| 242 | dcheck_always_on: 0, |
| 243 | debug_nghttp2: false, |
| 244 | debug_node: false, |
| 245 | enable_lto: false, |
| 246 | enable_pgo_generate: false, |
| 247 | enable_pgo_use: false, |
| 248 | error_on_warn: false, |
| 249 | force_dynamic_crt: 0, |
| 250 | gas_version: '', |
| 251 | host_arch: 'x64', |
| 252 | icu_data_in: '', |
| 253 | icu_endianness: 'l', |
| 254 | icu_gyp_path: '', |
| 255 | icu_path: '', |
| 256 | icu_small: false, |
| 257 | icu_ver_major: '', |
| 258 | is_debug: 0, |
| 259 | libdir: 'lib', |
| 260 | llvm_version: '', |
| 261 | napi_build_version: '9', |
| 262 | node_byteorder: 'little', |
| 263 | node_debug_lib: false, |
| 264 | node_enable_d8: false, |
| 265 | node_enable_v8_vtunejit: false, |
| 266 | node_enable_v8windbg: false, |
| 267 | node_fipsinstall: false, |
| 268 | node_install_corepack: false, |
| 269 | node_install_npm: false, |
| 270 | node_module_version: 127, |
| 271 | node_no_browser_globals: false, |
| 272 | node_prefix: '/usr/local', |
| 273 | node_quic: false, |
| 274 | node_release_urlbase: '', |
| 275 | node_section_ordering_info: '', |
| 276 | node_shared: false, |
| 277 | node_shared_ada: false, |
| 278 | node_shared_brotli: false, |
| 279 | node_shared_cares: false, |
| 280 | node_shared_gtest: false, |
| 281 | node_shared_hdr_histogram: false, |
| 282 | node_shared_http_parser: false, |
| 283 | node_shared_libuv: false, |
| 284 | node_shared_merve: false, |
| 285 | node_shared_nbytes: false, |
| 286 | node_shared_nghttp2: false, |
| 287 | node_shared_nghttp3: false, |
| 288 | node_shared_ngtcp2: false, |
| 289 | node_shared_openssl: false, |
| 290 | node_shared_simdjson: false, |
| 291 | node_shared_simdutf: false, |
| 292 | node_shared_sqlite: false, |
| 293 | node_shared_uvwasi: false, |
| 294 | node_shared_zlib: false, |
| 295 | node_shared_zstd: false, |
| 296 | node_tag: '', |
| 297 | node_target_type: 'executable', |
| 298 | node_use_amaro: false, |
| 299 | node_use_bundled_v8: true, |
| 300 | node_use_node_code_cache: false, |
| 301 | node_use_node_snapshot: false, |
| 302 | node_use_openssl: true, |
| 303 | node_use_sqlite: false, |
| 304 | node_use_v8_platform: false, |
| 305 | node_with_ltcg: false, |
| 306 | node_without_node_options: true, |
| 307 | node_write_snapshot_as_array_literals: false, |
| 308 | openssl_is_fips: false, |
| 309 | openssl_quic: false, |
| 310 | ossfuzz: false, |
| 311 | shlib_suffix: 'so', |
| 312 | single_executable_application: false, |
| 313 | suppress_all_error_on_warn: false, |
| 314 | target_arch: 'x64', |
| 315 | ubsan: 0, |
| 316 | use_ccache_win: 0, |
| 317 | use_prefix_to_find_headers: false, |
| 318 | v8_enable_31bit_smis_on_64bit_arch: 0, |
| 319 | v8_enable_extensible_ro_snapshot: 0, |
| 320 | v8_enable_external_code_space: 0, |
| 321 | v8_enable_gdbjit: 0, |
| 322 | v8_enable_hugepage: 0, |
| 323 | v8_enable_i18n_support: 1, |
| 324 | v8_enable_inspector: 0, |
| 325 | v8_enable_javascript_promise_hooks: 0, |
| 326 | v8_enable_lite_mode: 0, |
| 327 | v8_enable_maglev: 1, |
| 328 | v8_enable_object_print: 0, |
| 329 | v8_enable_pointer_compression: 1, |
| 330 | v8_enable_pointer_compression_shared_cage: 1, |
| 331 | v8_enable_sandbox: 0, |
| 332 | v8_enable_shared_ro_heap: 1, |
| 333 | v8_enable_short_builtin_calls: 1, |
| 334 | v8_enable_v8_checks: 0, |
| 335 | v8_enable_wasm_simd256_revec: 0, |
| 336 | v8_enable_webassembly: 1, |
| 337 | v8_no_strict_aliasing: 1, |
| 338 | v8_optimized_debug: 1, |
| 339 | v8_promise_internal_field_count: 1, |
| 340 | v8_random_seed: 0, |
| 341 | v8_trace_maps: 0, |
| 342 | v8_use_siphash: 1, |
| 343 | want_separate_host_toolset: 0, |
| 344 | }), |
| 345 | }); |
| 346 | |
| 347 | export function emitWarning( |
| 348 | warning: string | Error, |
| 349 | ctor?: ErrorConstructor |
| 350 | ): void; |
| 351 | export function emitWarning( |
| 352 | warning: string | Error, |
| 353 | type?: string, |
| 354 | ctor?: ErrorConstructor |
| 355 | ): void; |
| 356 | export function emitWarning( |
| 357 | warning: string | Error, |
| 358 | type?: string, |
| 359 | code?: string, |
| 360 | ctor?: ErrorConstructor |
| 361 | ): void; |
| 362 | export function emitWarning( |
| 363 | warning: string | Error, |
| 364 | options?: ErrorConstructor | string | EmitWarningOptions, |
| 365 | codeOrCtor?: ErrorConstructor | string, |
| 366 | maybeCtor?: ErrorConstructor |
| 367 | ): void { |
| 368 | let err: Error; |
| 369 | let name = 'Warning'; |
| 370 | let detail: string | undefined; |
| 371 | let code: string | undefined; |
| 372 | let ctor: ErrorConstructor | undefined; |
| 373 | |
| 374 | // Handle different overloads |
| 375 | if (typeof options === 'object' && !Array.isArray(options)) { |
| 376 | // emitWarning(warning, options) |
| 377 | if (options.type) name = options.type; |
| 378 | if (options.code) code = options.code; |
| 379 | if (options.detail) detail = options.detail; |
| 380 | ctor = options.ctor; |
| 381 | } else if (typeof options === 'string') { |
| 382 | // emitWarning(warning, type, ...) |
| 383 | name = options; |
| 384 | if (typeof codeOrCtor === 'string') { |
| 385 | // emitWarning(warning, type, code, ctor) |
| 386 | code = codeOrCtor; |
| 387 | if (typeof maybeCtor === 'function') { |
| 388 | ctor = maybeCtor; |
| 389 | } else if ((maybeCtor as unknown) !== undefined) { |
| 390 | throw new ERR_INVALID_ARG_TYPE('ctor', 'function', maybeCtor); |
| 391 | } |
| 392 | } else if (typeof codeOrCtor === 'function') { |
| 393 | // emitWarning(warning, type, ctor) |
| 394 | ctor = codeOrCtor; |
| 395 | } else if ((codeOrCtor as unknown) !== undefined) { |
| 396 | throw new ERR_INVALID_ARG_TYPE('ctor', 'function', codeOrCtor); |
| 397 | } |
| 398 | } else if (typeof options === 'function') { |
| 399 | // emitWarning(warning, ctor) |
| 400 | ctor = options; |
| 401 | } else if (options !== undefined) { |
| 402 | throw new ERR_INVALID_ARG_TYPE('options', 'object', options); |
| 403 | } |
| 404 | |
| 405 | // Convert string warning to Error |
| 406 | if (typeof warning === 'string') { |
| 407 | // Use the provided constructor if available, otherwise use Error |
| 408 | const ErrorConstructor = ctor || Error; |
| 409 | err = new ErrorConstructor(warning); |
| 410 | err.name = name; |
| 411 | } else if (warning instanceof Error) { |
| 412 | err = warning; |
| 413 | // Override name if provided |
| 414 | if (name && name !== 'Warning') { |
| 415 | err.name = name; |
| 416 | } |
| 417 | } else { |
| 418 | throw new ERR_INVALID_ARG_TYPE('warning', 'string or Error', warning); |
| 419 | } |
| 420 | |
| 421 | // Add code if provided |
| 422 | if (code) { |
| 423 | (err as NodeError).code = code; |
| 424 | } |
| 425 | |
| 426 | // Add detail if provided |
| 427 | if (detail && typeof detail === 'string') { |
| 428 | (err as ErrorWithDetail).detail = detail; |
| 429 | } |
| 430 | |
| 431 | // Capture stack trace using the provided constructor or emitWarning itself |
| 432 | // This excludes the constructor (and frames above it) from the stack trace |
| 433 | Error.captureStackTrace(err, ctor || emitWarning); |
| 434 | |
| 435 | // Emit the warning event on the process object |
| 436 | // Use nextTick to ensure the warning is emitted asynchronously |
| 437 | queueMicrotask(() => { |
| 438 | (_process as typeof publicProcessType).emit('warning', err); |
| 439 | }); |
| 440 | } |
| 441 | |
| 442 | // Events has a cycle with process, so to resolve that we lazily bind |
| 443 | // this _process for events usage only. All other internal importers should |
| 444 | // import 'node:process' directly rather, as _eventsProcess is only guaranteed |
| 445 | // to be available when that has been imported. |
| 446 | export let _process: typeof legacyProcessType | typeof publicProcessType; |
| 447 | export function _setEventsProcess( |
| 448 | process: typeof legacyProcessType | typeof publicProcessType |
| 449 | ): void { |
| 450 | _process = process; |
| 451 | } |