Skip to content

Commit 94d8530

Browse files
committed
Support bundling node native modules
1 parent 3d91d15 commit 94d8530

File tree

11 files changed

+211
-20
lines changed

11 files changed

+211
-20
lines changed

packages/configs/default/index.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
],
4545
"*.svg": ["@parcel/transformer-svg"],
4646
"*.{xml,rss,atom}": ["@parcel/transformer-xml"],
47+
"*.node": ["@parcel/transformer-node"],
4748
"url:*": ["...", "@parcel/transformer-raw"]
4849
},
4950
"namers": ["@parcel/namer-default"],

packages/configs/default/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@parcel/transformer-image": "2.13.3",
4545
"@parcel/transformer-js": "2.13.3",
4646
"@parcel/transformer-json": "2.13.3",
47+
"@parcel/transformer-node": "2.13.3",
4748
"@parcel/transformer-postcss": "2.13.3",
4849
"@parcel/transformer-posthtml": "2.13.3",
4950
"@parcel/transformer-raw": "2.13.3",

packages/core/integration-tests/test/javascript.js

+48
Original file line numberDiff line numberDiff line change
@@ -6231,4 +6231,52 @@ describe('javascript', function () {
62316231
assert.equal(res.result, 2);
62326232
});
62336233
}
6234+
6235+
for (let defaultTargetOptions of [
6236+
{shouldScopeHoist: false},
6237+
{shouldScopeHoist: true, outputFormat: 'commonjs'},
6238+
{shouldScopeHoist: true, outputFormat: 'esmodule'},
6239+
]) {
6240+
it(
6241+
'supports native .node modules with options: ' +
6242+
JSON.stringify(defaultTargetOptions),
6243+
async function () {
6244+
await fsFixture(overlayFS, __dirname)`
6245+
native-node
6246+
index.js:
6247+
output = require('@parcel/rust');
6248+
6249+
package.json:
6250+
{
6251+
"targets": {
6252+
"default": {
6253+
"context": "node",
6254+
"includeNodeModules": true
6255+
}
6256+
}
6257+
}
6258+
6259+
yarn.lock:`;
6260+
6261+
let b = await bundle(path.join(__dirname, 'native-node/index.js'), {
6262+
defaultTargetOptions,
6263+
inputFS: overlayFS,
6264+
outputFS: inputFS,
6265+
});
6266+
6267+
let res = await run(
6268+
b,
6269+
{output: null},
6270+
{require: false},
6271+
{
6272+
fs: () => require('fs'),
6273+
path: () => require('path'),
6274+
module: () => require('module'),
6275+
url: () => require('url'),
6276+
},
6277+
);
6278+
assert.equal(typeof res.output.hashString, 'function');
6279+
},
6280+
);
6281+
}
62346282
});

packages/core/integration-tests/test/svg.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ describe('svg', function () {
314314

315315
assertBundles(b, [
316316
{
317-
assets: ['index.js', 'bundle-url.js'],
317+
assets: ['index.js'],
318318
},
319319
{
320320
assets: ['circle.svg'],

packages/core/test-utils/src/utils.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@ export async function runBundles(
365365
overlayFS,
366366
externalModules,
367367
true,
368+
target === 'node' ||
369+
target === 'electron-main' ||
370+
target === 'react-server',
368371
);
369372

370373
esmOutput = bundles.length === 1 ? res[0] : res;
@@ -927,6 +930,9 @@ function prepareNodeContext(
927930
readFileSync: (file, encoding) => {
928931
return overlayFS.readFileSync(file, encoding);
929932
},
933+
existsSync: file => {
934+
return overlayFS.existsSync(file);
935+
},
930936
};
931937
}
932938

@@ -939,6 +945,10 @@ function prepareNodeContext(
939945
return {};
940946
}
941947

948+
if (path.extname(res) === '.node') {
949+
return require(res);
950+
}
951+
942952
let cached = nodeCache.get(res);
943953
if (cached) {
944954
return cached.module.exports;
@@ -1002,6 +1012,7 @@ export async function runESM(
10021012
fs: FileSystem,
10031013
externalModules: ExternalModules = {},
10041014
requireExtensions: boolean = false,
1015+
isNode: boolean = false,
10051016
): Promise<Array<{|[string]: mixed|}>> {
10061017
let id = instanceId++;
10071018
let cache = new Map();
@@ -1048,7 +1059,11 @@ export async function runESM(
10481059
entry(specifier, referrer),
10491060
context,
10501061
initializeImportMeta(meta) {
1051-
meta.url = `http://localhost/${path.basename(filename)}`;
1062+
if (isNode) {
1063+
meta.url = url.pathToFileURL(filename).toString();
1064+
} else {
1065+
meta.url = `http://localhost/${path.basename(filename)}`;
1066+
}
10521067
},
10531068
});
10541069
cache.set(filename, m);

packages/runtimes/js/src/JSRuntime.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,23 @@ export default (new Runtime({
193193
.find(e => e.id === bundleGroup.entryAssetId);
194194
if (
195195
dependency.specifierType === 'url' ||
196-
mainBundle.type !== 'js' ||
197196
mainAsset?.meta.jsRuntime === 'url'
198197
) {
199198
assets.push(getURLRuntime(dependency, bundle, mainBundle, options));
199+
continue;
200+
}
201+
202+
if (mainBundle.type === 'node' && mainBundle.env.isNode()) {
203+
let relativePathExpr = getAbsoluteUrlExpr(
204+
getRelativePathExpr(bundle, mainBundle, options),
205+
mainBundle,
206+
);
207+
assets.push({
208+
filePath: __filename,
209+
code: `module.exports = require('./helpers/node/node-loader.js')(${relativePathExpr});`,
210+
dependency,
211+
env: {sourceType: 'module'},
212+
});
200213
}
201214
}
202215

@@ -655,7 +668,8 @@ function getAbsoluteUrlExpr(relativePathExpr: string, bundle: NamedBundle) {
655668
if (
656669
(bundle.env.outputFormat === 'esmodule' &&
657670
bundle.env.supports('import-meta-url')) ||
658-
bundle.env.outputFormat === 'commonjs'
671+
bundle.env.outputFormat === 'commonjs' ||
672+
bundle.env.isNode()
659673
) {
660674
// This will be compiled to new URL(url, import.meta.url) or new URL(url, 'file:' + __filename).
661675
return `new __parcel__URL__(${relativePathExpr}).toString()`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const url = require('url');
2+
const {createRequire} = require('module');
3+
4+
module.exports = function loadNodeModule(bundle) {
5+
let path = url.fileURLToPath(bundle);
6+
let require = createRequire(path);
7+
return require(path);
8+
};

packages/transformers/js/core/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ pub fn transform(
489489
filename: Path::new(&config.filename),
490490
unresolved_mark,
491491
has_node_replacements: &mut result.has_node_replacements,
492+
is_esm: config.is_esm_output,
492493
},
493494
config.node_replacer(),
494495
),

packages/transformers/js/core/src/node_replacer.rs

+84-16
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use std::{collections::HashMap, ffi::OsStr, path::Path};
33
use swc_core::{
44
common::{sync::Lrc, Mark, SourceMap, SyntaxContext, DUMMY_SP},
55
ecma::{
6-
ast,
7-
ast::MemberProp,
6+
ast::{self, MemberProp},
87
atoms::JsWord,
8+
utils::member_expr,
99
visit::{VisitMut, VisitMutWith},
1010
},
1111
};
@@ -26,6 +26,7 @@ pub struct NodeReplacer<'a> {
2626
pub global_mark: Mark,
2727
pub globals: HashMap<JsWord, (SyntaxContext, ast::Stmt)>,
2828
pub filename: &'a Path,
29+
pub is_esm: bool,
2930
pub unresolved_mark: Mark,
3031
/// This will be set to true if the file has either __dirname or __filename replacements inserted
3132
pub has_node_replacements: &'a mut bool,
@@ -50,6 +51,7 @@ impl<'a> VisitMut for NodeReplacer<'a> {
5051
let replace_me_value = swc_core::ecma::atoms::JsWord::from("$parcel$filenameReplace");
5152

5253
let unresolved_mark = self.unresolved_mark;
54+
let is_esm = self.is_esm;
5355
let expr = |this: &NodeReplacer| {
5456
let filename = if let Some(name) = this.filename.file_name() {
5557
name
@@ -63,14 +65,9 @@ impl<'a> VisitMut for NodeReplacer<'a> {
6365
args: vec![
6466
ast::ExprOrSpread {
6567
spread: None,
66-
expr: Box::new(ast::Expr::Ident(ast::Ident {
67-
optional: false,
68-
span: DUMMY_SP,
69-
ctxt: SyntaxContext::empty(),
70-
// This also uses __dirname as later in the path.join call the hierarchy is then correct
71-
// Otherwise path.join(__filename, '..') would be one level to shallow (due to the /filename.js at the end)
72-
sym: swc_core::ecma::atoms::JsWord::from("__dirname"),
73-
})),
68+
// This also uses __dirname as later in the path.join call the hierarchy is then correct
69+
// Otherwise path.join(__filename, '..') would be one level to shallow (due to the /filename.js at the end)
70+
expr: Box::new(dirname(is_esm, unresolved_mark)),
7471
},
7572
ast::ExprOrSpread {
7673
spread: None,
@@ -119,6 +116,7 @@ impl<'a> VisitMut for NodeReplacer<'a> {
119116
let replace_me_value = swc_core::ecma::atoms::JsWord::from("$parcel$dirnameReplace");
120117

121118
let unresolved_mark = self.unresolved_mark;
119+
let is_esm = self.is_esm;
122120
if self.update_binding(id, "$parcel$__dirname".into(), |_| {
123121
Call(ast::CallExpr {
124122
span: DUMMY_SP,
@@ -127,12 +125,7 @@ impl<'a> VisitMut for NodeReplacer<'a> {
127125
args: vec![
128126
ast::ExprOrSpread {
129127
spread: None,
130-
expr: Box::new(ast::Expr::Ident(ast::Ident {
131-
optional: false,
132-
span: DUMMY_SP,
133-
ctxt: SyntaxContext::empty(),
134-
sym: swc_core::ecma::atoms::JsWord::from("__dirname"),
135-
})),
128+
expr: Box::new(dirname(is_esm, unresolved_mark)),
136129
},
137130
ast::ExprOrSpread {
138131
spread: None,
@@ -220,6 +213,43 @@ impl NodeReplacer<'_> {
220213
}
221214
}
222215

216+
fn dirname(is_esm: bool, unresolved_mark: Mark) -> ast::Expr {
217+
if is_esm {
218+
use ast::*;
219+
// require('path').dirname(require('url').fileURLToPath(import.meta.url))
220+
// TODO: use import.meta.dirname if available?
221+
Expr::Call(CallExpr {
222+
callee: Callee::Expr(Box::new(Expr::Member(MemberExpr {
223+
obj: Box::new(Expr::Call(create_require("path".into(), unresolved_mark))),
224+
prop: MemberProp::Ident(IdentName::new("dirname".into(), DUMMY_SP)),
225+
span: DUMMY_SP,
226+
}))),
227+
args: vec![ExprOrSpread {
228+
expr: Box::new(Expr::Call(CallExpr {
229+
callee: Callee::Expr(Box::new(Expr::Member(MemberExpr {
230+
obj: Box::new(Expr::Call(create_require("url".into(), unresolved_mark))),
231+
prop: MemberProp::Ident(IdentName::new("fileURLToPath".into(), DUMMY_SP)),
232+
span: DUMMY_SP,
233+
}))),
234+
args: vec![ExprOrSpread {
235+
expr: Box::new(Expr::Member(member_expr!(
236+
Default::default(),
237+
DUMMY_SP,
238+
import.meta.url
239+
))),
240+
spread: None,
241+
}],
242+
..Default::default()
243+
})),
244+
spread: None,
245+
}],
246+
..Default::default()
247+
})
248+
} else {
249+
ast::Expr::Ident(ast::Ident::new_no_ctxt("__dirname".into(), DUMMY_SP))
250+
}
251+
}
252+
223253
#[cfg(test)]
224254
mod test {
225255
use crate::test_utils::run_visit;
@@ -243,6 +273,7 @@ console.log(__filename);
243273
has_node_replacements: &mut has_node_replacements,
244274
items: &mut items,
245275
unresolved_mark: context.unresolved_mark,
276+
is_esm: false,
246277
})
247278
.output_code;
248279

@@ -277,6 +308,7 @@ console.log(__dirname);
277308
has_node_replacements: &mut has_node_replacements,
278309
items: &mut items,
279310
unresolved_mark: context.unresolved_mark,
311+
is_esm: false,
280312
})
281313
.output_code;
282314

@@ -314,6 +346,7 @@ function something(__filename, __dirname) {
314346
has_node_replacements: &mut has_node_replacements,
315347
items: &mut items,
316348
unresolved_mark: context.unresolved_mark,
349+
is_esm: false,
317350
})
318351
.output_code;
319352

@@ -346,6 +379,7 @@ const filename = obj.__filename;
346379
has_node_replacements: &mut has_node_replacements,
347380
items: &mut items,
348381
unresolved_mark: context.unresolved_mark,
382+
is_esm: false,
349383
})
350384
.output_code;
351385

@@ -374,6 +408,7 @@ const filename = obj[__filename];
374408
has_node_replacements: &mut has_node_replacements,
375409
items: &mut items,
376410
unresolved_mark: context.unresolved_mark,
411+
is_esm: false,
377412
})
378413
.output_code;
379414

@@ -386,4 +421,37 @@ const filename = obj[$parcel$__filename];
386421
assert_eq!(has_node_replacements, true);
387422
assert_eq!(items.len(), 1);
388423
}
424+
425+
#[test]
426+
fn test_esm() {
427+
let mut has_node_replacements = false;
428+
let mut items = vec![];
429+
430+
let code = r#"
431+
console.log(__filename);
432+
console.log(__dirname);
433+
"#;
434+
let output_code = run_visit(code, |context| NodeReplacer {
435+
source_map: context.source_map.clone(),
436+
global_mark: context.global_mark,
437+
globals: HashMap::new(),
438+
filename: Path::new("/path/random.js"),
439+
has_node_replacements: &mut has_node_replacements,
440+
items: &mut items,
441+
unresolved_mark: context.unresolved_mark,
442+
is_esm: true,
443+
})
444+
.output_code;
445+
446+
let expected_code = r#"
447+
var $parcel$__filename = require("path").resolve(require("path").dirname(require("url").fileURLToPath(import.meta.url)), "$parcel$filenameReplace", "random.js");
448+
var $parcel$__dirname = require("path").resolve(require("path").dirname(require("url").fileURLToPath(import.meta.url)), "$parcel$dirnameReplace");
449+
console.log($parcel$__filename);
450+
console.log($parcel$__dirname);
451+
"#
452+
.trim_start();
453+
assert_eq!(output_code, expected_code);
454+
assert_eq!(has_node_replacements, true);
455+
assert_eq!(items.len(), 2);
456+
}
389457
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@parcel/transformer-node",
3+
"version": "2.13.3",
4+
"license": "MIT",
5+
"publishConfig": {
6+
"access": "public"
7+
},
8+
"funding": {
9+
"type": "opencollective",
10+
"url": "https://opencollective.com/parcel"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/parcel-bundler/parcel.git"
15+
},
16+
"main": "lib/NodeTransformer.js",
17+
"source": "src/NodeTransformer.js",
18+
"engines": {
19+
"node": ">= 16.0.0",
20+
"parcel": "^2.13.3"
21+
},
22+
"dependencies": {
23+
"@parcel/plugin": "2.13.3"
24+
}
25+
}

0 commit comments

Comments
 (0)