Skip to content
File

Blob: src/node/internal/internal_process.ts

typescript452 lines
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 
10import { validateObject } from 'node-internal:validators';
11import {
12 ERR_INVALID_ARG_TYPE,
13 ERR_INVALID_ARG_VALUE,
14 NodeError,
15} from 'node-internal:internal_errors';
16import {
17 type EmitWarningOptions,
18 type ErrorWithDetail,
19 default as processImpl,
20} from 'node-internal:process';
21import type publicProcessType from 'node-internal:public_process';
22import type legacyProcessType from 'node-internal:legacy_process';
23 
24export const platform = processImpl.platform;
25 
26// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
27export 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.
35function 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 
96function 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 
140export 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
203export 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.
233export 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 
347export function emitWarning(
348 warning: string | Error,
349 ctor?: ErrorConstructor
350): void;
351export function emitWarning(
352 warning: string | Error,
353 type?: string,
354 ctor?: ErrorConstructor
355): void;
356export function emitWarning(
357 warning: string | Error,
358 type?: string,
359 code?: string,
360 ctor?: ErrorConstructor
361): void;
362export 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.
446export let _process: typeof legacyProcessType | typeof publicProcessType;
447export function _setEventsProcess(
448 process: typeof legacyProcessType | typeof publicProcessType
449): void {
450 _process = process;
451}