Repository navigation
Expand file tree
/
Copy pathmodule_runner.rs
More file actions
457 lines (407 loc) · 18.6 KB
/
Copy pathmodule_runner.rs
File metadata and controls
457 lines (407 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! ES modules for napi engines without a module loader reachable through their shim (QuickJS,
//! Hermes, JSC; Hermes can't parse `import`/`export` at all). Each module is transformed with oxc's
//! `ModuleRunnerTransform` (Vite's ssrTransform) into an async function whose imports, exports,
//! `import.meta` and `import()` go through runtime calls, and a JS module runner (`RUNNER`) keeps
//! the registry, links imports, resolves cycles and settles dynamic imports. Exports are getters,
//! hoisted above the imports, so live bindings hold through import cycles.
//!
//! Resolution, source loading (files, the sealed app.nsbundle, HTTP dev-server modules, the
//! `ns:module` builtin) and error text come from `crate::esm_loader`, as for the V8 engines, so a
//! graph resolves the same way on every engine. The engine supplies only its script evaluator.
//! Top-level await is supported (every module body is async); `import()` from a classic script is
//! not, since that syntax never reaches the transform.
use std::cell::Cell;
use napi::{sys, CallContext, Env, JsObject, JsUnknown, NapiRaw, NapiValue, Status, ValueType};
use crate::esm_loader;
/// Evaluate `code` as a classic script named `filename` and return its completion value. On
/// failure the engine's exception is left pending.
pub type EvalScript = fn(env: &Env, code: &str, filename: &str) -> Result<sys::napi_value, ()>;
thread_local! {
static EVAL: Cell<Option<EvalScript>> = const { Cell::new(None) };
}
/// The runner's module body parameters, in the order `RUNNER` passes them.
const WRAPPER_HEAD: &str = "(async function (__vite_ssr_exports__, __vite_ssr_import_meta__, \
__vite_ssr_import__, __vite_ssr_dynamic_import__, __vite_ssr_exportAll__, __vite_ssr_exportName__) \
{\"use strict\";\n";
const WRAPPER_TAIL: &str = "\n})";
/// A transformed module: the wrapper function's source and the specifiers it imports statically.
pub struct Transformed {
pub code: String,
pub deps: Vec<String>,
}
fn line_col(source: &str, offset: usize) -> (usize, usize) {
let before = &source[..offset.min(source.len())];
let line = before.matches('\n').count() + 1;
let col = before.rfind('\n').map_or(before.len(), |i| before.len() - i - 1) + 1;
(line, col)
}
/// Transform an ES module into the runner's async wrapper. `Err` carries a SyntaxError message
/// with the first error's location.
pub fn transform(source: &str, key: &str) -> Result<Transformed, String> {
use oxc::allocator::Allocator;
use oxc::codegen::Codegen;
use oxc::parser::Parser;
use oxc::semantic::SemanticBuilder;
use oxc::span::SourceType;
use oxc_transformer_plugins::ModuleRunnerTransform;
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::mjs()).parse();
if let Some(error) = parsed.diagnostics.first() {
let at = error
.labels
.as_ref()
.first()
.map(|label| line_col(source, label.offset() as usize));
return Err(match at {
Some((line, col)) => format!("{} ({key}:{line}:{col})", error.message),
None => format!("{} ({key})", error.message),
});
}
let mut program = parsed.program;
let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
let (deps, _dynamic) = ModuleRunnerTransform::new().transform(&allocator, &mut program, scoping);
let body = Codegen::new().build(&program).code;
let mut deps: Vec<String> = deps.into_iter().collect();
deps.sort();
Ok(Transformed { code: format!("{WRAPPER_HEAD}{body}{WRAPPER_TAIL}"), deps })
}
fn arg_string(ctx: &CallContext, index: usize) -> napi::Result<Option<String>> {
if ctx.length <= index {
return Ok(None);
}
let v = ctx.get::<JsUnknown>(index)?;
if matches!(v.get_type()?, ValueType::Undefined | ValueType::Null) {
return Ok(None);
}
Ok(Some(v.coerce_to_string()?.into_utf8()?.as_str()?.to_owned()))
}
fn arg_strings(ctx: &CallContext, index: usize) -> napi::Result<Vec<String>> {
let mut out = Vec::new();
if ctx.length <= index {
return Ok(out);
}
let list = ctx.get::<JsUnknown>(index)?;
if !list.is_array()? {
return Ok(out);
}
let list: JsObject = unsafe { list.cast() };
for i in 0..list.get_array_length()? {
let item = list.get_element::<JsUnknown>(i)?;
out.push(item.coerce_to_string()?.into_utf8()?.as_str()?.to_owned());
}
Ok(out)
}
/// Throw a real `SyntaxError` (napi8 has no constructor for one) and return the pending marker.
fn throw_syntax_error(env: &Env, message: &str) -> napi::Error {
let thrown = (|| -> napi::Result<()> {
let ctor: napi::JsFunction = env.get_global()?.get_named_property("SyntaxError")?;
let error = ctor.new_instance(&[env.create_string(message)?])?;
env.throw(error)
})();
match thrown {
Ok(()) => napi::Error::new(Status::PendingException, String::new()),
Err(_) => napi::Error::from_reason(message.to_string()),
}
}
/// String coercion of the pending exception, cleared.
fn take_exception(env: &Env) -> String {
let mut exc: sys::napi_value = std::ptr::null_mut();
if unsafe { sys::napi_get_and_clear_last_exception(env.raw(), &mut exc) } != sys::Status::napi_ok
|| exc.is_null()
{
return "<no exception>".into();
}
unsafe { JsUnknown::from_raw_unchecked(env.raw(), exc) }
.coerce_to_string()
.and_then(|s| s.into_utf8())
.and_then(|s| Ok(s.as_str()?.to_owned()))
.unwrap_or_else(|_| "<unprintable exception>".into())
}
/// `__nsEsmLoad(key, spec, referrer) → { fn, deps }`: read, transform and evaluate a module's
/// wrapper. Throws the not-found reason, a SyntaxError, or the engine's own evaluation error.
fn load(ctx: CallContext) -> napi::Result<JsObject> {
let env = &ctx.env;
let key = arg_string(&ctx, 0)?.unwrap_or_default();
let Some(source) = esm_loader::read_source(&key) else {
let spec = arg_string(&ctx, 1)?.unwrap_or_else(|| key.clone());
let referrer = arg_string(&ctx, 2)?;
return Err(napi::Error::from_reason(esm_loader::not_found_message(
&spec,
referrer.as_deref(),
&key,
)));
};
let transformed = transform(&source, &key).map_err(|e| throw_syntax_error(env, &e))?;
let Some(eval) = EVAL.with(Cell::get) else {
return Err(napi::Error::from_reason("module runner is not installed"));
};
let function = eval(env, &transformed.code, &key)
.map_err(|()| napi::Error::new(Status::PendingException, String::new()))?;
let mut deps = env.create_array_with_length(transformed.deps.len())?;
for (i, dep) in transformed.deps.iter().enumerate() {
deps.set_element(i as u32, env.create_string(dep)?)?;
}
let mut out = env.create_object()?;
out.set_named_property("fn", unsafe { JsUnknown::from_raw_unchecked(env.raw(), function) })?;
out.set_named_property("deps", deps)?;
Ok(out)
}
/// Install the runner's natives and the runner itself, then `ns:module`'s natives over it. Call
/// after `host_abi::initialize_runtime` and before the shared prelude (which builds
/// `__nsModuleBuiltin` from `__nsModuleConfigureLoader`).
pub fn install(env: &Env, eval: EvalScript) -> napi::Result<()> {
EVAL.with(|e| e.set(Some(eval)));
let mut global = env.get_global()?;
crate::napi_engine::module_natives::install_script_compiler(env, eval)?;
let load_fn = env.create_function_from_closure("__nsEsmLoad", load)?;
global.set_named_property("__nsEsmLoad", load_fn)?;
let resolve = env.create_function_from_closure("__nsEsmResolve", |ctx: CallContext| {
let spec = arg_string(&ctx, 0)?.unwrap_or_default();
let referrer = arg_string(&ctx, 1)?;
ctx.env.create_string(&esm_loader::resolve(&spec, referrer.as_deref()))
})?;
global.set_named_property("__nsEsmResolve", resolve)?;
let prefetch = env.create_function_from_closure("__nsEsmPrefetch", |ctx: CallContext| {
esm_loader::prefetch(&arg_strings(&ctx, 0)?);
ctx.env.get_undefined()
})?;
global.set_named_property("__nsEsmPrefetch", prefetch)?;
let volatile = env.create_function_from_closure("__nsEsmIsVolatile", |ctx: CallContext| {
let key = arg_string(&ctx, 0)?.unwrap_or_default();
ctx.env.get_boolean(esm_loader::is_volatile(&key))
})?;
global.set_named_property("__nsEsmIsVolatile", volatile)?;
let registry_key = env.create_function_from_closure("__nsEsmRegistryKey", |ctx: CallContext| {
let url = arg_string(&ctx, 0)?.unwrap_or_default();
ctx.env.create_string(&esm_loader::registry_key_for(&url))
})?;
global.set_named_property("__nsEsmRegistryKey", registry_key)?;
// Registry keys the runner dropped: forget their load failures and prefetched bodies, and
// cache-bust the next fetch of each HTTP one.
let forget = env.create_function_from_closure("__nsEsmForget", |ctx: CallContext| {
let keys = arg_strings(&ctx, 0)?;
for key in &keys {
esm_loader::forget(key);
}
let http: Vec<String> = keys.into_iter().filter(|k| esm_loader::is_http(k)).collect();
esm_loader::mark_keys_for_cache_bust(&http);
ctx.env.get_undefined()
})?;
global.set_named_property("__nsEsmForget", forget)?;
let report = env.create_function_from_closure("__nsEsmReportError", |ctx: CallContext| {
let report = arg_string(&ctx, 0)?.unwrap_or_default();
crate::debug_output(&format!("[NativeScript] Uncaught error evaluating module: {report}\n"));
crate::store_last_js_error(report);
ctx.env.get_undefined()
})?;
global.set_named_property("__nsEsmReportError", report)?;
let configure = env.create_function_from_closure("__nsModuleConfigureLoader", |ctx: CallContext| {
let json = arg_string(&ctx, 0)?.unwrap_or_default();
esm_loader::configure_loader(&json).map_err(napi::Error::from_reason)?;
ctx.env.get_undefined()
})?;
global.set_named_property("__nsModuleConfigureLoader", configure)?;
let boot_complete = env.create_function_from_closure("__nsModuleSetDevBootComplete", |ctx: CallContext| {
esm_loader::set_dev_boot_complete(ctx.get::<JsUnknown>(0)?.coerce_to_bool()?.get_value()?);
ctx.env.get_undefined()
})?;
global.set_named_property("__nsModuleSetDevBootComplete", boot_complete)?;
eval(env, RUNNER, "<module-runner>").map_err(|()| {
napi::Error::from_reason(format!("module runner failed to install: {}", take_exception(env)))
})?;
Ok(())
}
/// Run the entry module the host passes to `runtime_runscript` (`filename`, absolute or relative
/// to the app root). Failures are reported through `__nsEsmReportError` when the graph settles.
pub fn run_entry(env: &Env, filename: &str, app_root: &str) -> napi::Result<()> {
let key = esm_loader::entry_key(filename, app_root);
let global = env.get_global()?;
let runner: JsObject = global.get_named_property("__nsEsmRunner")?;
let run_entry: napi::JsFunction = runner.get_named_property("runEntry")?;
run_entry.call(Some(&runner), &[env.create_string(&key)?])?;
Ok(())
}
/// The module runner. Engine-conservative like the prelude (function expressions, no syntax the
/// transform would have to lower), since it's evaluated as a classic script on every engine.
pub const RUNNER: &str = r#"
(function (g) {
'use strict';
if (typeof g.__nsEsmLoad !== 'function' || g.__nsEsmRunner) { return; }
var load = g.__nsEsmLoad;
var resolve = g.__nsEsmResolve;
var prefetch = g.__nsEsmPrefetch;
var isVolatile = g.__nsEsmIsVolatile;
var registryKey = g.__nsEsmRegistryKey;
var forget = g.__nsEsmForget;
var report = g.__nsEsmReportError;
// Registry key -> { key, exports, promise, evaluated, stack }. `stack` is the static import
// chain the module was first reached through, which is how a cycle is recognized.
var registry = new Map();
function newNamespace() {
var ns = Object.create(null);
Object.defineProperty(ns, Symbol.toStringTag, { value: 'Module' });
return ns;
}
// `import.meta`: `url` (a file:/// URL, as bundlers' `new URL(x, import.meta.url)` expect),
// `filename` and `dirname`. A served module's identity is its URL alone.
function importMeta(key) {
if (/^https?:\/\//.test(key) || key === 'ns:module') { return { url: key }; }
var path = key.indexOf('\\\\?\\') === 0 ? key.slice(4) : key;
var sep = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/'));
return {
url: 'file:///' + path.replace(/\\/g, '/'),
filename: path,
dirname: sep < 0 ? '' : path.slice(0, sep)
};
}
function defineExport(exports, name, getter) {
Object.defineProperty(exports, name, { enumerable: true, configurable: true, get: getter });
}
// `export * from`: every name but `default` and the ones the module declares itself.
function exportAll(exports, source) {
for (var name in source) {
if (name !== 'default' && name !== '__esModule' && !(name in exports)) {
defineExport(exports, name, (function (n) { return function () { return source[n]; }; })(name));
}
}
}
function describe(e) {
var text = String(e);
var stack = e && e.stack ? String(e.stack) : '';
if (!stack) { return text; }
return stack.indexOf(text) >= 0 ? stack : text + '\n' + stack;
}
function evict(key) {
var removed = registry.delete(key);
forget([key]);
return removed;
}
// Load and start evaluating `key`. A load failure throws and leaves nothing registered, so a
// later import retries it.
function instantiate(key, spec, referrer, parentStack) {
var loaded = load(key, spec, referrer);
var record = { key: key, exports: newNamespace(), promise: null, evaluated: false, stack: parentStack.concat([key]) };
registry.set(key, record);
// Fetch the dependencies served over HTTP concurrently before the body awaits them in turn.
var deps = [];
for (var i = 0; i < loaded.deps.length; i++) {
var dep = resolve(loaded.deps[i], key);
if (!registry.has(dep) && deps.indexOf(dep) < 0) { deps.push(dep); }
}
if (deps.length > 1) { prefetch(deps); }
var exports = record.exports;
record.promise = loaded.fn.call(
undefined,
exports,
importMeta(key),
function (s) { return importFrom(s, record); },
function (s) { return dynamicImport(s, key); },
function (source) { exportAll(exports, source); },
function (name, getter) { defineExport(exports, name, getter); }
).then(function () {
record.evaluated = true;
return exports;
});
return record;
}
// Static import from `importer`'s body.
function importFrom(spec, importer) {
var key = resolve(spec, importer.key);
var record = registry.get(key);
if (record) {
// Inside an import cycle, take the partially evaluated exports: their getters fill in once
// the module's body runs.
if (record.evaluated || importer.stack.indexOf(key) >= 0) { return record.exports; }
return record.promise;
}
try {
return instantiate(key, spec, importer.key, importer.stack).promise;
} catch (e) {
return Promise.reject(e);
}
}
// `import()`: settles with the namespace once the graph (including any top-level await) has
// evaluated, or rejects with the real error.
function dynamicImport(spec, referrer) {
try {
var key = resolve(spec, referrer);
// Volatile URLs (configureLoader) are re-fetched on every import.
if (isVolatile(key)) { evict(key); }
var record = registry.get(key) || instantiate(key, spec, referrer, []);
return record.promise;
} catch (e) {
return Promise.reject(e);
}
}
function runEntry(key) {
var promise;
try {
promise = (registry.get(key) || instantiate(key, key, null, [])).promise;
} catch (e) {
promise = Promise.reject(e);
}
return promise.then(null, function (e) { report(describe(e)); });
}
// `ns:module`'s `invalidateModules(urls)`: registry eviction plus a one-shot cache-bust nonce on
// each evicted URL's next fetch. Importers already linked keep the old exports; the HMR client
// re-imports the graph above them.
g.__nsModuleInvalidate = function (urls) {
if (!Array.isArray(urls)) { throw new TypeError('invalidateModules expects an array of URL strings'); }
var keys = [];
for (var i = 0; i < urls.length; i++) {
if (typeof urls[i] !== 'string') { throw new TypeError('invalidateModules: urls[' + i + '] must be a string'); }
var key = registryKey(urls[i]);
if (key && keys.indexOf(key) < 0) { keys.push(key); }
}
var removed = 0;
for (var j = 0; j < keys.length; j++) {
if (registry.delete(keys[j])) { removed++; }
}
forget(keys);
return removed;
};
// `ns:module`'s `getLoadedModuleUrls()`: the URL-keyed (served) modules currently registered.
g.__nsModuleLoadedUrls = function () {
var urls = [];
registry.forEach(function (record, key) {
if (key.indexOf('://') >= 0) { urls.push(key); }
});
return urls.sort();
};
Object.defineProperty(g, '__nsEsmRunner', {
value: Object.freeze({ runEntry: runEntry, importModule: dynamicImport }),
configurable: true
});
})(globalThis);
'module-runner-ok'
"#;
#[cfg(test)]
mod tests {
use super::transform;
#[test]
fn exports_are_getters_hoisted_above_awaited_imports() {
let out = transform(
"import d, { a } from './dep.mjs';\nexport let n = a;\nexport default d;\nexport * from './star.mjs';",
"C:/app/main.mjs",
)
.unwrap();
assert_eq!(out.deps, vec!["./dep.mjs".to_string(), "./star.mjs".to_string()]);
assert!(out.code.starts_with("(async function (__vite_ssr_exports__"), "{}", out.code);
let getter = out.code.find("Object.defineProperty(__vite_ssr_exports__, \"n\"").unwrap();
let import = out.code.find("await __vite_ssr_import__(\"./dep.mjs\"").unwrap();
assert!(getter < import, "{}", out.code);
assert!(out.code.contains("__vite_ssr_exportAll__("), "{}", out.code);
}
#[test]
fn import_meta_and_dynamic_import_go_through_the_runner() {
let out = transform("const u = import.meta.url; const m = () => import('./lazy.mjs');", "k").unwrap();
assert!(out.code.contains("__vite_ssr_import_meta__.url"), "{}", out.code);
assert!(out.code.contains("__vite_ssr_dynamic_import__(\"./lazy.mjs\")"), "{}", out.code);
assert!(out.deps.is_empty());
}
#[test]
fn syntax_errors_name_the_module_and_location() {
let err = transform("export const ok = 1;\nexport const = 2;", "C:/app/bad.mjs").err().unwrap();
assert!(err.contains("(C:/app/bad.mjs:2:"), "{err}");
}
}