@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.
- package/core/bin/cliOperations.js +3 -3
- package/core/components/ApplicationScope.ts +1 -0
- package/core/components/componentLoader.ts +4 -1
- package/core/resources/DatabaseTransaction.ts +67 -32
- package/core/resources/Table.ts +29 -10
- package/core/security/jsLoader.ts +149 -101
- package/core/server/REST.ts +12 -2
- package/core/server/http.ts +4 -1
- package/dist/core/bin/cliOperations.js +3 -3
- package/dist/core/bin/cliOperations.js.map +1 -1
- package/dist/core/components/ApplicationScope.js +1 -0
- package/dist/core/components/ApplicationScope.js.map +1 -1
- package/dist/core/components/componentLoader.js +2 -1
- package/dist/core/components/componentLoader.js.map +1 -1
- package/dist/core/resources/DatabaseTransaction.js +71 -35
- package/dist/core/resources/DatabaseTransaction.js.map +1 -1
- package/dist/core/resources/Table.js +15 -10
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/security/jsLoader.js +123 -90
- package/dist/core/security/jsLoader.js.map +1 -1
- package/dist/core/server/REST.js +13 -4
- package/dist/core/server/REST.js.map +1 -1
- package/dist/core/server/http.js +5 -0
- package/dist/core/server/http.js.map +1 -1
- package/dist/licensing/usageLicensing.js +16 -26
- package/dist/licensing/usageLicensing.js.map +1 -1
- package/dist/replication/replicationConnection.js +2 -2
- package/dist/replication/replicationConnection.js.map +1 -1
- package/licensing/usageLicensing.ts +22 -32
- package/npm-shrinkwrap.json +223 -223
- package/package.json +3 -3
- package/replication/replicationConnection.ts +2 -2
- package/studio/web/assets/{index-ClD_q6ya.js → index-CXQsBaYq.js} +5 -5
- package/studio/web/assets/{index-ClD_q6ya.js.map → index-CXQsBaYq.js.map} +1 -1
- package/studio/web/assets/{index.lazy-CXzU1gVu.js → index.lazy-C3Ejfvna.js} +2 -2
- package/studio/web/assets/{index.lazy-CXzU1gVu.js.map → index.lazy-C3Ejfvna.js.map} +1 -1
- package/studio/web/assets/{profile-DCNVg5yY.js → profile-BbbbWJCN.js} +2 -2
- package/studio/web/assets/{profile-DCNVg5yY.js.map → profile-BbbbWJCN.js.map} +1 -1
- package/studio/web/assets/{status-CoGlcjSB.js → status-CFe85l8C.js} +2 -2
- package/studio/web/assets/{status-CoGlcjSB.js.map → status-CFe85l8C.js.map} +1 -1
- 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(
|
|
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:
|
|
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(
|
|
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.
|
|
120
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
transaction.isRetry = true;
|
|
164
175
|
}
|
|
165
|
-
if (!txnTime) txnTime = this.timestamp =
|
|
176
|
+
if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
|
|
166
177
|
if (reloadEntry || operation.entry === undefined) {
|
|
167
|
-
operation.entry = operation.store.getEntry(operation.key, { 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
|
-
|
|
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
|
-
?
|
|
197
|
-
|
|
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
|
-
|
|
238
|
+
this.transaction = null; // clear transaction so any further operations operate immediately
|
|
239
|
+
if (transaction) {
|
|
203
240
|
if (this.writes.length > 0) {
|
|
204
|
-
commitResolution =
|
|
241
|
+
commitResolution = transaction.commit();
|
|
205
242
|
} else {
|
|
206
|
-
commitResolution =
|
|
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
|
-
|
|
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(
|
|
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(), () => {
|
package/core/resources/Table.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
129
|
+
function stripTypeScriptTypes(source: string): string {
|
|
128
130
|
// Use amaro - the library that Node.js uses internally for type stripping
|
|
129
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
320
|
-
return
|
|
339
|
+
// Return the module
|
|
340
|
+
return getOrCreateModule(resolvedUrl, useContainment);
|
|
321
341
|
}
|
|
322
342
|
|
|
323
|
-
|
|
343
|
+
function getOrCreateModule(
|
|
324
344
|
url: string,
|
|
325
345
|
usePrivateGlobal: boolean
|
|
326
|
-
): Promise<SourceTextModule | SyntheticModule> {
|
|
327
|
-
// Check
|
|
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
|
-
|
|
349
|
-
|
|
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
|
|
387
|
+
* Create a SyntheticModule from exported object
|
|
361
388
|
*/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
// Handle JSON files in
|
|
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 (
|
|
607
|
+
if (globalsToExclude.includes(name)) continue;
|
|
560
608
|
appGlobal[name] = global[name];
|
|
561
609
|
}
|
|
562
610
|
// now assign Harper scope-specific variables
|