@harperfast/harper-pro 5.0.0-beta.7 → 5.0.0-beta.8

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 (41) hide show
  1. package/core/bin/cliOperations.js +3 -3
  2. package/core/components/ApplicationScope.ts +1 -0
  3. package/core/components/componentLoader.ts +4 -1
  4. package/core/resources/DatabaseTransaction.ts +67 -32
  5. package/core/resources/Table.ts +29 -10
  6. package/core/security/jsLoader.ts +149 -101
  7. package/core/server/REST.ts +12 -2
  8. package/core/server/http.ts +4 -1
  9. package/dist/core/bin/cliOperations.js +3 -3
  10. package/dist/core/bin/cliOperations.js.map +1 -1
  11. package/dist/core/components/ApplicationScope.js +1 -0
  12. package/dist/core/components/ApplicationScope.js.map +1 -1
  13. package/dist/core/components/componentLoader.js +2 -1
  14. package/dist/core/components/componentLoader.js.map +1 -1
  15. package/dist/core/resources/DatabaseTransaction.js +71 -35
  16. package/dist/core/resources/DatabaseTransaction.js.map +1 -1
  17. package/dist/core/resources/Table.js +15 -10
  18. package/dist/core/resources/Table.js.map +1 -1
  19. package/dist/core/security/jsLoader.js +123 -90
  20. package/dist/core/security/jsLoader.js.map +1 -1
  21. package/dist/core/server/REST.js +13 -4
  22. package/dist/core/server/REST.js.map +1 -1
  23. package/dist/core/server/http.js +5 -0
  24. package/dist/core/server/http.js.map +1 -1
  25. package/dist/licensing/usageLicensing.js +16 -26
  26. package/dist/licensing/usageLicensing.js.map +1 -1
  27. package/dist/replication/replicationConnection.js +2 -2
  28. package/dist/replication/replicationConnection.js.map +1 -1
  29. package/licensing/usageLicensing.ts +22 -32
  30. package/npm-shrinkwrap.json +223 -223
  31. package/package.json +3 -3
  32. package/replication/replicationConnection.ts +2 -2
  33. package/studio/web/assets/{index-ClD_q6ya.js → index-CXQsBaYq.js} +5 -5
  34. package/studio/web/assets/{index-ClD_q6ya.js.map → index-CXQsBaYq.js.map} +1 -1
  35. package/studio/web/assets/{index.lazy-CXzU1gVu.js → index.lazy-C3Ejfvna.js} +2 -2
  36. package/studio/web/assets/{index.lazy-CXzU1gVu.js.map → index.lazy-C3Ejfvna.js.map} +1 -1
  37. package/studio/web/assets/{profile-DCNVg5yY.js → profile-BbbbWJCN.js} +2 -2
  38. package/studio/web/assets/{profile-DCNVg5yY.js.map → profile-BbbbWJCN.js.map} +1 -1
  39. package/studio/web/assets/{status-CoGlcjSB.js → status-CFe85l8C.js} +2 -2
  40. package/studio/web/assets/{status-CoGlcjSB.js.map → status-CFe85l8C.js.map} +1 -1
  41. package/studio/web/index.html +1 -1
@@ -10,7 +10,7 @@ const YAML = require('yaml');
10
10
  const { packageDirectory } = require('../components/packageComponent.ts');
11
11
  const { encode } = require('cbor-x');
12
12
  const { getHdbPid } = require('../utility/processManagement/processManagement.js');
13
- const { initConfig } = require('../config/configUtils.js');
13
+ const { initConfig, getConfigPath } = require('../config/configUtils.js');
14
14
 
15
15
  const OP_ALIASES = { deploy: 'deploy_component', package: 'package_component' };
16
16
 
@@ -93,7 +93,7 @@ async function cliOperations(req) {
93
93
  process.exit(1);
94
94
  }
95
95
 
96
- if (!fs.existsSync(envMgr.get(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET))) {
96
+ if (!fs.existsSync(getConfigPath(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET))) {
97
97
  console.error('No domain socket found, unable to perform this operation');
98
98
  process.exit(1);
99
99
  }
@@ -102,7 +102,7 @@ async function cliOperations(req) {
102
102
  try {
103
103
  let options = target ?? {
104
104
  protocol: 'http:',
105
- socketPath: envMgr.get(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
105
+ socketPath: getConfigPath(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
106
106
  };
107
107
  options.method = 'POST';
108
108
  options.headers = { 'Content-Type': 'application/json' };
@@ -23,6 +23,7 @@ export class ApplicationScope {
23
23
  dependencyContainment?: boolean; // option to set this from the scope
24
24
  verifyPath?: string;
25
25
  config: any;
26
+ moduleCache: any; // used by the loader to retain a cache of modules, type is an internal detail of the loader
26
27
  constructor(name: string, resources: Resources, server: Server, isInternal = false, verifyPath?: string) {
27
28
  this.logger = forComponent(name, !isInternal);
28
29
 
@@ -39,6 +39,7 @@ import { lifecycle as componentLifecycle } from './status/index.ts';
39
39
  import { DEFAULT_CONFIG } from './DEFAULT_CONFIG.ts';
40
40
  import { PluginModule } from './PluginModule.ts';
41
41
  import { getEnvBuiltInComponents } from './Application.ts';
42
+ import { pathToFileURL } from 'node:url';
42
43
 
43
44
  const CF_ROUTES_DIR = getConfigPath(CONFIG_PARAMS.COMPONENTSROOT);
44
45
  let loadedComponents = new Map<any, any>();
@@ -360,7 +361,9 @@ export async function loadComponent(
360
361
  const plugin = TRUSTED_RESOURCE_PLUGINS[componentName];
361
362
  extensionModule =
362
363
  typeof plugin === 'string'
363
- ? await import(plugin.startsWith('@/') ? join(PACKAGE_ROOT, plugin.slice(1)) : plugin)
364
+ ? await import(
365
+ plugin.startsWith('@/') ? pathToFileURL(join(PACKAGE_ROOT, plugin.slice(1))).toString() : plugin
366
+ )
364
367
  : plugin;
365
368
  }
366
369
 
@@ -35,6 +35,7 @@ export type CommitOptions = {
35
35
  timestamp?: number;
36
36
  retries?: number;
37
37
  flush?: boolean;
38
+ transaction?: RocksTransaction;
38
39
  };
39
40
 
40
41
  type ReadTransaction = (LMDBTransaction | RocksTransaction) & {
@@ -116,8 +117,13 @@ export class DatabaseTransaction implements Transaction {
116
117
  if (!this.transaction) return;
117
118
  if (--this.readTxnsUsed === 0) {
118
119
  trackedTxns.delete(this);
119
- this.transaction?.abort();
120
- this.transaction = null;
120
+ if (this.open === TRANSACTION_STATE.LINGERING) {
121
+ // if we have lingering writes, we have to call commit to finish them
122
+ this.commit();
123
+ } else {
124
+ this.transaction?.abort();
125
+ this.transaction = null;
126
+ }
121
127
  }
122
128
  }
123
129
 
@@ -139,9 +145,6 @@ export class DatabaseTransaction implements Transaction {
139
145
  }
140
146
 
141
147
  addWrite(operation: TransactionWrite) {
142
- if (this.open === TRANSACTION_STATE.CLOSED) {
143
- throw new Error('Can not use a transaction that is no longer open');
144
- }
145
148
  this.writes.push(operation);
146
149
  if (!operation.deferSave) {
147
150
  // Setting saved to false means to defer saving
@@ -150,61 +153,94 @@ export class DatabaseTransaction implements Transaction {
150
153
  return operation;
151
154
  }
152
155
 
153
- save(operation: TransactionWrite, reloadEntry = false) {
156
+ save(operation: TransactionWrite, transaction?: RocksTransaction, reloadEntry = false) {
154
157
  let txnTime = this.timestamp;
155
- if (!this.transaction) {
156
- this.transaction = new RocksTransaction(this.db.store as RocksStore);
158
+ transaction ??= this.transaction;
159
+ let immediateCommit = false;
160
+ if (!transaction) {
161
+ transaction = new RocksTransaction(this.db.store as RocksStore);
162
+ if (this.open === TRANSACTION_STATE.OPEN) {
163
+ this.transaction = transaction;
164
+ } else {
165
+ // if it is closed, we have to immediately commit, using our immediate transaction
166
+ immediateCommit = true;
167
+ }
157
168
  if (txnTime) {
158
- this.transaction.setTimestamp(txnTime);
169
+ transaction.setTimestamp(txnTime);
159
170
  }
160
171
  }
161
172
  if (this.retries > 0) {
162
173
  // this is marks the rocks transaction as a retry so we don't write the transaction log again
163
- this.transaction.isRetry = true;
174
+ transaction.isRetry = true;
164
175
  }
165
- if (!txnTime) txnTime = this.timestamp = this.transaction.getTimestamp();
176
+ if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
166
177
  if (reloadEntry || operation.entry === undefined) {
167
- operation.entry = operation.store.getEntry(operation.key, { transaction: this.transaction });
178
+ operation.entry = operation.store.getEntry(operation.key, { transaction });
179
+ }
180
+ if (!operation.saved) {
181
+ operation.saved = true;
182
+ // immediately execute in this transaction
183
+ if (operation.validate?.(txnTime) === false) {
184
+ operation.commit = () => {}; // noop if we try again
185
+ return;
186
+ }
187
+ let result: Promise<void> = operation.before?.() as Promise<void>;
188
+ if (result?.then) this.completions.push(result);
189
+ result = operation.beforeIntermediate?.() as Promise<void>;
190
+ if (result?.then) this.completions.push(result);
191
+ }
192
+ operation.commit(txnTime, operation.entry, this.retries > 0, transaction);
193
+ if (immediateCommit) {
194
+ return this.commit({ transaction }); // immediately commit if the harper transaction is closed
168
195
  }
169
- operation.saved = true;
170
- // immediately execute in this transaction
171
- if (operation.validate?.(txnTime) === false) return;
172
- let result: Promise<void> = operation.before?.() as Promise<void>;
173
- if (result?.then) this.completions.push(result);
174
- result = operation.beforeIntermediate?.() as Promise<void>;
175
- if (result?.then) this.completions.push(result);
176
- operation.commit(txnTime, operation.entry, this.retries > 0, this.transaction);
177
196
  }
178
197
 
179
198
  /**
180
199
  * Resolves with information on the timestamp and success of the commit
181
200
  */
182
201
  commit(options: CommitOptions = {}): MaybePromise<CommitResolution> {
202
+ let transaction = options.transaction ?? this.transaction; // we need to preserve this transaction as we might to resurrect it if we have to retry
183
203
  for (let i = 0; i < this.writes.length; i++) {
184
204
  let operation = this.writes[i];
185
205
  if (this.retries === 0 && operation.saved) continue;
186
- this.save(operation, i < this.validated);
206
+ this.save(operation, transaction, i < this.validated);
187
207
  }
188
208
  this.validated = this.writes.length;
189
- return when(this.completions.length > 0 ? Promise.all(this.completions) : null, () => {
209
+ const completions = this.completions;
210
+ if (completions.length > 0) this.completions = []; // reset
211
+ return when(completions.length > 0 ? Promise.all(completions) : null, () => {
212
+ if (this.writes.length > this.validated) {
213
+ // check just in case we got any more transactions while we were waiting, if so just recursively continue to finish the additional writes now
214
+ return this.commit(options);
215
+ }
216
+ this.open = TRANSACTION_STATE.CLOSED;
190
217
  let commitResolution: MaybePromise<void>;
191
218
  if (--this.readTxnsUsed > 0) {
192
219
  // we still have outstanding iterators using the transaction, we can't just commit/abort it, we will still
193
220
  // need to use it
221
+ if (this.writes.length > 0) {
222
+ // if there are outstanding writes, we have to call commit later to finish them
223
+ this.open = TRANSACTION_STATE.LINGERING;
224
+ /* TODO: This is not really the intended behavior though, we want to immediately commit writes, but continue to use
225
+ * the transaction, as there is likely existing references to the transaction in other parts of the codebase,
226
+ * particularly in the query iterator */
227
+ }
228
+ /*
194
229
  commitResolution =
195
230
  this.writes.length > 0
196
- ? this.transaction?.commit({ renewAfterCommit: true /* Try to use RocksDB's CommitAndTryCreateSnapshot */ })
197
- : // don't abort, we still have outstanding reads to complete
231
+ ? transaction?.commit({ renewAfterCommit: true }) // Try to use RocksDB's CommitAndTryCreateSnapshot
232
+ : // don't abort, we still have outstanding reads to complete
198
233
  null;
234
+ */
199
235
  } else {
200
236
  // no more reads need to be performed, just commit/abort based if there are any writes
201
237
  trackedTxns.delete(this);
202
- if (this.transaction) {
238
+ this.transaction = null; // clear transaction so any further operations operate immediately
239
+ if (transaction) {
203
240
  if (this.writes.length > 0) {
204
- commitResolution = this.transaction.commit();
241
+ commitResolution = transaction.commit();
205
242
  } else {
206
- commitResolution = this.transaction.abort();
207
- this.transaction = null; // immediately clear transaction, no need to wait
243
+ commitResolution = transaction.abort();
208
244
  }
209
245
  }
210
246
  }
@@ -220,8 +256,7 @@ export class DatabaseTransaction implements Transaction {
220
256
  const completions = [];
221
257
  return commitResolution.then(
222
258
  () => {
223
- this.transaction.onCommit?.();
224
- this.transaction = null; // the native transaction is done (reset if needed)
259
+ transaction.onCommit?.();
225
260
  if (this.next) {
226
261
  completions.push(this.next.commit(options));
227
262
  }
@@ -260,7 +295,7 @@ export class DatabaseTransaction implements Transaction {
260
295
  // if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
261
296
  // for future transactions
262
297
  this.retries++;
263
- return this.commit(options); // try again
298
+ return this.commit({ transaction }); // try again
264
299
  } else throw error;
265
300
  }
266
301
  );
@@ -318,7 +353,7 @@ export class ImmediateTransaction extends DatabaseTransaction {
318
353
  save(transaction: ImmediateTransaction) {
319
354
  if (this.isCommitting) {
320
355
  // if we are in the commit, do the save and force a reload so we get a read within the transaction
321
- super.save(transaction, true);
356
+ super.save(transaction, null, true);
322
357
  } else {
323
358
  this.isCommitting = true;
324
359
  return when(this.commit(), () => {
@@ -580,10 +580,16 @@ export function makeTable(options) {
580
580
  // dictates not to go to source
581
581
  if (!this.doesExist()) throw new ServerError('Entry is not cached', 504);
582
582
  } else if (resourceOptions?.ensureLoaded) {
583
- const loadingFromSource = ensureLoadedFromSource(this.constructor.source, id, entry, request, this);
583
+ const loadingFromSource = ensureLoadedFromSource(
584
+ this.constructor.source,
585
+ id,
586
+ entry,
587
+ request,
588
+ this,
589
+ target
590
+ );
584
591
  if (loadingFromSource) {
585
592
  txn?.disregardReadTxn(); // this could take some time, so don't keep the transaction open if possible
586
- target.loadedFromSource = true;
587
593
  return when(loadingFromSource, (entry) => {
588
594
  TableResource._updateResource(this, entry);
589
595
  return this;
@@ -988,10 +994,16 @@ export function makeTable(options) {
988
994
  // dictates not to go to source
989
995
  if (!entry?.value) throw new ServerError('Entry is not cached', 504);
990
996
  } else if (ensureLoaded) {
991
- const loadingFromSource = ensureLoadedFromSource(constructor.source, id, entry, context, this);
997
+ const loadingFromSource = ensureLoadedFromSource(
998
+ constructor.source,
999
+ id,
1000
+ entry,
1001
+ context,
1002
+ this,
1003
+ target
1004
+ );
992
1005
  if (loadingFromSource) {
993
1006
  txn?.disregardReadTxn(); // this could take some time, so don't keep the transaction open if possible
994
- target.loadedFromSource = true;
995
1007
  return loadingFromSource.then((entry) => entry?.value);
996
1008
  }
997
1009
  }
@@ -3732,7 +3744,7 @@ export function makeTable(options) {
3732
3744
  }
3733
3745
  }
3734
3746
 
3735
- function ensureLoadedFromSource(source: typeof TableResource, id, entry, context, resource?) {
3747
+ function ensureLoadedFromSource(source: typeof TableResource, id, entry, context, resource?, target?) {
3736
3748
  if (hasSourceGet) {
3737
3749
  let needsSourceData = false;
3738
3750
  if (context.noCache) needsSourceData = true;
@@ -3751,7 +3763,7 @@ export function makeTable(options) {
3751
3763
  recordActionBinary(!needsSourceData, 'cache-hit', tableName);
3752
3764
  }
3753
3765
  if (needsSourceData) {
3754
- const loadingFromSource = getFromSource(source, id, entry, context).then((entry) => {
3766
+ const loadingFromSource = getFromSource(source, id, entry, context, target).then((entry) => {
3755
3767
  if (entry?.value && entry?.value.getRecord?.())
3756
3768
  logger.error?.('Can not assign a record that is already a resource');
3757
3769
  if (context) {
@@ -3921,7 +3933,8 @@ export function makeTable(options) {
3921
3933
  source: typeof TableResource,
3922
3934
  id: Id,
3923
3935
  existingEntry: Entry,
3924
- context: Context
3936
+ context: Context,
3937
+ target?
3925
3938
  ): Promise<Entry> {
3926
3939
  const metadataFlags = existingEntry?.metadataFlags;
3927
3940
 
@@ -3943,9 +3956,13 @@ export function makeTable(options) {
3943
3956
  entry.metadataFlags & (INVALIDATED | EVICTED) ||
3944
3957
  (entry.expiresAt != undefined && entry.expiresAt < Date.now())
3945
3958
  )
3946
- // try again
3947
- whenResolved(getFromSource(source, id, primaryStore.getEntry(id), context));
3948
- else whenResolved(entry);
3959
+ // try again — entry still not valid, need to actually fetch from source
3960
+ whenResolved(getFromSource(source, id, primaryStore.getEntry(id), context, target));
3961
+ else {
3962
+ // served from cache after waiting for another request to resolve
3963
+ if (target) target.loadedFromSource = false;
3964
+ whenResolved(entry);
3965
+ }
3949
3966
  };
3950
3967
  const lockAcquired = primaryStore.tryLock(id, callback);
3951
3968
 
@@ -3957,6 +3974,8 @@ export function makeTable(options) {
3957
3974
  }, LOCK_TIMEOUT);
3958
3975
  });
3959
3976
  }
3977
+ // lock acquired — this request will actually load from source
3978
+ if (target) target.loadedFromSource = true;
3960
3979
 
3961
3980
  const existingRecord = existingEntry?.value;
3962
3981
  // it is important to remember that this is _NOT_ part of the current transaction; nothing is changing
@@ -56,6 +56,9 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
56
56
  }
57
57
  overridableProperty(Promise.prototype, 'then');
58
58
  overridableProperty(Date, 'now');
59
+ for (let name of ['get', 'set', 'has', 'delete', 'clear', 'forEach', 'entries', 'keys', 'values']) {
60
+ overridableProperty(Map.prototype, name);
61
+ }
59
62
  for (let Intrinsic of [
60
63
  Object,
61
64
  Array,
@@ -121,12 +124,13 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
121
124
 
122
125
  let amaro: typeof import('amaro') | undefined;
123
126
  /**
124
- * Strip TypeScript types using the amaro library (what Node.js uses internally)
125
- * Falls back to regex-based stripping if amaro is not available
127
+ * Strip TypeScript types synchronously using the amaro library (what Node.js uses internally)
126
128
  */
127
- async function stripTypeScriptTypes(source: string): Promise<string> {
129
+ function stripTypeScriptTypes(source: string): string {
128
130
  // Use amaro - the library that Node.js uses internally for type stripping
129
- amaro = await import('amaro');
131
+ if (!amaro) {
132
+ amaro = require('amaro');
133
+ }
130
134
  return amaro.transformSync(source, { mode: 'strip-only' }).code;
131
135
  }
132
136
 
@@ -146,13 +150,27 @@ function parseJsonModule(source: string, url: string): any {
146
150
  * Load a module using Node's vm.Module API with (not really secure) sandboxing
147
151
  */
148
152
  async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
149
- const moduleCache = new Map<string, Promise<SourceTextModule | SyntheticModule>>();
150
- const linkingPromises = new Map<string, Promise<void>>();
151
- const cjsCache = new Map<string, { exports: any }>();
152
-
153
- // Create a secure context with limited globals
154
- const contextObject = getGlobalObject(scope);
155
- const context = createContext(contextObject);
153
+ // we want to retain the same module caches across any loading with the application scope
154
+ let moduleCaches = scope.moduleCache as {
155
+ moduleCache: Map<string, SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule>>;
156
+ linkingPromises: Map<string, Promise<void>>;
157
+ cjsCache: Map<string, { exports: any }>;
158
+ contextObject: any;
159
+ context: any;
160
+ };
161
+ if (!moduleCaches) {
162
+ // if they haven't been initialized, do so now
163
+ const contextObject = getGlobalObject(scope, true);
164
+ moduleCaches = scope.moduleCache = {
165
+ moduleCache: new Map(),
166
+ linkingPromises: new Map(),
167
+ cjsCache: new Map(),
168
+ // Create a secure context with limited globals
169
+ contextObject,
170
+ context: createContext(contextObject),
171
+ };
172
+ }
173
+ const { moduleCache, linkingPromises, cjsCache, contextObject, context } = moduleCaches;
156
174
 
157
175
  /**
158
176
  * Resolve module specifier to absolute URL
@@ -298,9 +316,11 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
298
316
  }
299
317
 
300
318
  /**
301
- * Linker function for module resolution during instantiation
319
+ * Linker function for module resolution during instantiation.
320
+ * This is synchronous because Node's module.link() requires the linker
321
+ * to return modules synchronously.
302
322
  */
303
- async function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
323
+ function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
304
324
  const resolvedUrl = resolveModule(specifier, referencingModule.identifier);
305
325
 
306
326
  // Determine if we should use VM containment for this module
@@ -316,15 +336,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
316
336
  }
317
337
  }
318
338
 
319
- // Return the module immediately (even if not yet linked) to support circular dependencies
320
- return await getOrCreateModule(resolvedUrl, useContainment);
339
+ // Return the module
340
+ return getOrCreateModule(resolvedUrl, useContainment);
321
341
  }
322
342
 
323
- async function getOrCreateModule(
343
+ function getOrCreateModule(
324
344
  url: string,
325
345
  usePrivateGlobal: boolean
326
- ): Promise<SourceTextModule | SyntheticModule> {
327
- // Check cache first - return cached module immediately (even if not linked yet)
346
+ ): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
347
+ // Check if module is already created
328
348
  if (moduleCache.has(url)) {
329
349
  return moduleCache.get(url)!;
330
350
  }
@@ -345,8 +365,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
345
365
  // Only link/evaluate once per module
346
366
  if (!linkingPromises.has(url)) {
347
367
  const linkingPromise = (async () => {
348
- await module.link(linker);
349
- await module.evaluate();
368
+ // Check module status - only link if it's 'unlinked'
369
+ // Status can be: 'unlinked', 'linking', 'linked', 'evaluating', 'evaluated'
370
+ if (module.status === 'unlinked') {
371
+ await module.link(linker);
372
+ }
373
+ // Only evaluate if not already evaluated
374
+ if (module.status === 'linked') {
375
+ await module.evaluate();
376
+ }
350
377
  })();
351
378
  linkingPromises.set(url, linkingPromise);
352
379
  }
@@ -357,94 +384,107 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
357
384
  return module;
358
385
  }
359
386
  /**
360
- * Create a module from URL without linking or evaluating
387
+ * Create a SyntheticModule from exported object
361
388
  */
362
- async function createModule(url: string, usePrivateGlobal: boolean): Promise<SourceTextModule | SyntheticModule> {
363
- let module: SourceTextModule | SyntheticModule;
364
-
365
- // Handle special built-in modules
366
- if (url === 'harper') {
367
- let harperExports = getHarperExports(scope);
368
- module = new SyntheticModule(
369
- Object.keys(harperExports),
370
- function () {
371
- for (let key in harperExports) {
372
- this.setExport(key, harperExports[key]);
373
- }
374
- },
375
- { identifier: url, context }
376
- );
377
- } else if (url.startsWith('file://') && usePrivateGlobal) {
378
- checkAllowedModulePath(url, scope.verifyPath);
379
- let source = await readFile(new URL(url), { encoding: 'utf-8' });
380
-
381
- // Handle JSON modules as a SyntheticModule with a default export.
382
- // JSON imports only support default exports per the ESM spec.
383
- if (url.endsWith('.json')) {
384
- const jsonData = parseJsonModule(source, url);
385
- module = new SyntheticModule(
386
- ['default'],
387
- function () {
388
- this.setExport('default', jsonData);
389
- },
390
- { identifier: url, context }
391
- );
392
- } else {
393
- // Strip TypeScript types if this is a .ts file
394
- if (url.endsWith('.ts') || url.endsWith('.tsx')) {
395
- source = await stripTypeScriptTypes(source);
389
+ function createSyntheticModule(url: string, exportedObject: any): SyntheticModule {
390
+ const exportNames = Object.keys(exportedObject);
391
+ return new SyntheticModule(
392
+ exportNames,
393
+ function () {
394
+ for (const key of exportNames) {
395
+ this.setExport(key, exportedObject[key]);
396
396
  }
397
+ },
398
+ { identifier: url, context }
399
+ );
400
+ }
397
401
 
398
- // Try CJS first since it will fail fast with clear syntax errors on ESM syntax
399
- try {
400
- module = loadCJSModule(url, source, usePrivateGlobal);
401
- } catch {
402
- // If CJS loading fails (likely due to ESM syntax like import/export), try ESM
403
- try {
404
- module = new SourceTextModule(source, {
405
- identifier: url,
406
- context,
407
- initializeImportMeta(meta) {
408
- meta.url = url;
409
- },
410
- async importModuleDynamically(specifier: string) {
411
- const resolvedUrl = resolveModule(specifier, url);
412
- const dynamicModule = await loadModuleWithCache(resolvedUrl, true);
413
- return dynamicModule;
414
- },
415
- });
416
- } catch (esmErr) {
417
- // Both failed - throw the ESM error as it's likely more relevant
418
- throw esmErr;
419
- }
420
- }
421
- }
422
- } else {
423
- const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
424
- // For Node.js built-in modules (node:) and npm packages without dependency containment
425
- // Always try require first to properly handle CJS modules with named exports
426
- // Fall back to dynamic import for ESM packages
427
- let importedModule = replacedModule ?? (await import(url));
428
- const cjsModule = importedModule['module.exports'];
429
- if (cjsModule) {
430
- // back-compat import
431
- importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
432
- }
433
- const exportNames = Object.keys(importedModule);
434
- module = new SyntheticModule(
435
- exportNames,
402
+ /**
403
+ * Normalize imported module to ensure it has proper exports including default
404
+ */
405
+ function normalizeImportedModule(importedModule: any): any {
406
+ const cjsModule = importedModule['module.exports'];
407
+ if (cjsModule) {
408
+ // back-compat import
409
+ importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
410
+ }
411
+ // Ensure there's a default export for ESM imports that expect it
412
+ if (!importedModule.default) {
413
+ importedModule = { default: importedModule, ...importedModule };
414
+ }
415
+ return importedModule;
416
+ }
417
+
418
+ /**
419
+ * Create a SourceTextModule or SyntheticModule from source code
420
+ */
421
+ function createModuleFromSource(
422
+ url: string,
423
+ source: string,
424
+ usePrivateGlobal: boolean
425
+ ): SourceTextModule | SyntheticModule {
426
+ // Handle JSON modules
427
+ if (url.endsWith('.json')) {
428
+ const jsonData = parseJsonModule(source, url);
429
+ return new SyntheticModule(
430
+ ['default'],
436
431
  function () {
437
- for (const key of exportNames) {
438
- this.setExport(key, importedModule[key]);
439
- }
432
+ this.setExport('default', jsonData);
440
433
  },
441
434
  { identifier: url, context }
442
435
  );
443
436
  }
444
437
 
445
- return module;
438
+ // Strip TypeScript types if this is a .ts file
439
+ if (url.endsWith('.ts') || url.endsWith('.tsx')) {
440
+ source = stripTypeScriptTypes(source);
441
+ }
442
+
443
+ // Try CJS first since it will fail fast with clear syntax errors on ESM syntax
444
+ try {
445
+ return loadCJSModule(url, source, usePrivateGlobal);
446
+ } catch {
447
+ // If CJS loading fails (likely due to ESM syntax like import/export), try ESM
448
+ return new SourceTextModule(source, {
449
+ identifier: url,
450
+ context,
451
+ initializeImportMeta(meta) {
452
+ meta.url = url;
453
+ },
454
+ importModuleDynamically(specifier: string) {
455
+ const resolvedUrl = resolveModule(specifier, url);
456
+ const useContainment = specifier.startsWith('.') || scope.dependencyContainment !== false;
457
+ return loadModuleWithCache(resolvedUrl, useContainment);
458
+ },
459
+ });
460
+ }
446
461
  }
447
462
 
463
+ /**
464
+ * Create a module from URL without linking or evaluating (async version for initial load)
465
+ */
466
+ function createModule(
467
+ url: string,
468
+ usePrivateGlobal: boolean
469
+ ): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
470
+ // Handle special built-in modules
471
+ if (url === 'harper') {
472
+ return createSyntheticModule(url, getHarperExports(scope));
473
+ }
474
+
475
+ if (url.startsWith('file://') && usePrivateGlobal) {
476
+ checkAllowedModulePath(url, scope.verifyPath);
477
+ const source = readFileSync(new URL(url), { encoding: 'utf-8' });
478
+ return createModuleFromSource(url, source, usePrivateGlobal);
479
+ }
480
+
481
+ // For Node.js built-in modules (node:) and npm packages without dependency containment
482
+ const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
483
+ if (replacedModule) {
484
+ return createSyntheticModule(url, normalizeImportedModule(replacedModule));
485
+ }
486
+ return import(url).then((importedModule) => createSyntheticModule(url, normalizeImportedModule(importedModule)));
487
+ }
448
488
  // Load the entry module
449
489
  const entryModule = await loadModuleWithCache(moduleUrl, true);
450
490
 
@@ -490,8 +530,8 @@ async function getCompartment(scope: ApplicationScope, globals) {
490
530
  },
491
531
  };
492
532
  } else if (moduleSpecifier.startsWith('file:') && !moduleSpecifier.includes('node_modules')) {
493
- const moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
494
- // Handle JSON files in comparttment mode the same way as in VM mode
533
+ let moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
534
+ // Handle JSON files in compartment mode the same way as in VM mode
495
535
  if (moduleSpecifier.endsWith('.json')) {
496
536
  const jsonData = parseJsonModule(moduleText, moduleSpecifier);
497
537
  return {
@@ -502,6 +542,10 @@ async function getCompartment(scope: ApplicationScope, globals) {
502
542
  },
503
543
  };
504
544
  }
545
+ // Strip TypeScript types if this is a .ts file
546
+ if (moduleSpecifier.endsWith('.ts') || moduleSpecifier.endsWith('.tsx')) {
547
+ moduleText = stripTypeScriptTypes(moduleText);
548
+ }
505
549
  return new StaticModuleRecord(moduleText, moduleSpecifier);
506
550
  } else {
507
551
  checkAllowedModulePath(moduleSpecifier, scope.verifyPath);
@@ -536,6 +580,9 @@ function secureOnlyFetch(resource, options) {
536
580
  return fetch(resource, options);
537
581
  }
538
582
 
583
+ // These globals need to match the literals produced in the VM context
584
+ const contextualizedJSGlobals = ['Object', 'Array', 'Function', 'globalThis'];
585
+
539
586
  let defaultJSGlobalNames: string[];
540
587
  // get the global variable names that are intrinsically present in a VM context (so we don't override them)
541
588
  function getDefaultJSGlobalNames() {
@@ -551,12 +598,13 @@ function getDefaultJSGlobalNames() {
551
598
  /**
552
599
  * Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
553
600
  */
554
- function getGlobalObject(scope: ApplicationScope) {
601
+ function getGlobalObject(scope: ApplicationScope, copyIntrinsics = false) {
555
602
  const appGlobal = {};
556
603
  // create the new global object, assigning all the global variables from this global
557
604
  // except those that will be natural intrinsics of the new VM
605
+ const globalsToExclude = copyIntrinsics ? contextualizedJSGlobals : getDefaultJSGlobalNames();
558
606
  for (let name of Object.getOwnPropertyNames(global)) {
559
- if (getDefaultJSGlobalNames().includes(name)) continue;
607
+ if (globalsToExclude.includes(name)) continue;
560
608
  appGlobal[name] = global[name];
561
609
  }
562
610
  // now assign Harper scope-specific variables