@documentdb-js/shell-runtime 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +94 -0
  3. package/dist/CommandInterceptor.d.ts +55 -0
  4. package/dist/CommandInterceptor.d.ts.map +1 -0
  5. package/dist/CommandInterceptor.js +87 -0
  6. package/dist/CommandInterceptor.js.map +1 -0
  7. package/dist/CommandInterceptor.test.d.ts +2 -0
  8. package/dist/CommandInterceptor.test.d.ts.map +1 -0
  9. package/dist/CommandInterceptor.test.js +195 -0
  10. package/dist/CommandInterceptor.test.js.map +1 -0
  11. package/dist/DocumentDBServiceProvider.d.ts +48 -0
  12. package/dist/DocumentDBServiceProvider.d.ts.map +1 -0
  13. package/dist/DocumentDBServiceProvider.js +59 -0
  14. package/dist/DocumentDBServiceProvider.js.map +1 -0
  15. package/dist/DocumentDBShellRuntime.d.ts +144 -0
  16. package/dist/DocumentDBShellRuntime.d.ts.map +1 -0
  17. package/dist/DocumentDBShellRuntime.js +521 -0
  18. package/dist/DocumentDBShellRuntime.js.map +1 -0
  19. package/dist/DocumentDBShellRuntime.test.d.ts +2 -0
  20. package/dist/DocumentDBShellRuntime.test.d.ts.map +1 -0
  21. package/dist/DocumentDBShellRuntime.test.js +200 -0
  22. package/dist/DocumentDBShellRuntime.test.js.map +1 -0
  23. package/dist/HelpProvider.d.ts +45 -0
  24. package/dist/HelpProvider.d.ts.map +1 -0
  25. package/dist/HelpProvider.js +179 -0
  26. package/dist/HelpProvider.js.map +1 -0
  27. package/dist/HelpProvider.test.d.ts +2 -0
  28. package/dist/HelpProvider.test.d.ts.map +1 -0
  29. package/dist/HelpProvider.test.js +133 -0
  30. package/dist/HelpProvider.test.js.map +1 -0
  31. package/dist/ResultTransformer.d.ts +58 -0
  32. package/dist/ResultTransformer.d.ts.map +1 -0
  33. package/dist/ResultTransformer.js +96 -0
  34. package/dist/ResultTransformer.js.map +1 -0
  35. package/dist/ResultTransformer.test.d.ts +2 -0
  36. package/dist/ResultTransformer.test.d.ts.map +1 -0
  37. package/dist/ResultTransformer.test.js +166 -0
  38. package/dist/ResultTransformer.test.js.map +1 -0
  39. package/dist/index.d.ts +7 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +20 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types.d.ts +65 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +7 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +30 -0
@@ -0,0 +1,144 @@
1
+ import { type MongoClient } from 'mongodb';
2
+ import { type ShellEvalOptions, type ShellEvaluationResult, type ShellRuntimeCallbacks, type ShellRuntimeOptions } from './types';
3
+ /**
4
+ * Shell runtime abstraction for DocumentDB.
5
+ *
6
+ * Wraps the @mongosh evaluation pipeline behind a clean API that both the
7
+ * query playground (scratchpad) and future interactive shell (Step 9) consume.
8
+ *
9
+ * The runtime:
10
+ * - Intercepts known commands (help) before they reach @mongosh
11
+ * - Creates a fresh @mongosh evaluation context per `evaluate()` call
12
+ * - Transforms raw @mongosh ShellResult into a protocol-agnostic result type
13
+ * - Delegates to `DocumentDBServiceProvider` for database operations
14
+ *
15
+ * The caller owns the `MongoClient` lifecycle — the runtime only uses it.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const runtime = new DocumentDBShellRuntime(mongoClient, {
20
+ * onConsoleOutput: (output) => console.log(output),
21
+ * onLog: (level, msg) => logger[level](msg),
22
+ * });
23
+ *
24
+ * const result = await runtime.evaluate('db.users.find({})', 'myDatabase');
25
+ * console.log(result.type, result.printable);
26
+ *
27
+ * runtime.dispose();
28
+ * ```
29
+ */
30
+ export declare class DocumentDBShellRuntime {
31
+ private readonly _mongoClient;
32
+ private readonly _callbacks;
33
+ private readonly _options;
34
+ private readonly _commandInterceptor;
35
+ private readonly _resultTransformer;
36
+ private _disposed;
37
+ private _persistent;
38
+ constructor(mongoClient: MongoClient, callbacks?: ShellRuntimeCallbacks, options?: ShellRuntimeOptions);
39
+ /**
40
+ * Evaluate shell code against the specified database.
41
+ *
42
+ * Creates a fresh @mongosh context per call — no variable leakage between
43
+ * evaluations. The target database is pre-selected via `use()` before
44
+ * executing user code.
45
+ *
46
+ * @param code - JavaScript/shell code string to evaluate
47
+ * @param databaseName - Target database name for execution
48
+ * @param evalOptions - Per-eval overrides (e.g. displayBatchSize from user settings)
49
+ * @returns Evaluation result with type, printable value, and timing
50
+ * @throws Error if the runtime has been disposed
51
+ * @throws Error if @mongosh evaluation fails (syntax error, runtime error, etc.)
52
+ */
53
+ evaluate(code: string, databaseName: string, evalOptions?: ShellEvalOptions): Promise<ShellEvaluationResult>;
54
+ /**
55
+ * Fresh-context evaluation (playground mode).
56
+ * Creates a new @mongosh context per call — no variable leakage between evaluations.
57
+ */
58
+ private evaluateFresh;
59
+ /**
60
+ * Persistent-context evaluation (interactive shell mode).
61
+ * Reuses the same @mongosh context across calls — variables, cursor state,
62
+ * and the `db` reference persist between evaluations.
63
+ */
64
+ private evaluatePersistent;
65
+ /**
66
+ * Dispose the runtime. After disposal, `evaluate()` calls will throw.
67
+ * Does NOT close the MongoClient — the caller owns its lifecycle.
68
+ */
69
+ dispose(): void;
70
+ /**
71
+ * Apply the display batch size to the instance state.
72
+ * Uses @mongosh's displayBatchSizeFromDBQuery property which takes
73
+ * precedence over config.get('displayBatchSize') in cursor iteration.
74
+ */
75
+ private applyBatchSize;
76
+ /**
77
+ * Register the console output listener on the @mongosh instance state.
78
+ * Routes `print()`, `printjson()`, and `console.log()` output to the
79
+ * caller-provided callback.
80
+ */
81
+ private registerConsoleOutputListener;
82
+ private log;
83
+ }
84
+ /**
85
+ * Rewrites bare `use <name>` / `show <name>` direct shell commands into
86
+ * function-call form (`use("<name>");` / `show("<name>");`).
87
+ *
88
+ * ## Why this is needed
89
+ *
90
+ * Direct shell commands like `use mydb` are detected by the evaluation
91
+ * pipeline as special tokens. When they appear as the first line of a
92
+ * multi-line code block, the pipeline processes only the direct command
93
+ * and silently discards all subsequent statements. Converting them to
94
+ * function-call form bypasses that short-circuit so the entire block is
95
+ * evaluated normally.
96
+ *
97
+ * ## How it works
98
+ *
99
+ * A single linear scan of the input tracks whether each character sits in
100
+ * plain code, a `//` line comment, a `/* ... *\/` block comment, a `'...'`
101
+ * or `"..."` string literal, a `` `...` `` template literal (including
102
+ * `${...}` expression nesting), or a `/.../` regex literal. Only line
103
+ * starts that fall in the plain-code state are considered candidates for
104
+ * rewriting. The per-line regex extracts the argument once the context
105
+ * check has passed.
106
+ *
107
+ * This avoids the collateral rewrites a naive regex would produce in:
108
+ *
109
+ * - single- and double-quoted string literals
110
+ * - template literals (`` `use mydb` `` as string content)
111
+ * - line and block comments
112
+ * - regular-expression literals (`/use mydb/`)
113
+ *
114
+ * ### Scope note — why a scanner, not a parser
115
+ *
116
+ * This is a lexical scanner, not a syntactic parser. It recognizes the
117
+ * literal forms listed above, which covers every collateral-rewrite
118
+ * failure mode reported so far. It intentionally does **not** understand
119
+ * JavaScript statement structure, so a handful of exotic cases are not
120
+ * distinguished:
121
+ *
122
+ * - `use` / `show` as a declared identifier rather than a statement
123
+ * starter (e.g. `const use = mydb; use` — runtime-invalid anyway).
124
+ * - `use mydb` nested inside a block such as `if (x) { use mydb }`
125
+ * (the bare form was never valid inside a block either; rewriting it
126
+ * to `use("mydb");` is still a legal expression statement).
127
+ * - Contextual keywords used where a regex is legal but the scanner
128
+ * guessed division, or vice-versa.
129
+ *
130
+ * Getting 100% of these right would require a real JS parser (e.g.
131
+ * `acorn` / `acorn-loose`) or the TypeScript compiler. We deliberately
132
+ * avoid adding that dependency: `@documentdb-js/shell-runtime`
133
+ * is intended to also ship as a lightweight standalone runtime for CLI
134
+ * tooling, and pulling in a full JS parser would dominate its footprint
135
+ * for an edge case that is not observed in real user input.
136
+ *
137
+ * ## ASI safety
138
+ *
139
+ * The emitted replacement always ends with `;` so a following line that
140
+ * begins with `[`, `(`, `+`, `-`, or `/` starts a fresh statement instead
141
+ * of binding to the call expression.
142
+ */
143
+ export declare function normalizeDirectCommands(code: string): string;
144
+ //# sourceMappingURL=DocumentDBShellRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentDBShellRuntime.d.ts","sourceRoot":"","sources":["../src/DocumentDBShellRuntime.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAC3B,MAAM,SAAS,CAAC;AAgBjB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,sBAAsB;IAC/B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,WAAW,CAOH;gBAEJ,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC,EAAE,mBAAmB;IAStG;;;;;;;;;;;;;OAaG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAgClH;;;OAGG;YACW,aAAa;IAoD3B;;;;OAIG;YACW,kBAAkB;IAiEhC;;;OAGG;IACH,OAAO,IAAI,IAAI;IAKf;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAMtB;;;;OAIG;IACH,OAAO,CAAC,6BAA6B;IAyBrC,OAAO,CAAC,GAAG;CAGd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6C5D"}
@@ -0,0 +1,521 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Microsoft Corporation. All rights reserved.
4
+ * Licensed under the MIT License. See License.txt in the project root for license information.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DocumentDBShellRuntime = void 0;
11
+ exports.normalizeDirectCommands = normalizeDirectCommands;
12
+ const shell_api_1 = require("@mongosh/shell-api");
13
+ const shell_evaluator_1 = require("@mongosh/shell-evaluator");
14
+ const vm_1 = __importDefault(require("vm"));
15
+ const CommandInterceptor_1 = require("./CommandInterceptor");
16
+ const DocumentDBServiceProvider_1 = require("./DocumentDBServiceProvider");
17
+ const HelpProvider_1 = require("./HelpProvider");
18
+ const ResultTransformer_1 = require("./ResultTransformer");
19
+ /**
20
+ * Matches `<cmd> <arg>` on a single line. Used to extract the argument text
21
+ * after the scanner has already confirmed that `<cmd>` starts a real top-level
22
+ * `use`/`show` statement (not inside a string, comment, or regex literal).
23
+ */
24
+ const DIRECT_COMMAND_LINE_RE = /^(\s*)(use|show)\s+([^(\s][^;]*?)\s*;?\s*$/;
25
+ const DEFAULT_OPTIONS = {
26
+ productName: 'DocumentDB for VS Code',
27
+ productDocsLink: 'https://github.com/microsoft/vscode-documentdb',
28
+ displayBatchSize: 50,
29
+ persistent: false,
30
+ };
31
+ /**
32
+ * Shell runtime abstraction for DocumentDB.
33
+ *
34
+ * Wraps the @mongosh evaluation pipeline behind a clean API that both the
35
+ * query playground (scratchpad) and future interactive shell (Step 9) consume.
36
+ *
37
+ * The runtime:
38
+ * - Intercepts known commands (help) before they reach @mongosh
39
+ * - Creates a fresh @mongosh evaluation context per `evaluate()` call
40
+ * - Transforms raw @mongosh ShellResult into a protocol-agnostic result type
41
+ * - Delegates to `DocumentDBServiceProvider` for database operations
42
+ *
43
+ * The caller owns the `MongoClient` lifecycle — the runtime only uses it.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const runtime = new DocumentDBShellRuntime(mongoClient, {
48
+ * onConsoleOutput: (output) => console.log(output),
49
+ * onLog: (level, msg) => logger[level](msg),
50
+ * });
51
+ *
52
+ * const result = await runtime.evaluate('db.users.find({})', 'myDatabase');
53
+ * console.log(result.type, result.printable);
54
+ *
55
+ * runtime.dispose();
56
+ * ```
57
+ */
58
+ class DocumentDBShellRuntime {
59
+ _mongoClient;
60
+ _callbacks;
61
+ _options;
62
+ _commandInterceptor;
63
+ _resultTransformer;
64
+ _disposed = false;
65
+ // Persistent mode state — reused across evaluate() calls when options.persistent is true
66
+ _persistent;
67
+ constructor(mongoClient, callbacks, options) {
68
+ this._mongoClient = mongoClient;
69
+ this._callbacks = callbacks ?? {};
70
+ this._options = { ...DEFAULT_OPTIONS, ...options };
71
+ const helpSurface = this._options.persistent ? 'shell' : 'playground';
72
+ this._commandInterceptor = new CommandInterceptor_1.CommandInterceptor(new HelpProvider_1.HelpProvider(helpSurface));
73
+ this._resultTransformer = new ResultTransformer_1.ResultTransformer();
74
+ }
75
+ /**
76
+ * Evaluate shell code against the specified database.
77
+ *
78
+ * Creates a fresh @mongosh context per call — no variable leakage between
79
+ * evaluations. The target database is pre-selected via `use()` before
80
+ * executing user code.
81
+ *
82
+ * @param code - JavaScript/shell code string to evaluate
83
+ * @param databaseName - Target database name for execution
84
+ * @param evalOptions - Per-eval overrides (e.g. displayBatchSize from user settings)
85
+ * @returns Evaluation result with type, printable value, and timing
86
+ * @throws Error if the runtime has been disposed
87
+ * @throws Error if @mongosh evaluation fails (syntax error, runtime error, etc.)
88
+ */
89
+ async evaluate(code, databaseName, evalOptions) {
90
+ if (this._disposed) {
91
+ throw new Error('Shell runtime has been disposed');
92
+ }
93
+ // Check for intercepted commands (help, etc.)
94
+ const intercepted = this._commandInterceptor.tryIntercept(code);
95
+ if (intercepted) {
96
+ return intercepted;
97
+ }
98
+ // Normalize bare direct commands (`use dbName`, `show dbs`) into function-call
99
+ // form (`use("dbName")`, `show("dbs")`) so they go through the async rewriter
100
+ // instead of short-circuiting. Without this, a bare `use` as the first token
101
+ // of a multi-line block consumes the entire input and silently drops subsequent
102
+ // statements.
103
+ code = normalizeDirectCommands(code);
104
+ this.log('trace', `Evaluating code (${code.split('\n').length} lines, ${code.length} chars, db: ${databaseName})`);
105
+ const startTime = Date.now();
106
+ if (this._options.persistent) {
107
+ return this.evaluatePersistent(code, databaseName, evalOptions, startTime);
108
+ }
109
+ else {
110
+ return this.evaluateFresh(code, databaseName, evalOptions, startTime);
111
+ }
112
+ }
113
+ /**
114
+ * Fresh-context evaluation (playground mode).
115
+ * Creates a new @mongosh context per call — no variable leakage between evaluations.
116
+ */
117
+ async evaluateFresh(code, databaseName, evalOptions, startTime) {
118
+ // Create fresh shell context per execution
119
+ const { serviceProvider, bus } = DocumentDBServiceProvider_1.DocumentDBServiceProvider.createForDocumentDB(this._mongoClient, this._options.productName, this._options.productDocsLink);
120
+ const instanceState = new shell_api_1.ShellInstanceState(serviceProvider, bus);
121
+ try {
122
+ const evaluator = new shell_evaluator_1.ShellEvaluator(instanceState);
123
+ this.applyBatchSize(instanceState, evalOptions);
124
+ this.registerConsoleOutputListener(instanceState);
125
+ // Set up eval context with shell globals (db, ObjectId, ISODate, etc.)
126
+ const context = {};
127
+ instanceState.setCtx(context);
128
+ // Custom eval function using vm.runInContext for sandboxed execution
129
+ // eslint-disable-next-line @typescript-eslint/require-await
130
+ const customEvalFn = async (evalCode, ctx) => {
131
+ const vmContext = vm_1.default.isContext(ctx) ? ctx : vm_1.default.createContext(ctx);
132
+ return vm_1.default.runInContext(evalCode, vmContext);
133
+ };
134
+ // Pre-select the target database (fresh context each time)
135
+ await evaluator.customEval(customEvalFn, `use(${JSON.stringify(databaseName)})`, context, 'playground');
136
+ // Evaluate user code
137
+ const result = await evaluator.customEval(customEvalFn, code, context, 'playground');
138
+ const durationMs = Date.now() - startTime;
139
+ this.log('trace', `Evaluation complete (${durationMs}ms)`);
140
+ return this._resultTransformer.transform(result, durationMs);
141
+ }
142
+ finally {
143
+ await instanceState.close();
144
+ }
145
+ }
146
+ /**
147
+ * Persistent-context evaluation (interactive shell mode).
148
+ * Reuses the same @mongosh context across calls — variables, cursor state,
149
+ * and the `db` reference persist between evaluations.
150
+ */
151
+ async evaluatePersistent(code, databaseName, evalOptions, startTime) {
152
+ // Initialize persistent state on first call
153
+ if (!this._persistent) {
154
+ const { serviceProvider, bus } = DocumentDBServiceProvider_1.DocumentDBServiceProvider.createForDocumentDB(this._mongoClient, this._options.productName, this._options.productDocsLink);
155
+ const instanceState = new shell_api_1.ShellInstanceState(serviceProvider, bus);
156
+ const evaluator = new shell_evaluator_1.ShellEvaluator(instanceState);
157
+ const context = {};
158
+ instanceState.setCtx(context);
159
+ const vmContext = vm_1.default.createContext(context);
160
+ this.registerConsoleOutputListener(instanceState);
161
+ // Pre-select the initial database
162
+ await evaluator.customEval(
163
+ // eslint-disable-next-line @typescript-eslint/require-await
164
+ async (evalCode, _ctx) => {
165
+ return vm_1.default.runInContext(evalCode, vmContext);
166
+ }, `use(${JSON.stringify(databaseName)})`, context, 'shell');
167
+ this._persistent = { instanceState, evaluator, context, vmContext };
168
+ }
169
+ const { instanceState, evaluator, context, vmContext } = this._persistent;
170
+ // Apply batch size per-eval (may change between evaluations via settings)
171
+ this.applyBatchSize(instanceState, evalOptions);
172
+ // Evaluate user code using the persistent context
173
+ const result = await evaluator.customEval(
174
+ // eslint-disable-next-line @typescript-eslint/require-await
175
+ async (evalCode, _ctx) => {
176
+ return vm_1.default.runInContext(evalCode, vmContext);
177
+ }, code, context, 'shell');
178
+ const durationMs = Date.now() - startTime;
179
+ this.log('trace', `Evaluation complete (${durationMs}ms)`);
180
+ return this._resultTransformer.transform(result, durationMs);
181
+ }
182
+ /**
183
+ * Dispose the runtime. After disposal, `evaluate()` calls will throw.
184
+ * Does NOT close the MongoClient — the caller owns its lifecycle.
185
+ */
186
+ dispose() {
187
+ this._disposed = true;
188
+ this._persistent = undefined;
189
+ }
190
+ /**
191
+ * Apply the display batch size to the instance state.
192
+ * Uses @mongosh's displayBatchSizeFromDBQuery property which takes
193
+ * precedence over config.get('displayBatchSize') in cursor iteration.
194
+ */
195
+ applyBatchSize(instanceState, evalOptions) {
196
+ const batchSize = evalOptions?.displayBatchSize ?? this._options.displayBatchSize ?? DEFAULT_OPTIONS.displayBatchSize;
197
+ instanceState.displayBatchSizeFromDBQuery = batchSize;
198
+ }
199
+ /**
200
+ * Register the console output listener on the @mongosh instance state.
201
+ * Routes `print()`, `printjson()`, and `console.log()` output to the
202
+ * caller-provided callback.
203
+ */
204
+ registerConsoleOutputListener(instanceState) {
205
+ const onConsoleOutput = this._callbacks.onConsoleOutput;
206
+ if (!onConsoleOutput) {
207
+ return;
208
+ }
209
+ instanceState.setEvaluationListener({
210
+ onPrint(values, _type) {
211
+ const output = values
212
+ .map((v) => {
213
+ if (typeof v.printable === 'string') {
214
+ return v.printable;
215
+ }
216
+ try {
217
+ return JSON.stringify(v.printable, null, 2);
218
+ }
219
+ catch {
220
+ return String(v.printable);
221
+ }
222
+ })
223
+ .join(' ');
224
+ onConsoleOutput(output + '\n');
225
+ },
226
+ });
227
+ }
228
+ log(level, message) {
229
+ this._callbacks.onLog?.(level, message);
230
+ }
231
+ }
232
+ exports.DocumentDBShellRuntime = DocumentDBShellRuntime;
233
+ /**
234
+ * Rewrites bare `use <name>` / `show <name>` direct shell commands into
235
+ * function-call form (`use("<name>");` / `show("<name>");`).
236
+ *
237
+ * ## Why this is needed
238
+ *
239
+ * Direct shell commands like `use mydb` are detected by the evaluation
240
+ * pipeline as special tokens. When they appear as the first line of a
241
+ * multi-line code block, the pipeline processes only the direct command
242
+ * and silently discards all subsequent statements. Converting them to
243
+ * function-call form bypasses that short-circuit so the entire block is
244
+ * evaluated normally.
245
+ *
246
+ * ## How it works
247
+ *
248
+ * A single linear scan of the input tracks whether each character sits in
249
+ * plain code, a `//` line comment, a `/* ... *\/` block comment, a `'...'`
250
+ * or `"..."` string literal, a `` `...` `` template literal (including
251
+ * `${...}` expression nesting), or a `/.../` regex literal. Only line
252
+ * starts that fall in the plain-code state are considered candidates for
253
+ * rewriting. The per-line regex extracts the argument once the context
254
+ * check has passed.
255
+ *
256
+ * This avoids the collateral rewrites a naive regex would produce in:
257
+ *
258
+ * - single- and double-quoted string literals
259
+ * - template literals (`` `use mydb` `` as string content)
260
+ * - line and block comments
261
+ * - regular-expression literals (`/use mydb/`)
262
+ *
263
+ * ### Scope note — why a scanner, not a parser
264
+ *
265
+ * This is a lexical scanner, not a syntactic parser. It recognizes the
266
+ * literal forms listed above, which covers every collateral-rewrite
267
+ * failure mode reported so far. It intentionally does **not** understand
268
+ * JavaScript statement structure, so a handful of exotic cases are not
269
+ * distinguished:
270
+ *
271
+ * - `use` / `show` as a declared identifier rather than a statement
272
+ * starter (e.g. `const use = mydb; use` — runtime-invalid anyway).
273
+ * - `use mydb` nested inside a block such as `if (x) { use mydb }`
274
+ * (the bare form was never valid inside a block either; rewriting it
275
+ * to `use("mydb");` is still a legal expression statement).
276
+ * - Contextual keywords used where a regex is legal but the scanner
277
+ * guessed division, or vice-versa.
278
+ *
279
+ * Getting 100% of these right would require a real JS parser (e.g.
280
+ * `acorn` / `acorn-loose`) or the TypeScript compiler. We deliberately
281
+ * avoid adding that dependency: `@documentdb-js/shell-runtime`
282
+ * is intended to also ship as a lightweight standalone runtime for CLI
283
+ * tooling, and pulling in a full JS parser would dominate its footprint
284
+ * for an edge case that is not observed in real user input.
285
+ *
286
+ * ## ASI safety
287
+ *
288
+ * The emitted replacement always ends with `;` so a following line that
289
+ * begins with `[`, `(`, `+`, `-`, or `/` starts a fresh statement instead
290
+ * of binding to the call expression.
291
+ */
292
+ function normalizeDirectCommands(code) {
293
+ if (!code.includes('\n')) {
294
+ return code;
295
+ }
296
+ // Cheap early exit: if neither token appears anywhere, skip scanning.
297
+ if (!/\b(use|show)\b/.test(code)) {
298
+ return code;
299
+ }
300
+ const candidateLineStarts = findCodeLineStarts(code);
301
+ if (candidateLineStarts.length === 0) {
302
+ return code;
303
+ }
304
+ const edits = [];
305
+ for (const lineStart of candidateLineStarts) {
306
+ const nextNewline = code.indexOf('\n', lineStart);
307
+ const lineEnd = nextNewline === -1 ? code.length : nextNewline;
308
+ const line = code.slice(lineStart, lineEnd);
309
+ const match = DIRECT_COMMAND_LINE_RE.exec(line);
310
+ if (!match)
311
+ continue;
312
+ const [, indent, cmd, arg] = match;
313
+ edits.push({
314
+ lineStart,
315
+ lineEnd,
316
+ replacement: `${indent}${cmd}(${JSON.stringify(arg)});`,
317
+ });
318
+ }
319
+ if (edits.length === 0) {
320
+ return code;
321
+ }
322
+ // Apply right-to-left so earlier offsets stay valid.
323
+ edits.sort((a, b) => b.lineStart - a.lineStart);
324
+ let result = code;
325
+ for (const edit of edits) {
326
+ result = result.slice(0, edit.lineStart) + edit.replacement + result.slice(edit.lineEnd);
327
+ }
328
+ return result;
329
+ }
330
+ /**
331
+ * Scan `code` once and return the offsets of every line start that falls in
332
+ * plain-code state (i.e., outside any string, template, comment, or regex
333
+ * literal). The returned offsets are candidates for direct-command rewriting.
334
+ *
335
+ * The scanner covers exactly what we need to avoid false rewrites:
336
+ *
337
+ * - `//` line comments and `/* *\/` block comments
338
+ * - single- and double-quoted strings with `\` escapes
339
+ * - template literals, including nested `${ ... }` expressions (which are
340
+ * themselves code and can contain further strings/templates)
341
+ * - regex literals, disambiguated from division by tracking whether a `/`
342
+ * can begin an expression at its position
343
+ *
344
+ * It is **lexical** only; it does not build an AST or understand statement
345
+ * structure. A full parser (e.g. `acorn` / `acorn-loose`) would be needed
346
+ * for 100% syntactic accuracy — see the note on `normalizeDirectCommands`
347
+ * for why that trade-off is intentional here.
348
+ */
349
+ function findCodeLineStarts(code) {
350
+ const starts = [];
351
+ // `${...}` nesting inside template literals: each element counts the
352
+ // currently-open `{` inside that expression so we know when to pop back
353
+ // into template-literal state.
354
+ const templateStack = [];
355
+ let inLineComment = false;
356
+ let inBlockComment = false;
357
+ let stringQuote = null;
358
+ let inTemplate = false;
359
+ let inRegex = false;
360
+ let regexCharClass = false;
361
+ // Whether a `/` at the current cursor may start a regex literal.
362
+ let canRegex = true;
363
+ // True only at the FIRST offset of a line (offset 0, or the position
364
+ // right after a newline). Cleared as soon as we consume that offset,
365
+ // so we never record the same line twice.
366
+ let atLineStart = true;
367
+ const len = code.length;
368
+ for (let i = 0; i < len; i++) {
369
+ const ch = code[i];
370
+ const next = i + 1 < len ? code[i + 1] : '';
371
+ // Record plain-code line starts (at most once per line).
372
+ if (atLineStart &&
373
+ !inLineComment &&
374
+ !inBlockComment &&
375
+ stringQuote === null &&
376
+ !inTemplate &&
377
+ !inRegex &&
378
+ templateStack.length === 0) {
379
+ starts.push(i);
380
+ }
381
+ atLineStart = false;
382
+ if (inLineComment) {
383
+ if (ch === '\n') {
384
+ inLineComment = false;
385
+ atLineStart = true;
386
+ canRegex = true;
387
+ }
388
+ continue;
389
+ }
390
+ if (inBlockComment) {
391
+ if (ch === '*' && next === '/') {
392
+ inBlockComment = false;
393
+ i++;
394
+ canRegex = true;
395
+ }
396
+ else if (ch === '\n') {
397
+ atLineStart = true;
398
+ }
399
+ continue;
400
+ }
401
+ if (stringQuote !== null) {
402
+ if (ch === '\\' && next !== '') {
403
+ i++;
404
+ continue;
405
+ }
406
+ if (ch === stringQuote) {
407
+ stringQuote = null;
408
+ canRegex = false;
409
+ }
410
+ else if (ch === '\n') {
411
+ // Unterminated string at newline: recover by exiting string
412
+ // state so we don't swallow the rest of the input.
413
+ stringQuote = null;
414
+ atLineStart = true;
415
+ canRegex = true;
416
+ }
417
+ continue;
418
+ }
419
+ if (inTemplate) {
420
+ if (ch === '\\' && next !== '') {
421
+ i++;
422
+ continue;
423
+ }
424
+ if (ch === '`') {
425
+ inTemplate = false;
426
+ canRegex = false;
427
+ }
428
+ else if (ch === '$' && next === '{') {
429
+ templateStack.push(1);
430
+ inTemplate = false;
431
+ i++;
432
+ canRegex = true;
433
+ }
434
+ else if (ch === '\n') {
435
+ atLineStart = true;
436
+ }
437
+ continue;
438
+ }
439
+ if (inRegex) {
440
+ if (ch === '\\' && next !== '') {
441
+ i++;
442
+ continue;
443
+ }
444
+ if (ch === '[') {
445
+ regexCharClass = true;
446
+ }
447
+ else if (ch === ']') {
448
+ regexCharClass = false;
449
+ }
450
+ else if (ch === '/' && !regexCharClass) {
451
+ inRegex = false;
452
+ // Consume optional flags.
453
+ while (i + 1 < len && /[a-z]/i.test(code[i + 1]))
454
+ i++;
455
+ canRegex = false;
456
+ }
457
+ else if (ch === '\n') {
458
+ // Unterminated regex: recover.
459
+ inRegex = false;
460
+ regexCharClass = false;
461
+ atLineStart = true;
462
+ canRegex = true;
463
+ }
464
+ continue;
465
+ }
466
+ // Plain-code state (outer) or template-expression state (inner).
467
+ if (ch === '/' && next === '/') {
468
+ inLineComment = true;
469
+ i++;
470
+ continue;
471
+ }
472
+ if (ch === '/' && next === '*') {
473
+ inBlockComment = true;
474
+ i++;
475
+ continue;
476
+ }
477
+ if (ch === '"' || ch === "'") {
478
+ stringQuote = ch;
479
+ canRegex = false;
480
+ continue;
481
+ }
482
+ if (ch === '`') {
483
+ inTemplate = true;
484
+ canRegex = false;
485
+ continue;
486
+ }
487
+ if (ch === '/' && canRegex) {
488
+ inRegex = true;
489
+ regexCharClass = false;
490
+ continue;
491
+ }
492
+ if (ch === '{' && templateStack.length > 0) {
493
+ templateStack[templateStack.length - 1]++;
494
+ }
495
+ if (ch === '}' && templateStack.length > 0) {
496
+ templateStack[templateStack.length - 1]--;
497
+ if (templateStack[templateStack.length - 1] === 0) {
498
+ templateStack.pop();
499
+ inTemplate = true;
500
+ canRegex = false;
501
+ continue;
502
+ }
503
+ }
504
+ if (ch === '\n') {
505
+ atLineStart = true;
506
+ canRegex = true;
507
+ continue;
508
+ }
509
+ // Heuristic for the regex/division ambiguity: letters/digits/closers
510
+ // disallow a regex at the next `/`; other punctuation allows one.
511
+ // Adequate for line-start candidates, which is all we care about.
512
+ if (/[A-Za-z0-9_$)\]]/.test(ch)) {
513
+ canRegex = false;
514
+ }
515
+ else if (!/\s/.test(ch)) {
516
+ canRegex = true;
517
+ }
518
+ }
519
+ return starts;
520
+ }
521
+ //# sourceMappingURL=DocumentDBShellRuntime.js.map