From 52eae45dada6e7016b3fe9c29fe5e701a0e1cbf6 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 14 Sep 2023 22:34:33 +0200 Subject: [PATCH] feat: enable Node.js startup snapshot MONGOSH-1540 (#1662) --- .../build/src/compile/signable-compiler.ts | 5 +- packages/cli-repl/.depcheckrc | 4 + packages/cli-repl/src/async-repl.ts | 13 ++- packages/cli-repl/src/cli-repl.ts | 5 +- packages/cli-repl/src/mongosh-repl.ts | 51 +--------- packages/cli-repl/src/run.ts | 96 +++++++++++-------- packages/cli-repl/src/smoke-tests.ts | 4 +- .../cli-repl/src/tls-certificate-selector.ts | 19 ++-- .../src/update-notification-manager.ts | 6 +- packages/cli-repl/webpack.config.js | 7 +- packages/editor/src/editor.ts | 7 +- .../logging/src/setup-logger-and-telemetry.ts | 6 +- .../src/cli-service-provider.ts | 92 ++++++++++++------ .../shell-evaluator/src/shell-evaluator.ts | 19 +++- .../snippet-manager/src/snippet-manager.ts | 18 +++- 15 files changed, 206 insertions(+), 146 deletions(-) diff --git a/packages/build/src/compile/signable-compiler.ts b/packages/build/src/compile/signable-compiler.ts index b086d9b19..a67b13cb3 100644 --- a/packages/build/src/compile/signable-compiler.ts +++ b/packages/build/src/compile/signable-compiler.ts @@ -175,7 +175,10 @@ export class SignableCompiler { .concat(macKeychainAddon ? [macKeychainAddon] : []), preCompileHook, executableMetadata: this.executableMetadata, - useCodeCache: true, + // Node.js startup snapshots are an experimental feature of Node.js. + // If, at some point, something breaks because of it, we can always + // go back to using `useCodeCache: true` only. + useNodeSnapshot: true, }); } } diff --git a/packages/cli-repl/.depcheckrc b/packages/cli-repl/.depcheckrc index 685b47cc9..a712b4df0 100644 --- a/packages/cli-repl/.depcheckrc +++ b/packages/cli-repl/.depcheckrc @@ -11,5 +11,9 @@ ignores: - mocha - eslint-plugin-mocha - eslint-config-mongodb-js + # eagerly loaded startup snapshot dependencies + - '@mongodb-js/saslprep' + - socks + - emphasize ignore-patterns: - .eslintrc.js \ No newline at end of file diff --git a/packages/cli-repl/src/async-repl.ts b/packages/cli-repl/src/async-repl.ts index 28c326ca2..564814533 100644 --- a/packages/cli-repl/src/async-repl.ts +++ b/packages/cli-repl/src/async-repl.ts @@ -1,11 +1,9 @@ -import { Domain } from 'domain'; import type { EventEmitter } from 'events'; import type { ReadStream } from 'tty'; import isRecoverableError from 'is-recoverable-error'; import type { ReadLineOptions } from 'readline'; -import { Interface } from 'readline'; import type { ReplOptions, REPLServer } from 'repl'; -import { Recoverable, start as originalStart } from 'repl'; +import type { start as originalStart } from 'repl'; import { promisify } from 'util'; // Utility, inverse of Readonly @@ -75,6 +73,9 @@ function getPrompt(repl: any): string { * synchronous, and integrates nicely with Ctrl+C handling in that respect. */ export function start(opts: AsyncREPLOptions): REPLServer { + // 'repl' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Recoverable, start: originalStart } = require('repl'); const { asyncEval, wrapCallbackError = (err) => err, onAsyncSigint } = opts; if (onAsyncSigint) { (opts as ReplOptions).breakEvalOnSigint = true; @@ -118,6 +119,9 @@ export function start(opts: AsyncREPLOptions): REPLServer { // Use public getPrompt() API once available (Node.js 15+) const origPrompt = getPrompt(repl); + // 'readline' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Interface } = require('readline'); // Disable printing prompts while we're evaluating code. We're using the // readline superclass method instead of the REPL one here, because the REPL // one stores the prompt to later be reset in case of dropping into .editor @@ -255,6 +259,9 @@ function wrapNoSyncDomainError( fn: (...args: Args) => Ret ) { return (...args: Args): Ret => { + // 'domain' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Domain } = require('domain'); const origEmit = Domain.prototype.emit; // When the Node.js core REPL encounters an exception during synchronous diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index 27c980bc3..cc4357cf0 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -12,7 +12,7 @@ import type { CliOptions, DevtoolsConnectOptions } from '@mongosh/arg-parser'; import { SnippetManager } from '@mongosh/snippet-manager'; import { Editor } from '@mongosh/editor'; import { redactSensitiveData } from '@mongosh/history'; -import Analytics from 'analytics-node'; +import type Analytics from 'analytics-node'; import askpassword from 'askpassword'; import { EventEmitter, once } from 'events'; import yaml from 'js-yaml'; @@ -468,6 +468,9 @@ export class CliRepl implements MongoshIOProvider { if (!apiKey) { throw new Error('no analytics API key defined'); } + // 'http' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const Analytics = require('analytics-node'); this.segmentAnalytics = new Analytics( apiKey, { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index f3858b343..38517c597 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -23,11 +23,8 @@ import askcharacter from 'askcharacter'; import askpassword from 'askpassword'; import { Console } from 'console'; import { once } from 'events'; -import prettyRepl from 'pretty-repl'; import type { ReplOptions, REPLServer } from 'repl'; -import { start as replStart } from 'repl'; import type { Readable, Writable } from 'stream'; -import { PassThrough } from 'stream'; import type { ReadStream, WriteStream } from 'tty'; import { callbackify, promisify } from 'util'; import * as asyncRepl from './async-repl'; @@ -104,49 +101,6 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; -/** - * Helper function that tests whether the bug referenced in - * https://github.com/nodejs/node/pull/38314 is present, and if it is, - * monkey-patches the repl instance in question to avoid it. - * - * @param repl The REPLServer instance to patch - */ -function fixupReplForNodeBug38314(repl: REPLServer): void { - { - // Check whether bug is present: - const input = new PassThrough(); - const output = new PassThrough(); - const evalFn = (code: any, ctx: any, filename: any, cb: any) => - cb(new Error('err')); - const prompt = 'prompt#'; - replStart({ input, output, eval: evalFn as any, prompt }); - input.end('s\n'); - if (!String(output.read()).includes('prompt#prompt#')) { - return; // All good, nothing to do here. - } - } - - // If it is, fix up the REPL's domain 'error' listener to not call displayPrompt() - const domain = (repl as any)._domain; - const domainErrorListeners = domain.listeners('error'); - const origListener = domainErrorListeners.find( - (fn: any) => fn.name === 'debugDomainError' - ); - if (!origListener) { - throw new Error('Could not find REPL domain error listener'); - } - domain.removeListener('error', origListener); - domain.on('error', function (this: any, err: Error) { - const origDisplayPrompt = repl.displayPrompt; - repl.displayPrompt = () => {}; - try { - origListener.call(this, err); - } finally { - repl.displayPrompt = origDisplayPrompt; - } - }); -} - /** * An instance of a `mongosh` REPL, without any of the actual I/O. * Specifically, code called by this class should not do any @@ -239,7 +193,9 @@ class MongoshNodeRepl implements EvaluationListener { (await this.getConfig('redactHistory')) ?? this.redactHistory; const repl = asyncRepl.start({ - start: prettyRepl.start, + // 'repl' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + start: require('pretty-repl').start, input: this.lineByLineInput as unknown as Readable, output: this.output, prompt: '', @@ -255,7 +211,6 @@ class MongoshNodeRepl implements EvaluationListener { onAsyncSigint: this.onAsyncSigint.bind(this), ...this.nodeReplOptions, }); - fixupReplForNodeBug38314(repl); const console = new Console({ stdout: this.output, diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 53693b5b6..5d398f449 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -1,35 +1,73 @@ let fipsError: Error | undefined; -if (process.argv.includes('--tlsFIPSMode')) { - // FIPS mode should be enabled before we run any other code, including any dependencies. - try { - require('crypto').setFips(1); - } catch (err: any) { - fipsError = err; +function enableFipsIfRequested() { + if (process.argv.includes('--tlsFIPSMode')) { + // FIPS mode should be enabled before we run any other code, including any dependencies. + // We still wrap this into a function so we can also call it immediately after + // entering the snapshot main function. + try { + require('crypto').setFips(1); + } catch (err: any) { + fipsError ??= err; + } } } +enableFipsIfRequested(); +import { CliRepl } from './cli-repl'; +import { parseCliArgs } from './arg-parser'; +import { runSmokeTests } from './smoke-tests'; +import { USAGE } from './constants'; import { buildInfo } from './build-info'; -import { runMain } from 'module'; +import { getStoragePaths, getGlobalConfigPaths } from './config-directory'; +import { getCryptLibraryPaths } from './crypt-library-paths'; +import { getTlsCertificateSelector } from './tls-certificate-selector'; +import { redactURICredentials } from '@mongosh/history'; +import { generateConnectionInfoFromCliArgs } from '@mongosh/arg-parser'; +import askcharacter from 'askcharacter'; +import { PassThrough } from 'stream'; import crypto from 'crypto'; import net from 'net'; +import v8 from 'v8'; + +// TS does not yet have type definitions for v8.startupSnapshot +if ((v8 as any)?.startupSnapshot?.isBuildingSnapshot?.()) { + // Import a few nested deps of dependencies that cannot be included in the + // primary snapshot eagerly. + require('@mongodb-js/saslprep'); // Driver dependency + require('socks'); // Driver dependency + require('emphasize'); // Dependency of pretty-repl + + { + const console = require('console'); + const ConsoleCtor = console.Console; + (v8 as any).startupSnapshot.addDeserializeCallback(() => { + console.Console = ConsoleCtor; + }); + } -// eslint-disable-next-line @typescript-eslint/no-floating-promises -(async () => { + (v8 as any).startupSnapshot.setDeserializeMainFunction(() => { + enableFipsIfRequested(); + void main(); + }); +} else { + void main(); +} + +// eslint-disable-next-line complexity +async function main() { if (process.env.MONGOSH_RUN_NODE_SCRIPT) { // For uncompiled mongosh: node /path/to/this/file script ... -> node script ... // FOr compiled mongosh: mongosh mongosh script ... -> mongosh script ... process.argv.splice(1, 1); - (runMain as any)(process.argv[1]); + // 'module' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + (require('module').runMain as any)(process.argv[1]); return; } let repl; let isSingleConsoleProcess = false; try { - const { parseCliArgs } = await import('./arg-parser'); - const { generateConnectionInfoFromCliArgs } = await import( - '@mongosh/arg-parser' - ); (net as any)?.setDefaultAutoSelectFamily?.(true); const options = parseCliArgs(process.argv); @@ -73,7 +111,6 @@ import net from 'net'; } if (options.help) { - const { USAGE } = await import('./constants'); console.log(USAGE); return; } @@ -87,7 +124,6 @@ import net from 'net'; return; } if (options.smokeTests) { - const { runSmokeTests } = await import('./smoke-tests'); const smokeTestServer = process.env.MONGOSH_SMOKE_TEST_SERVER; const cryptLibraryOpts = options.cryptSharedLibPath ? [`--cryptSharedLibPath=${options.cryptSharedLibPath}`] @@ -111,27 +147,6 @@ import net from 'net'; return; } - // Common case: We want to actually start as mongosh. - // We lazy-load the larger dependencies here to speed up startup in the - // less common cases (particularly because the cloud team wants --version - // to be fast). - // Note that when we add snapshot support, we will most likely have - // to move these back to be import statements at the top of the file. - // See https://jira.mongodb.org/browse/MONGOSH-1214 for some context. - const [ - { CliRepl }, - { getStoragePaths, getGlobalConfigPaths }, - { getCryptLibraryPaths }, - { getTlsCertificateSelector }, - { redactURICredentials }, - ] = await Promise.all([ - await import('./cli-repl'), - await import('./config-directory'), - await import('./crypt-library-paths'), - await import('./tls-certificate-selector'), - await import('@mongosh/history'), - ]); - if (process.execPath === process.argv[1]) { // Remove the built-in Node.js listener that prints e.g. deprecation // warnings in single-binary release mode. @@ -209,7 +224,6 @@ import net from 'net'; repl.bus.emit('mongosh:error', e, 'startup'); } if (isSingleConsoleProcess) { - const askcharacter = (await import('askcharacter')).default; // In single-process-console mode, it's confusing for the window to be // closed immediately after receiving an error. In that case, ask the // user to explicitly close the window. @@ -219,7 +233,7 @@ import net from 'net'; } process.exit(1); } -})(); +} /** * Helper to set the window title for the terminal that stdout is @@ -249,13 +263,11 @@ function setTerminalWindowTitle(title: string): void { * @returns The written user input */ async function ask(prompt: string): Promise { - const { createInterface } = await import('readline'); - const { PassThrough } = await import('stream'); - // Copy stdin to a second stream so that we can still attach it // to the main mongosh REPL instance later without conflicts. const stdinCopy = process.stdin.pipe(new PassThrough()); try { + const { createInterface } = require('readline'); const readlineInterface = createInterface({ input: stdinCopy, output: process.stdout, diff --git a/packages/cli-repl/src/smoke-tests.ts b/packages/cli-repl/src/smoke-tests.ts index c2460e259..51b67a244 100644 --- a/packages/cli-repl/src/smoke-tests.ts +++ b/packages/cli-repl/src/smoke-tests.ts @@ -1,4 +1,3 @@ -import { spawn } from 'child_process'; import assert from 'assert'; import { once } from 'events'; import { redactURICredentials } from '@mongosh/history'; @@ -132,6 +131,9 @@ async function runSmokeTest({ exitCode?: number; includeStderr?: boolean; }): Promise { + // 'child_process' is not supported in startup snapshots yet. + // eslint-disable-next-line + const { spawn } = require('child_process') as typeof import('child_process'); const proc = spawn(executable, [...args], { stdio: ['pipe', 'pipe', includeStderr ? 'pipe' : 'inherit'], }); diff --git a/packages/cli-repl/src/tls-certificate-selector.ts b/packages/cli-repl/src/tls-certificate-selector.ts index 8ff24cc37..4435da218 100644 --- a/packages/cli-repl/src/tls-certificate-selector.ts +++ b/packages/cli-repl/src/tls-certificate-selector.ts @@ -2,6 +2,7 @@ import { MongoshUnimplementedError, MongoshInvalidInputError, } from '@mongosh/errors'; +import { createRequire } from 'module'; type TlsCertificateExporter = ( search: { subject: string } | { thumbprint: Buffer } @@ -42,18 +43,16 @@ export function getTlsCertificateSelector( } } -declare global { - const __non_webpack_require__: undefined | typeof require; -} - function getCertificateExporter(): TlsCertificateExporter | undefined { if (process.env.TEST_OS_EXPORT_CERTIFICATE_AND_KEY_PATH) { - if (typeof __non_webpack_require__ === 'function') { - return __non_webpack_require__( - process.env.TEST_OS_EXPORT_CERTIFICATE_AND_KEY_PATH - ); - } - return require(process.env.TEST_OS_EXPORT_CERTIFICATE_AND_KEY_PATH); + // Not using require() directly because that ends up referring + // to the webpack require, and even __non_webpack_require__ doesn't + // fully give us what we need because that can refer to the Node.js + // internal require() when running from a snapshot, not the public + // require(). + return createRequire(__filename)( + process.env.TEST_OS_EXPORT_CERTIFICATE_AND_KEY_PATH + ); } try { diff --git a/packages/cli-repl/src/update-notification-manager.ts b/packages/cli-repl/src/update-notification-manager.ts index b3a6b2a12..43531cdfb 100644 --- a/packages/cli-repl/src/update-notification-manager.ts +++ b/packages/cli-repl/src/update-notification-manager.ts @@ -1,6 +1,10 @@ import semver from 'semver'; import { promises as fs } from 'fs'; -import fetch from 'node-fetch'; +import type { RequestInfo, RequestInit, Response } from 'node-fetch'; + +// 'http' is not supported in startup snapshots yet. +const fetch = async (url: RequestInfo, init?: RequestInit): Promise => + await (await import('node-fetch')).default(url, init); interface MongoshUpdateLocalFileContents { lastChecked?: number; diff --git a/packages/cli-repl/webpack.config.js b/packages/cli-repl/webpack.config.js index 2ed75e1f5..5d1681c66 100644 --- a/packages/cli-repl/webpack.config.js +++ b/packages/cli-repl/webpack.config.js @@ -21,7 +21,12 @@ const config = { output: { path: path.resolve(__dirname, 'dist'), filename: 'mongosh.js', - libraryTarget: 'commonjs2', + library: { + name: 'mongosh', + // Doesn't really matter, we're not exposing anything here, but using `var` + // integrates more easily with snapshot building than e.g. CommonJS + type: 'var', + }, }, plugins: [webpackDependenciesPlugin], entry: './lib/run.js', diff --git a/packages/editor/src/editor.ts b/packages/editor/src/editor.ts index 8c0815936..fd0173046 100644 --- a/packages/editor/src/editor.ts +++ b/packages/editor/src/editor.ts @@ -2,7 +2,6 @@ import * as path from 'path'; import { once } from 'events'; import { promises as fs } from 'fs'; import type { Readable } from 'stream'; -import childProcess from 'child_process'; import { bson } from '@mongosh/service-provider-core'; import { makeMultilineJSIntoSingleLine } from '@mongosh/js-multiline-to-singleline'; @@ -237,7 +236,11 @@ export class Editor { code, }); - const proc = childProcess.spawn(editor, [path.basename(tmpDoc)], { + // 'child_process' is not supported in startup snapshots yet. + const { spawn } = + // eslint-disable-next-line + require('child_process') as typeof import('child_process'); + const proc = spawn(editor, [path.basename(tmpDoc)], { stdio: 'inherit', cwd: path.dirname(tmpDoc), shell: true, diff --git a/packages/logging/src/setup-logger-and-telemetry.ts b/packages/logging/src/setup-logger-and-telemetry.ts index 2c3c8499c..7fd76f748 100644 --- a/packages/logging/src/setup-logger-and-telemetry.ts +++ b/packages/logging/src/setup-logger-and-telemetry.ts @@ -34,7 +34,6 @@ import type { import { inspect } from 'util'; import type { MongoLogWriter } from 'mongodb-log-writer'; import { mongoLogId } from 'mongodb-log-writer'; -import { hookLogger as devtoolsConnectHookLogger } from '@mongodb-js/devtools-connect'; import type { MongoshAnalytics } from './analytics-helpers'; /** @@ -639,7 +638,10 @@ export function setupLoggerAndTelemetry( // Log ids 1_000_000_034 through 1_000_000_042 are reserved for the // devtools-connect package which was split out from mongosh. - devtoolsConnectHookLogger(bus, log, 'mongosh', redactURICredentials); + // 'mongodb' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { hookLogger } = require('@mongodb-js/devtools-connect'); + hookLogger(bus, log, 'mongosh', redactURICredentials); bus.on('mongosh-sp:reset-connection-options', function () { log.info( diff --git a/packages/service-provider-server/src/cli-service-provider.ts b/packages/service-provider-server/src/cli-service-provider.ts index c7ba53225..e1a81afa7 100644 --- a/packages/service-provider-server/src/cli-service-provider.ts +++ b/packages/service-provider-server/src/cli-service-provider.ts @@ -8,8 +8,9 @@ import type { RunCommandCursor, RunCursorCommandOptions, ClientEncryptionOptions, + MongoClient, + ReadPreference, } from 'mongodb'; -import { MongoClient, ReadPreference, BSON, ClientEncryption } from 'mongodb'; import type { ServiceProvider, @@ -68,10 +69,7 @@ import { ServiceProviderCore, } from '@mongosh/service-provider-core'; -import { - connectMongoClient, - DevtoolsConnectOptions, -} from '@mongodb-js/devtools-connect'; +import type { DevtoolsConnectOptions } from '@mongodb-js/devtools-connect'; import { MongoshCommandFailed, MongoshInternalError } from '@mongosh/errors'; import type { MongoshBus } from '@mongosh/types'; import { forceCloseMongoClient } from './mongodb-patches'; @@ -84,22 +82,46 @@ import type { CreateEncryptedCollectionOptions } from '@mongosh/service-provider import type { DevtoolsConnectionState } from '@mongodb-js/devtools-connect'; import { isDeepStrictEqual } from 'util'; -const bsonlib = { - Binary: BSON.Binary, - Code: BSON.Code, - DBRef: BSON.DBRef, - Double: BSON.Double, - Int32: BSON.Int32, - Long: BSON.Long, - MinKey: BSON.MinKey, - MaxKey: BSON.MaxKey, - ObjectId: BSON.ObjectId, - Timestamp: BSON.Timestamp, - Decimal128: BSON.Decimal128, - BSONSymbol: BSON.BSONSymbol, - calculateObjectSize: BSON.calculateObjectSize, - EJSON: BSON.EJSON, - BSONRegExp: BSON.BSONRegExp, +// 'mongodb' is not supported in startup snapshots yet. +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +function driver(): typeof import('mongodb') { + return require('mongodb'); +} + +const bsonlib = () => { + const { + Binary, + Code, + DBRef, + Double, + Int32, + Long, + MinKey, + MaxKey, + ObjectId, + Timestamp, + Decimal128, + BSONSymbol, + BSONRegExp, + BSON, + } = driver(); + return { + Binary, + Code, + DBRef, + Double, + Int32, + Long, + MinKey, + MaxKey, + ObjectId, + Timestamp, + Decimal128, + BSONSymbol, + calculateObjectSize: BSON.calculateObjectSize, + EJSON: BSON.EJSON, + BSONRegExp, + }; }; type DropDatabaseResult = { @@ -205,6 +227,9 @@ class CliServiceProvider }; } + const { MongoClient: MongoClientCtor } = driver(); + const { connectMongoClient } = require('@mongodb-js/devtools-connect'); + let client: MongoClient; let state: DevtoolsConnectionState | undefined; if (cliOptions.nodb) { @@ -218,13 +243,16 @@ class CliServiceProvider delete clientOptionsCopy.parentHandle; delete clientOptionsCopy.parentState; delete clientOptionsCopy.useSystemCA; - client = new MongoClient(connectionString.toString(), clientOptionsCopy); + client = new MongoClientCtor( + connectionString.toString(), + clientOptionsCopy + ); } else { ({ client, state } = await connectMongoClient( connectionString.toString(), clientOptions, bus, - MongoClient + MongoClientCtor )); } clientOptions.parentState = state; @@ -255,7 +283,7 @@ class CliServiceProvider clientOptions: DevtoolsConnectOptions, uri?: ConnectionString ) { - super(bsonlib); + super(bsonlib()); this.bus = bus; this.mongoClient = mongoClient; @@ -282,7 +310,7 @@ class CliServiceProvider return { nodeDriverVersion: tryCall(() => require('mongodb/package.json').version), libmongocryptVersion: tryCall( - () => ClientEncryption.libmongocryptVersion // getter that actually loads the native addon (!) + () => driver().ClientEncryption.libmongocryptVersion // getter that actually loads the native addon (!) ), libmongocryptNodeBindingsVersion: tryCall( () => require('mongodb-client-encryption/package.json').version @@ -296,11 +324,13 @@ class CliServiceProvider ): Promise { const connectionString = new ConnectionString(uri); const clientOptions = this.processDriverOptions(connectionString, options); + const { MongoClient: MongoClientCtor } = driver(); + const { connectMongoClient } = require('@mongodb-js/devtools-connect'); const { client, state } = await connectMongoClient( connectionString.toString(), clientOptions, this.bus, - MongoClient + MongoClientCtor ); clientOptions.parentState = state; return new CliServiceProvider( @@ -1199,6 +1229,7 @@ class CliServiceProvider readPreferenceFromOptions( options?: Omit ): ReadPreferenceLike | undefined { + const { ReadPreference } = driver(); return ReadPreference.fromOptions(options); } @@ -1208,6 +1239,8 @@ class CliServiceProvider * @param options */ async resetConnectionOptions(options: MongoClientOptions): Promise { + const { MongoClient: MongoClientCtor } = driver(); + const { connectMongoClient } = require('@mongodb-js/devtools-connect'); this.bus.emit('mongosh-sp:reset-connection-options'); this.currentClientOptions = { ...this.currentClientOptions, @@ -1221,7 +1254,7 @@ class CliServiceProvider (this.uri as ConnectionString).toString(), clientOptions, this.bus, - MongoClient + MongoClientCtor ); try { await this.mongoClient.close(); @@ -1400,7 +1433,10 @@ class CliServiceProvider .updateSearchIndex(indexName, definition); } - createClientEncryption(options: ClientEncryptionOptions): ClientEncryption { + createClientEncryption( + options: ClientEncryptionOptions + ): MongoCryptClientEncryption { + const { ClientEncryption } = driver(); return new ClientEncryption(this.mongoClient, options); } } diff --git a/packages/shell-evaluator/src/shell-evaluator.ts b/packages/shell-evaluator/src/shell-evaluator.ts index 04b6af8d2..b0416ee8e 100644 --- a/packages/shell-evaluator/src/shell-evaluator.ts +++ b/packages/shell-evaluator/src/shell-evaluator.ts @@ -14,6 +14,21 @@ type EvaluationFunction = ( import { HIDDEN_COMMANDS, redactSensitiveData } from '@mongosh/history'; +let hasAlreadyRunGlobalRuntimeSupportEval = false; +// `v8.startupSnapshot` is currently untyped, might as well use `any`. +let v8: any; +try { + v8 = require('v8'); +} catch { + /* not Node.js */ +} +if (v8?.startupSnapshot?.isBuildingSnapshot?.()) { + v8.startupSnapshot.addSerializeCallback(() => { + eval(new AsyncWriter().runtimeSupportCode()); + hasAlreadyRunGlobalRuntimeSupportEval = true; + }); +} + type ResultHandler = ( value: any ) => EvaluationResultType | Promise; @@ -85,7 +100,9 @@ class ShellEvaluator { // db.test.find().toArray() is a Promise for an Array from the context // in which the shell-api package lives and not from the context inside // the REPL (i.e. `db.test.find().toArray() instanceof Array` is `false`). - eval(supportCode); + if (!hasAlreadyRunGlobalRuntimeSupportEval) { + eval(supportCode); + } rewrittenInput = supportCode + ';\n' + rewrittenInput; } diff --git a/packages/snippet-manager/src/snippet-manager.ts b/packages/snippet-manager/src/snippet-manager.ts index 4f83f1638..69cae4db9 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -11,10 +11,8 @@ import path from 'path'; import { promisify, isDeepStrictEqual } from 'util'; import { Console } from 'console'; import { promises as fs } from 'fs'; -import spawn from 'cross-spawn'; import stream, { PassThrough } from 'stream'; import { once } from 'events'; -import fetch from 'node-fetch'; import tar from 'tar'; import zlib from 'zlib'; import bson from 'bson'; @@ -187,6 +185,12 @@ export class SnippetManager implements ShellPlugin { return this._instanceState.messageBus; } + async fetch(url: string) { + // 'http' is not supported in startup snapshots yet. + const fetch = await import('node-fetch'); + return await fetch.default(url); + } + async prepareNpm(): Promise { const npmdir = path.join(this.installdir, 'node_modules', 'npm'); const npmclipath = path.join(npmdir, 'bin', 'npm-cli.js'); @@ -225,7 +229,7 @@ export class SnippetManager implements ShellPlugin { const npmMetadataURL = (await this.registryBaseUrl()) + '/npm/latest'; interrupted.checkpoint(); - const npmMetadataResponse = await fetch(npmMetadataURL); + const npmMetadataResponse = await this.fetch(npmMetadataURL); if (!npmMetadataResponse.ok) { this.messageBus.emit('mongosh-snippets:npm-download-failed', { npmMetadataURL, @@ -248,7 +252,7 @@ export class SnippetManager implements ShellPlugin { } interrupted.checkpoint(); await this.print(`Downloading npm from ${npmTarballURL}...`); - const npmTarball = await fetch(npmTarballURL); + const npmTarball = await this.fetch(npmTarballURL); if (!npmTarball.ok) { this.messageBus.emit('mongosh-snippets:npm-download-failed', { npmMetadataURL, @@ -317,7 +321,7 @@ export class SnippetManager implements ShellPlugin { // Fetch all index files. repoData = await Promise.all( sourceURLs.map(async (url: string) => { - const repoRes = await fetch(url); + const repoRes = await this.fetch(url); if (!repoRes.ok) { this.messageBus.emit('mongosh-snippets:fetch-index-error', { action: 'fetch', @@ -454,6 +458,10 @@ export class SnippetManager implements ShellPlugin { args: [cmd, ...args], }); interrupted.checkpoint(); + + // 'child_process' is not supported in startup snapshots yet. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const spawn = require('cross-spawn'); const proc = spawn(cmd, args, { cwd: this.installdir, env: { ...process.env, MONGOSH_RUN_NODE_SCRIPT: '1' },