Compare commits

...

37 Commits

Author SHA1 Message Date
RiskyMH
0c82efd6cd Implement eager mode for import.meta.glob()
- Add eager mode support that loads modules synchronously
- Generate static import statements for eager mode instead of dynamic imports
- Support import option with eager mode to extract specific exports
- Support with option (import attributes like type: 'text') with eager mode
- Works in both bundler and runtime
- Mark file as ESM when eager glob imports are added
- Create symbols in module scope for import bindings
- All 22 import.meta.glob tests now pass
2025-10-17 13:42:22 +11:00
RiskyMH
abcfcd5bc4 negative patterns 2025-10-16 04:46:22 +11:00
autofix-ci[bot]
bd3822348b [autofix.ci] apply automated fixes 2025-10-15 16:25:50 +00:00
RiskyMH
8a6710f1a5 Merge remote-tracking branch 'origin/main' into riskymh/import.meta.glob 2025-10-16 03:20:15 +11:00
RiskyMH
69d9f83319 update 2025-10-16 03:20:04 +11:00
RiskyMH
533011d25f Merge branch 'main' into riskymh/import.meta.glob 2025-10-14 23:16:16 +11:00
RiskyMH
4c7e8f0649 fix asan ci 2025-10-14 23:13:49 +11:00
Michael H
490c551812 Merge branch 'main' into riskymh/import.meta.glob 2025-10-11 08:26:05 +11:00
RiskyMH
c431a726d6 . 2025-09-14 12:26:38 +10:00
RiskyMH
7236daa029 upd types 2025-09-14 12:25:01 +10:00
RiskyMH
72cb84ce17 semi better 2025-09-14 02:55:27 +10:00
RiskyMH
b69193ce05 base option too 2025-09-14 02:06:30 +10:00
RiskyMH
0e5c48bb8d fix some things 2025-09-13 23:49:13 +10:00
RiskyMH
f530a16f1d Merge origin/main into riskymh/import.meta.glob 2025-09-13 23:21:03 +10:00
Jarred Sumner
8ba8e96a33 Merge branch 'main' into riskymh/import.meta.glob 2025-08-13 20:49:22 -07:00
RiskyMH
fe188726dc Merge branch 'main' into riskymh/import.meta.glob 2025-08-07 18:17:24 +10:00
RiskyMH
ed3f2d6629 updates 2025-08-07 18:17:06 +10:00
RiskyMH
95bd24e6b0 Merge branch 'main' into riskymh/import.meta.glob
Properly integrated import.meta.glob support into new parser structure:
- Added handleImportMetaGlobCall to P.zig
- Added import_meta_glob case handling in visitExpr.zig
- Added glob detection for import.meta.glob in maybe.zig
- Updated SideEffects.zig to handle import_meta_glob
2025-08-07 00:23:35 +10:00
RiskyMH
ccaef4bc19 Merge branch 'main' of https://github.com/oven-sh/bun into riskymh/import.meta.glob 2025-08-02 15:50:39 +10:00
RiskyMH
ce1756a2d4 Update js_printer.zig 2025-07-30 16:02:07 +10:00
RiskyMH
ddb56e83db Merge branch 'main' into riskymh/import.meta.glob 2025-07-30 15:56:30 +10:00
RiskyMH
642fb9d181 fix: strip import options when bundling with applied loader 2025-07-30 13:01:37 +10:00
autofix-ci[bot]
e86f0c65f1 [autofix.ci] apply automated fixes 2025-07-30 02:18:45 +00:00
RiskyMH
18bf62da3e support await import('./script.js', { with: { type: 'text' } }); 2025-07-30 12:15:37 +10:00
autofix-ci[bot]
5083f26b02 [autofix.ci] apply automated fixes 2025-07-30 01:38:55 +00:00
RiskyMH
6cd626a4ce Update no-validate-exceptions.txt 2025-07-30 11:35:22 +10:00
RiskyMH
8150288fda Update import-meta-glob.test.ts 2025-07-30 11:22:42 +10:00
RiskyMH
65d491e5ec Merge branch 'main' into riskymh/import.meta.glob 2025-07-30 10:58:51 +10:00
RiskyMH
892a20d108 maybe fix things 2025-07-30 10:58:34 +10:00
autofix-ci[bot]
6894f8a14a [autofix.ci] apply automated fixes 2025-07-29 23:12:58 +00:00
RiskyMH
61f2d9eedd fix --splitting 2025-07-30 09:09:55 +10:00
autofix-ci[bot]
5ddea5ce1d [autofix.ci] apply automated fixes 2025-07-29 21:29:03 +00:00
RiskyMH
e0bd157f68 fix ci 2025-07-30 07:26:33 +10:00
autofix-ci[bot]
6a60b82fd2 [autofix.ci] apply automated fixes 2025-07-29 13:12:16 +00:00
RiskyMH
2f9ff3f31d docs 2025-07-29 23:09:51 +10:00
autofix-ci[bot]
5812103e08 [autofix.ci] apply automated fixes 2025-07-29 13:06:37 +00:00
RiskyMH
581d72ae60 implement import.meta.glob 2025-07-29 22:59:46 +10:00
15 changed files with 1484 additions and 4 deletions

View File

@@ -12,6 +12,8 @@ import.meta.main; // `true` if this file is directly executed by `bun run`
// `false` otherwise
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"
import.meta.glob("./src/*.ts"); // => { "./src/a.ts": () => import("./src/a.ts"), ... }
```
{% table %}
@@ -66,4 +68,17 @@ import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/inde
- `import.meta.url`
- A `string` url to the current file, e.g. `file:///path/to/project/index.ts`. Equivalent to [`import.meta.url` in browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta#url)
---
- `import.meta.glob`
- Import multiple modules using glob patterns. Returns an object mapping file paths to lazy-loading functions.
```ts
const modules = import.meta.glob("./src/*.ts");
// const modules = {
// './src/a.ts': () => import('./src/a.ts'),
// './src/b.ts': () => import('./src/b.ts'),
// }
```
{% /table %}

View File

@@ -1344,6 +1344,35 @@ interface ImportMeta {
*/
main: boolean;
/**
* Import multiple modules using glob patterns.
* Inspired by [Vite's `import.meta.glob`](https://vite.dev/guide/features.html#glob-import).
*
* @param pattern - A glob pattern or array of glob patterns to match files
* @param options - Options for how imports are handled
* @returns An object mapping file paths to dynamic import functions
*
* @example
* const modules = import.meta.glob('./src/*.ts')
* const module = await modules['./src/foo.ts']()
*
* // equivalent to
* const modules = {
* './src/foo.ts': () => import('./src/foo.ts'),
* './src/bar.ts': () => import('./src/bar.ts'),
* }
* const module = await modules['./src/foo.ts']()
*/
glob<Eager extends boolean = false, TModule = unknown>(
pattern: string | string[],
options?: ImportMetaGlobOptions<Eager>,
): Eager extends true ? Record<string, TModule> : Record<string, () => Promise<TModule>>;
glob<TModule = unknown>(
pattern: string | string[],
options?: ImportMetaGlobOptions<false>,
): Record<string, () => Promise<TModule>>;
glob<TModule = unknown>(pattern: string | string[], options?: ImportMetaGlobOptions<true>): Record<string, TModule>;
/** Alias of `import.meta.dir`. Exists for Node.js compatibility */
dirname: string;
@@ -1351,6 +1380,90 @@ interface ImportMeta {
filename: string;
}
interface ImportMetaGlobOptions<Eager extends boolean = false> {
// todo:
// /**
// * If true, imports all modules eagerly as if they were top level imports.
// * If false (default), returns functions that import modules lazily with dynamic imports.
// *
// * @example
// * const eager = import.meta.glob('./src/*.ts', { eager: true })
// * const normal = import.meta.glob('./src/*.ts', { eager: false })
// *
// * // equivalent to
// * import * as __modules_foo from './src/foo.ts'
// * import * as __modules_bar from './src/bar.ts'
// *
// * const eager = {
// * './src/foo.ts': __modules_foo,
// * './src/bar.ts': __modules_bar,
// * }
// * const normal = {
// * './src/foo.ts': () => import('./src/foo.ts'),
// * './src/bar.ts': () => import('./src/bar.ts'),
// * }
// */
// eager?: Eager;
/** Right now Bun doesn't support eager mode */
eager?: false;
/**
* Specify a named export to import from matched modules.
* If not specified, imports the entire module.
*
* @example
* const modules = import.meta.glob('./src/*.ts', { import: 'default' })
*
* // equivalent to
* const modules = {
* './src/bar.ts': () => import('./src/bar.ts').then((m) => m.default),
* './src/foo.ts': () => import('./src/foo.ts').then((m) => m.default),
* }
*/
import?: "default" | (string & {});
/**
* Add a query string to the end of the "generated" dynamic import.
*
* @example
* const modules = import.meta.glob('./assets/*.txt', { query: '?something' })
*
* // equivalent to
* const modules = {
* './assets/file.txt': () => import('./assets/file.txt?something'),
* }
*/
query?: string;
/**
* Import attributes to pass to the import statement.
* This is like the standard way to specify import options to a normal dynamic import.
*
* @example
* const modules = import.meta.glob('./assets/*.txt', { with: { type: 'text' } })
*
* // equivalent to
* const modules = {
* './assets/file.txt': () => import('./assets/file.txt', { with: { type: 'text' } }),
* }
*/
with?: ImportAttributes;
/**
* The base path to prepend to the imports.
* Basically changing the "cwd" of the directory to scan.
*
* @example
* const modules = import.meta.glob('./*.txt', { base: '../public' })
*
* // equivalent to
* const modules = {
* './file.txt': () => import('../public/file.txt'),
* }
*/
base?: string;
}
interface ImportAttributes {
type: "css" | "file" | "json" | "jsonc" | "toml" | "yaml" | "txt" | "text" | "html" | (string & {});
}
/**
* NodeJS-style `require` function
*

View File

@@ -192,6 +192,8 @@ pub const Special = union(enum) {
hot_accept_visited,
/// Prints the resolved specifier string for an import record.
resolved_specifier_string: ImportRecord.Index,
/// `import.meta.glob`
import_meta_glob,
};
pub const Call = struct {

View File

@@ -298,6 +298,10 @@ pub fn NewParser_(
export_star_import_records: List(u32) = .{},
import_symbol_property_uses: SymbolPropertyUseMap = .{},
// Track generated import statements from eager globs
eager_glob_import_stmts: List(Stmt) = .{},
glob_call_counter: u32 = 0,
// These are for handling ES6 imports and exports
esm_import_keyword: logger.Range = logger.Range.None,
esm_export_keyword: logger.Range = logger.Range.None,
@@ -511,6 +515,10 @@ pub fn NewParser_(
p.import_records.items[import_record_index].tag = tag;
}
if (state.import_loader) |loader| {
p.import_records.items[import_record_index].loader = loader;
}
p.import_records.items[import_record_index].handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target;
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;
@@ -6014,6 +6022,306 @@ pub fn NewParser_(
} };
}
pub fn handleImportMetaGlobCall(p: *P, call: *E.Call, loc: logger.Loc) Expr {
if (call.args.len == 0) {
bun.handleOom(p.log.addError(p.source, loc, "import.meta.glob() requires at least one argument"));
return p.newExpr(E.Object{}, loc);
}
// Parse patterns
var patterns = std.ArrayList([]const u8).init(p.allocator);
defer patterns.deinit();
switch (call.args.at(0).data) {
.e_string => |str| bun.handleOom(patterns.append(str.slice(p.allocator))),
.e_array => |arr| {
for (arr.items.slice()) |item| {
if (item.data == .e_string) {
bun.handleOom(patterns.append(item.data.e_string.slice(p.allocator)));
} else {
bun.handleOom(p.log.addError(p.source, item.loc, "import.meta.glob() patterns must be string literals"));
return p.newExpr(E.Object{}, loc);
}
}
},
else => {
bun.handleOom(p.log.addError(p.source, call.args.at(0).loc, "import.meta.glob() patterns must be string literals or an array of string literals"));
return p.newExpr(E.Object{}, loc);
},
}
// Parse options
var query: ?[]const u8 = null;
var import_name: ?[]const u8 = null;
var loader: ?options.Loader = null;
var with_attrs: ?*const E.Object = null;
var base_path: ?[]const u8 = null;
var eager: bool = false;
if (call.args.len >= 2 and call.args.at(1).data == .e_object) {
const obj = call.args.at(1).data.e_object;
if (obj.get("query")) |query_value| {
if (query_value.data == .e_string) {
query = query_value.data.e_string.slice(p.allocator);
}
}
if (obj.get("import")) |import_value| {
if (import_value.data == .e_string) {
import_name = import_value.data.e_string.slice(p.allocator);
}
}
if (obj.get("eager")) |eager_value| {
if (eager_value.data == .e_boolean) {
eager = eager_value.data.e_boolean.value;
}
}
if (obj.get("base")) |base_value| {
if (base_value.data == .e_string) {
const _base_path = base_value.data.e_string.slice(p.allocator);
base_path = if (strings.hasPrefixComptime(_base_path, "./") or strings.hasPrefixComptime(_base_path, "../") or strings.hasPrefixComptime(_base_path, "/"))
_base_path
else
bun.handleOom(std.fmt.allocPrint(p.allocator, "./{s}", .{_base_path}));
}
}
if (obj.get("with")) |with_value| {
if (with_value.data == .e_object) {
with_attrs = with_value.data.e_object;
if (with_attrs.?.get("type")) |type_value| {
if (type_value.data == .e_string) {
loader = options.Loader.fromString(type_value.data.e_string.slice(p.allocator));
}
}
}
}
}
// Find matching files
const source_dir = p.source.path.sourceDir();
const search_dir = if (base_path) |base| blk: {
if (strings.hasPrefixComptime(base, "/")) {
break :blk base;
} else {
var path_buf: bun.PathBuffer = undefined;
const resolved = bun.path.joinAbsStringBuf(source_dir, &path_buf, &.{base}, .auto);
break :blk bun.handleOom(p.allocator.dupe(u8, resolved));
}
} else source_dir;
var matched_files = std.ArrayList([]const u8).init(p.allocator);
defer matched_files.deinit();
var glob_arena = bun.ArenaAllocator.init(p.allocator);
defer glob_arena.deinit();
for (patterns.items) |pattern| {
var walker = glob.BunGlobWalker{};
defer walker.deinit(false);
switch (walker.initWithCwd(&glob_arena, pattern, search_dir, true, false, true, false, true) catch continue) {
.err => continue,
.result => {},
}
var iter = glob.BunGlobWalker.Iterator{ .walker = &walker };
defer iter.deinit();
switch (iter.init() catch continue) {
.err => continue,
.result => {},
}
while (switch (iter.next() catch continue) {
.err => null,
.result => |path| path,
}) |path| brk: {
if (patterns.items.len > 0) for (patterns.items) |patt| {
if (patt.len < 1 or patt[0] != '!') continue;
if (glob.match(patt[1..], path).matches()) {
break :brk;
}
};
var path_buf: bun.PathBuffer = undefined;
const slash_normalized = if (comptime bun.Environment.isWindows)
strings.normalizeSlashesOnly(&path_buf, path, '/')
else
path;
const duped = bun.handleOom(p.allocator.dupe(u8, slash_normalized));
bun.handleOom(matched_files.append(duped));
}
}
std.sort.block([]const u8, matched_files.items, {}, struct {
fn lessThan(_: void, a: []const u8, b_path: []const u8) bool {
return strings.order(a, b_path) == .lt;
}
}.lessThan);
var properties: []G.Property = bun.handleOom(p.allocator.alloc(G.Property, matched_files.items.len));
for (matched_files.items, 0..) |file_path, i| {
const import_path: []const u8 = if (base_path) |base|
if (query) |q|
bun.handleOom(std.fmt.allocPrint(p.allocator, "{s}/{s}{s}", .{ base, file_path, q }))
else
bun.handleOom(std.fmt.allocPrint(p.allocator, "{s}/{s}", .{ base, file_path }))
else if (query) |q|
bun.handleOom(std.fmt.allocPrint(p.allocator, "{s}{s}", .{ file_path, q }))
else
file_path;
// For eager mode, always use static import (synchronous)
// For lazy mode, use dynamic import (returns Promise)
const import_kind = if (eager)
ImportKind.stmt
else
ImportKind.dynamic;
const import_record_index = p.addImportRecord(import_kind, loc, import_path);
bun.handleOom(p.import_records_for_current_part.append(p.allocator, import_record_index));
if (loader) |l| p.import_records.items[import_record_index].loader = l;
if (eager) {
// Mark file as ESM since we're adding imports
if (p.esm_import_keyword.len == 0) {
p.esm_import_keyword = logger.Range{ .loc = loc, .len = 0 };
}
const namespace_name = bun.handleOom(std.fmt.allocPrint(p.allocator, "__glob_{d}_{d}", .{ p.glob_call_counter, i }));
if (import_name) |name| {
p.import_records.items[import_record_index].contains_default_alias = strings.eqlComptime(name, "default");
} else {
p.import_records.items[import_record_index].contains_import_star = true;
}
// Create symbol in module scope since imports must be at top level
const saved_scope = p.current_scope;
p.current_scope = p.module_scope;
const namespace_ref = bun.handleOom(p.newSymbol(.import, namespace_name));
p.current_scope = saved_scope;
bun.handleOom(p.is_import_item.put(p.allocator, namespace_ref, {}));
if (import_name) |name| {
bun.handleOom(p.named_imports.put(p.allocator, namespace_ref, js_ast.NamedImport{
.alias_is_star = false,
.alias = name,
.alias_loc = loc,
.namespace_ref = Ref.None,
.import_record_index = import_record_index,
}));
} else {
bun.handleOom(p.named_imports.put(p.allocator, namespace_ref, js_ast.NamedImport{
.alias_is_star = true,
.alias = "",
.alias_loc = loc,
.namespace_ref = Ref.None,
.import_record_index = import_record_index,
}));
bun.handleOom(p.import_items_for_namespace.put(p.allocator, namespace_ref, ImportItemForNamespaceMap.init(p.allocator)));
}
const import_stmt = if (import_name) |name| blk: {
var items = bun.handleOom(p.allocator.alloc(js_ast.ClauseItem, 1));
items[0] = js_ast.ClauseItem{
.alias = name,
.alias_loc = loc,
.name = LocRef{
.loc = loc,
.ref = namespace_ref,
},
.original_name = namespace_name,
};
break :blk p.s(S.Import{
.namespace_ref = Ref.None,
.import_record_index = import_record_index,
.items = items,
.is_single_line = true,
}, loc);
} else p.s(S.Import{
.namespace_ref = namespace_ref,
.import_record_index = import_record_index,
.star_name_loc = null,
.is_single_line = true,
}, loc);
bun.handleOom(p.eager_glob_import_stmts.append(p.allocator, import_stmt));
const namespace_expr = p.newExpr(E.Identifier{ .ref = namespace_ref }, loc);
p.recordUsage(namespace_ref);
const value_expr = namespace_expr;
properties[i] = .{
.key = p.newExpr(E.String{ .data = file_path }, loc),
.value = value_expr,
};
} else {
const import_expr = p.newExpr(E.Import{
.expr = p.newExpr(E.String{ .data = import_path }, loc),
.options = if (with_attrs) |attrs| blk: {
var with_props: []G.Property = bun.handleOom(p.allocator.alloc(G.Property, 1));
with_props[0] = .{
.key = p.newExpr(E.String{ .data = "with" }, loc),
.value = p.newExpr(E.Object{ .properties = attrs.properties }, loc),
};
break :blk p.newExpr(E.Object{ .properties = G.Property.List.fromOwnedSlice(with_props) }, loc);
} else Expr.empty,
.import_record_index = import_record_index,
}, loc);
const return_expr = if (import_name) |name| blk: {
const m_ref: Ref = bun.handleOom(p.newSymbol(.other, "m"));
var arrow_stmts: []Stmt = bun.handleOom(p.allocator.alloc(Stmt, 1));
arrow_stmts[0] = p.s(S.Return{ .value = p.newExpr(E.Dot{
.target = p.newExpr(E.Identifier{ .ref = m_ref }, loc),
.name = name,
.name_loc = loc,
}, loc) }, loc);
var arrow_args: []G.Arg = bun.handleOom(p.allocator.alloc(G.Arg, 1));
arrow_args[0] = .{
.binding = p.b(B.Identifier{ .ref = m_ref }, logger.Loc.Empty),
};
const arrow_fn = p.newExpr(E.Arrow{
.args = arrow_args,
.body = .{ .loc = loc, .stmts = arrow_stmts },
.prefer_expr = true,
}, loc);
break :blk p.newExpr(E.Call{
.target = p.newExpr(E.Dot{
.target = import_expr,
.name = "then",
.name_loc = loc,
}, loc),
.args = bun.handleOom(ExprNodeList.fromSlice(p.allocator, &.{arrow_fn})),
}, loc);
} else import_expr;
var outer_stmts: []Stmt = bun.handleOom(p.allocator.alloc(Stmt, 1));
outer_stmts[0] = p.s(S.Return{ .value = return_expr }, loc);
properties[i] = .{
.key = p.newExpr(E.String{ .data = file_path }, loc),
.value = p.newExpr(E.Arrow{
.args = &.{},
.body = .{ .loc = loc, .stmts = outer_stmts },
.prefer_expr = true,
}, loc),
};
}
}
return p.newExpr(E.Object{
.properties = G.Property.List.fromOwnedSlice(properties),
}, loc);
}
const ReactRefreshExportKind = enum { named, default };
pub fn handleReactRefreshRegister(p: *P, stmts: *ListManaged(Stmt), original_name: []const u8, ref: Ref, export_kind: ReactRefreshExportKind) !void {
@@ -6239,6 +6547,19 @@ pub fn NewParser_(
) !js_ast.Ast {
const allocator = p.allocator;
// Inject eager glob imports as real import statements
if (p.eager_glob_import_stmts.items.len > 0 and parts.items.len > 0) {
// Mark as ESM since we're adding import statements
p.has_es_module_syntax = true;
// Prepend import statements to the first part
const old_stmts = parts.items[0].stmts;
const new_stmts = try allocator.alloc(Stmt, p.eager_glob_import_stmts.items.len + old_stmts.len);
@memcpy(new_stmts[0..p.eager_glob_import_stmts.items.len], p.eager_glob_import_stmts.items);
@memcpy(new_stmts[p.eager_glob_import_stmts.items.len..], old_stmts);
parts.items[0].stmts = new_stmts;
}
// if (p.options.tree_shaking and p.options.features.trim_unused_imports) {
// p.treeShake(&parts, false);
// }
@@ -6723,6 +7044,8 @@ var falseValueExpr = Expr.Data{ .e_boolean = E.Boolean{ .value = false } };
const string = []const u8;
const glob = @import("../glob.zig");
const Define = @import("../defines.zig").Define;
const DefineData = @import("../defines.zig").DefineData;

View File

@@ -869,6 +869,7 @@ pub const SideEffects = enum(u1) {
.module_exports,
.resolved_specifier_string,
.hot_data,
.import_meta_glob,
=> {},
.hot_accept,
.hot_accept_visited,

View File

@@ -410,6 +410,12 @@ pub fn AstMaybe(
}, .loc = loc };
}
if (strings.eqlComptime(name, "glob")) {
return .{ .data = .{
.e_special = .import_meta_glob,
}, .loc = loc };
}
// Inline import.meta properties for Bake
if (p.options.framework != null) {
if (strings.eqlComptime(name, "dir") or strings.eqlComptime(name, "dirname")) {

View File

@@ -1358,6 +1358,9 @@ pub fn VisitExpr(
if (!p.options.features.hot_module_reloading)
return .{ .data = .e_undefined, .loc = expr.loc };
},
.import_meta_glob => {
return p.handleImportMetaGlobCall(e_, expr.loc);
},
else => {},
};

View File

@@ -13,7 +13,8 @@
/// Version 14: Updated global defines table list.
/// Version 15: Updated global defines table list.
/// Version 16: Added typeof undefined minification optimization.
const expected_version = 16;
/// Version 17: Added import.meta.glob support.
const expected_version = 17;
const debug = Output.scoped(.cache, .visible);
const MINIMUM_CACHE_SIZE = 50 * 1024;

View File

@@ -2462,7 +2462,16 @@ pub const BundleV2 = struct {
existing.value_ptr.* = source_index.get();
out_source_index = source_index;
this.graph.ast.append(this.allocator(), JSAst.empty) catch unreachable;
const loader = path.loader(&this.transpiler.options.loaders) orelse options.Loader.file;
const loader = blk: {
if (this.graph.ast.len > resolve.import_record.importer_source_index) {
const record: *ImportRecord = &this.graph.ast.items(.import_records)[resolve.import_record.importer_source_index].slice()[resolve.import_record.import_record_index];
if (record.loader) |override_loader| {
break :blk override_loader;
}
}
break :blk path.loader(&this.transpiler.options.loaders) orelse options.Loader.file;
};
this.graph.input_files.append(this.allocator(), .{
.source = .{

View File

@@ -1797,9 +1797,14 @@ fn NewPrinter(
p.printStringLiteralUTF8(path.pretty, false);
}
// Only print import options if the loader hasn't already been applied
// When bundling with a loader applied, the transformation has already happened
// so we don't need the import options anymore
if (!import_options.isMissing()) {
p.printWhitespacer(ws(", "));
p.printExpr(import_options, .comma, .{});
if (!p.options.bundling or record.loader == null) {
p.printWhitespacer(ws(", "));
p.printExpr(import_options, .comma, .{});
}
}
p.print(")");
@@ -2091,6 +2096,10 @@ fn NewPrinter(
bun.debugAssert(p.options.module_type == .internal_bake_dev);
p.printStringLiteralUTF8(p.importRecord(index.get()).path.pretty, true);
},
.import_meta_glob => {
// This should not reach the printer - it should be transformed in the parser
p.print("(function() { throw new Error('import.meta.glob was not transformed at build time'); })");
},
},
.e_commonjs_export_identifier => |id| {

View File

@@ -65,6 +65,64 @@ describe("bundler", async () => {
},
run: { stdout: '{"hello":"world"}' },
});
itBundled("bun/loader-text-js-file", {
target,
files: {
"/entry.ts": /* js */ `
import hello from './hello.js' with {type: "text"};
console.write(hello);
`,
"/hello.js": `console.log("This should not be executed!");
export default "Hello from JS";`,
},
run: { stdout: `console.log("This should not be executed!");\nexport default "Hello from JS";` },
});
itBundled("bun/loader-text-dynamic-import", {
target,
files: {
"/entry.ts": /* js */ `
const mod = await import('./script.js', { with: { type: 'text' } });
console.write(mod.default);
`,
"/script.js": `console.log("This should not run!");
export const data = "test";`,
},
run: { stdout: `console.log("This should not run!");\nexport const data = "test";` },
});
// Verify that without type: "text", JS files are executed normally
itBundled("bun/loader-js-normal-execution", {
target,
files: {
"/entry.ts": /* js */ `
import { message } from './module.js';
console.write(message);
`,
"/module.js": `export const message = "executed";`,
},
run: { stdout: "executed" },
});
itBundled("bun/loader-text-splitting-strips-options", {
target,
splitting: true,
outdir: "/out",
files: {
"/entry.ts": /* js */ `
const mod = await import('./data.js', { with: { type: 'text' } });
console.write(mod.default);
`,
"/data.js": `export default "test data";`,
},
run: { stdout: `export default "test data";` },
onAfterBundle(api) {
const entryFile = api.readFile("/out/entry.js");
expect(entryFile).not.toContain("with:");
expect(entryFile).not.toContain("{ type:");
},
});
});
}

View File

@@ -0,0 +1,34 @@
import { expectType } from "./utilities";
const fixtures = import.meta.glob("./**/*.ts");
for (const [file, importFn] of Object.entries(fixtures)) {
console.log(file, await importFn());
}
expectType<Record<string, () => Promise<any>>>(fixtures);
const http = import.meta.glob(["*.html", "**/*.html"], { with: { type: "text" } });
expectType<Record<string, () => Promise<any>>>(http);
const tests = import.meta.glob("*.test.ts", { base: "../", eager: false });
expectType<Record<string, () => Promise<any>>>(tests);
const jsons = import.meta.glob<false, Record<string, number>>("*.json");
expectType<Record<string, () => Promise<Record<string, number>>>>(jsons);
const jsons2 = import.meta.glob<Record<string, number>>("*.json");
expectType<Record<string, () => Promise<Record<string, number>>>>(jsons2);
// @ts-expect-error: right now bun doesn't support eager
const eagerJsons = import.meta.glob<Record<string, number>>("*.json", { eager: true });
// @ts-expect-error: right now bun doesn't support eager
expectType<Record<string, Record<string, number>>>(eagerJsons);
expectType<string>(import.meta.dir);
expectType<string>(import.meta.dirname);
expectType<string>(import.meta.file);
expectType<string>(import.meta.path);
expectType<string>(import.meta.url);
expectType<boolean>(import.meta.main);
expectType<string>(import.meta.resolve("zod"));
expectType<Record<string, () => Promise<any>>>(import.meta.glob("*"));
expectType<Object>(import.meta.hot);

View File

@@ -392,6 +392,7 @@ exports[`fast-glob e2e tests patterns regular cwd **/{nested,file.md}/*: **/{nes
exports[`fast-glob e2e tests patterns regular relative cwd ./*: ./* 1`] = `
[
"./import-meta-glob.test.ts",
"./leak.test.ts",
"./match.test.ts",
"./proto.test.ts",

View File

@@ -0,0 +1,903 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir, tempDirWithFiles } from "harness";
import path from "node:path";
describe("import.meta.glob", () => {
describe("runtime behavior", () => {
test("returns lazy-loading functions for matched files", async () => {
const dir = tempDirWithFiles("import-glob-basic", {
"index.js": `
const modules = import.meta.glob('./modules/*.js');
console.log(JSON.stringify(Object.keys(modules)));
console.log(typeof modules['./modules/a.js']);
`,
"modules/a.js": `export const name = "a";`,
"modules/b.js": `export const name = "b";`,
"modules/c.js": `export const name = "c";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[0])).toEqual(["./modules/a.js", "./modules/b.js", "./modules/c.js"]);
expect(lines[1]).toBe("function");
});
test("import option extracts specific named export", async () => {
const dir = tempDirWithFiles("import-glob-named", {
"index.js": `
const defaultModules = import.meta.glob('./routes/*.js', { import: 'default' });
const namedModules = import.meta.glob('./routes/*.js', { import: 'handler' });
console.log('DEFAULT_MODULES:');
for (const [path, loader] of Object.entries(defaultModules)) {
const result = await loader();
console.log(path + ':', result);
}
console.log('NAMED_MODULES:');
for (const [path, loader] of Object.entries(namedModules)) {
const result = await loader();
console.log(path + ':', result);
}
`,
"routes/home.js": `
export default "home-route";
export const handler = "home-handler";
export const unused = "should-not-see-this";
`,
"routes/about.js": `
export default "about-route";
export const handler = "about-handler";
export const unused = "should-not-see-this";
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines).toContain("./routes/about.js: about-route");
expect(lines).toContain("./routes/home.js: home-route");
expect(lines).toContain("./routes/about.js: about-handler");
expect(lines).toContain("./routes/home.js: home-handler");
expect(stdout).not.toContain("should-not-see-this");
});
test("options are passed through (query and with)", async () => {
const dir = tempDirWithFiles("import-glob-options", {
"index.js": `
const withType = import.meta.glob('./src/*.ts', { with: { type: 'text' } });
const withQuery = import.meta.glob('./data/*.js', { query: '?inline' });
console.log('WITH_TYPE:', Object.keys(withType).length);
console.log('WITH_QUERY:', Object.keys(withQuery).length);
`,
"src/helper.ts": `export function helper() { return "typescript"; }`,
"data/config.js": `export const config = { version: "1.0" };`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("WITH_TYPE: 1");
expect(lines[1]).toBe("WITH_QUERY: 1");
});
test("eager mode loads modules synchronously", async () => {
const dir = tempDirWithFiles("import-glob-eager", {
"index.js": `
const modules = import.meta.glob('./modules/*.js', { eager: true });
console.log('TYPE:', typeof modules['./modules/a.js']);
console.log('NAME:', modules['./modules/a.js'].name);
console.log('KEYS:', JSON.stringify(Object.keys(modules).sort()));
`,
"modules/a.js": `export const name = "module-a";`,
"modules/b.js": `export const name = "module-b";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("TYPE: object");
expect(lines[1]).toBe("NAME: module-a");
expect(lines[2]).toBe('KEYS: ["./modules/a.js","./modules/b.js"]');
});
test("supports recursive ** and multiple patterns", async () => {
const dir = tempDirWithFiles("import-glob-patterns", {
"index.js": `
const recursive = import.meta.glob('./src/**/*.js');
const multiple = import.meta.glob(['./lib/*.js', './config/*.js']);
const negativeTest = import.meta.glob('./src/**/*.ts');
const complexPattern = import.meta.glob('./src/**/[a-m]*.js');
console.log('RECURSIVE:', JSON.stringify(Object.keys(recursive).sort()));
console.log('MULTIPLE:', JSON.stringify(Object.keys(multiple).sort()));
console.log('NEGATIVE_TEST:', JSON.stringify(Object.keys(negativeTest)));
console.log('COMPLEX_PATTERN:', JSON.stringify(Object.keys(complexPattern).sort()));
`,
"src/main.js": `export default "main";`,
"src/lib/util.js": `export default "util";`,
"src/components/button.js": `export default "button";`,
"lib/helper.js": `export default "helper";`,
"config/app.js": `export default "app";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[0].split(": ")[1])).toEqual([
"./src/components/button.js",
"./src/lib/util.js",
"./src/main.js",
]);
expect(JSON.parse(lines[1].split(": ")[1])).toEqual(["./config/app.js", "./lib/helper.js"]);
expect(JSON.parse(lines[2].split(": ")[1])).toEqual([]);
expect(JSON.parse(lines[3].split(": ")[1])).toEqual(["./src/components/button.js", "./src/main.js"]);
});
test("negative patterns exclude files from results", async () => {
const dir = tempDirWithFiles("import-glob-negative", {
"index.js": `
const all = import.meta.glob('./dir/*.js');
const filtered = import.meta.glob(['./dir/*.js', '!**/bar.js']);
console.log('ALL:', JSON.stringify(Object.keys(all).sort()));
console.log('FILTERED:', JSON.stringify(Object.keys(filtered).sort()));
`,
"dir/foo.js": `export default "foo";`,
"dir/bar.js": `export default "bar";`,
"dir/baz.js": `export default "baz";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[0].split(": ")[1])).toEqual(["./dir/bar.js", "./dir/baz.js", "./dir/foo.js"]);
expect(JSON.parse(lines[1].split(": ")[1])).toEqual(["./dir/baz.js", "./dir/foo.js"]);
});
test("handles empty results gracefully", () => {
const modules = import.meta.glob("./non-existent/*.js");
expect(typeof modules).toBe("object");
expect(Object.keys(modules)).toHaveLength(0);
expect(JSON.stringify(modules)).toBe("{}");
});
test("dynamic imports work when functions are called", async () => {
const dir = tempDirWithFiles("import-glob-dynamic", {
"index.js": `
const modules = import.meta.glob('./modules/*.js');
const loader = modules['./modules/test.js'];
if (loader) {
const mod = await loader();
console.log('SUCCESS:', mod.message);
}
`,
"modules/test.js": `export const message = "Hello from module!";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("SUCCESS: Hello from module!");
});
test("dynamic patterns work at runtime", async () => {
const dir = tempDirWithFiles("import-glob-runtime", {
"index.js": `
const pattern = './modules/*.js';
const modules = import.meta.glob(pattern);
console.log('COUNT:', Object.keys(modules).length);
`,
"modules/test.js": `export const name = "test";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("COUNT: 1");
});
test("error handling and edge cases", async () => {
const dir = tempDirWithFiles("import-glob-errors", {
"index.js": `
const withQuery = import.meta.glob('./data/**/*.json', { query: '?raw' });
console.log('WITH_QUERY_PATHS:', Object.keys(withQuery).sort());
const complex = import.meta.glob('./{data,config}/**/*.{js,json}');
console.log('COMPLEX_COUNT:', Object.keys(complex).length);
const keys = Object.keys(withQuery);
const key = keys.find(k => k.includes('config.json'));
console.log('ACTUAL_KEY:', key);
if (key && withQuery[key]) {
const first = await withQuery[key]();
const second = await withQuery[key]();
console.log('SAME_INSTANCE:', first === second);
}
`,
"data/config.json": `{"version": "1.0"}`,
"data/nested/deep.json": `{"level": "deep"}`,
"config/app.js": `export default { name: "app" };`,
"config/settings.json": `{"theme": "dark"}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toContain("./data/config.json");
expect(lines[0]).toContain("./data/nested/deep.json");
expect(lines[1]).toBe("COMPLEX_COUNT: 4");
expect(lines[2]).toContain("ACTUAL_KEY:");
expect(lines[3]).toBe("SAME_INSTANCE: true");
});
test("base option prepends base path to imports but not keys", async () => {
using dir = tempDir("import-glob-base", {
"index.js": `
const modules = import.meta.glob('./modules/*.js', { base: './src' });
const moduleKeys = Object.keys(modules).sort();
console.log('KEYS:', JSON.stringify(moduleKeys));
for (const [key, loader] of Object.entries(modules)) {
console.log('KEY:', key);
const result = await loader();
console.log('VALUE:', result.default);
}
`,
"src/modules/foo.js": `export default "foo-value";`,
"src/modules/bar.js": `export default "bar-value";`,
"modules/baz.js": `export default "baz-should-not-match";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0) {
console.log("STDERR:", stderr);
console.log("STDOUT:", stdout);
}
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe('KEYS: ["./modules/bar.js","./modules/foo.js"]');
expect(lines).toContain("KEY: ./modules/foo.js");
expect(lines).toContain("VALUE: foo-value");
expect(lines).toContain("KEY: ./modules/bar.js");
expect(lines).toContain("VALUE: bar-value");
});
test("base option with relative path upward", async () => {
using dir = tempDir("import-glob-base-relative", {
"src/index.js": `
const modules = import.meta.glob('./lib/*.js', { base: '../base' });
console.log('KEYS:', JSON.stringify(Object.keys(modules).sort()));
for (const [key, loader] of Object.entries(modules)) {
const result = await loader();
console.log(key + ':', result.default);
}
`,
"base/lib/util.js": `export default "util-module";`,
"base/lib/helper.js": `export default "helper-module";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: path.join(String(dir), "src"),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0 || !stdout.includes("KEYS:")) {
console.log("STDERR:", stderr);
console.log("STDOUT:", stdout);
}
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe('KEYS: ["./lib/helper.js","./lib/util.js"]');
expect(lines).toContain("./lib/util.js: util-module");
expect(lines).toContain("./lib/helper.js: helper-module");
});
test("base option with parent directory", async () => {
using dir = tempDir("import-glob-base-parent", {
"src/index.js": `
const modules = import.meta.glob('./components/*.js', { base: '../shared' });
console.log('KEYS:', JSON.stringify(Object.keys(modules).sort()));
for (const [key, loader] of Object.entries(modules)) {
const result = await loader();
console.log(key + ':', result.name);
}
`,
"shared/components/button.js": `export const name = "button-component";`,
"shared/components/input.js": `export const name = "input-component";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: path.join(String(dir), "src"),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe('KEYS: ["./components/button.js","./components/input.js"]');
expect(lines).toContain("./components/button.js: button-component");
expect(lines).toContain("./components/input.js: input-component");
});
});
describe("bundler behavior", () => {
test("preserves import.meta.glob functionality after bundling", async () => {
const dir = tempDirWithFiles("import-glob-bundle", {
"index.js": `
const modules = import.meta.glob('./src/*.js');
console.log('COUNT:', Object.keys(modules).length);
console.log('FIRST_TYPE:', typeof Object.values(modules)[0]);
`,
"src/a.js": `export default "a";`,
"src/b.js": `export default "b";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--outfile", "dist/bundle.js"],
env: bunEnv,
cwd: dir,
});
await buildProc.exited;
await using runProc = Bun.spawn({
cmd: [bunExe(), "dist/bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("COUNT: 2");
expect(lines[1]).toBe("FIRST_TYPE: function");
});
test("bundled code works with different glob modes", async () => {
const dir = tempDirWithFiles("import-glob-bundle-modes", {
"index.js": `
const regular = import.meta.glob('./lib/*.js');
const eager = import.meta.glob('./lib/*.js', { eager: true });
const withImport = import.meta.glob('./lib/*.js', { import: 'name' });
console.log('REGULAR:', typeof Object.values(regular)[0]);
console.log('EAGER:', typeof Object.values(eager)[0]);
console.log('IMPORT:', typeof Object.values(withImport)[0]);
`,
"lib/util.js": `export const name = "util";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--outfile", "dist/bundle.js"],
env: bunEnv,
cwd: dir,
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
expect(buildStderr).toBe("");
await using runProc = Bun.spawn({
cmd: [bunExe(), "dist/bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("REGULAR: function");
expect(lines[1]).toBe("EAGER: object");
expect(lines[2]).toBe("IMPORT: function");
});
test("bundled code maintains correct file paths", async () => {
const dir = tempDirWithFiles("import-glob-bundle-paths", {
"index.js": `
const modules = import.meta.glob('./src/**/*.js');
const paths = Object.keys(modules).sort();
console.log('PATHS:', JSON.stringify(paths));
console.log('COUNT:', paths.length);
`,
"src/main.js": `export default "main";`,
"src/lib/util.js": `export default "util";`,
"src/lib/helper.js": `export default "helper";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--outfile", "dist/bundle.js"],
env: bunEnv,
cwd: dir,
});
await buildProc.exited;
await using runProc = Bun.spawn({
cmd: [bunExe(), "dist/bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe('PATHS: ["./src/lib/helper.js","./src/lib/util.js","./src/main.js"]');
expect(lines[1]).toBe("COUNT: 3");
});
test("bundled code works with --splitting", async () => {
const dir = tempDirWithFiles("import-glob-splitting", {
"entry1.js": `
const modules = import.meta.glob('./shared/*.js');
export function getModules() {
return modules;
}
console.log('ENTRY1_MODULES:', Object.keys(modules).length);
`,
"entry2.js": `
const modules = import.meta.glob('./shared/*.js');
export function getModules() {
return modules;
}
console.log('ENTRY2_MODULES:', Object.keys(modules).length);
`,
"shared/util.js": `export default "util"; export const name = "util";`,
"shared/helper.js": `export default "helper"; export const name = "helper";`,
"test.js": `
import { getModules as getModules1 } from './dist/entry1.js';
import { getModules as getModules2 } from './dist/entry2.js';
const modules1 = getModules1();
const modules2 = getModules2();
console.log('TEST_MODULES1:', Object.keys(modules1).length);
console.log('TEST_MODULES2:', Object.keys(modules2).length);
console.log('SAME_KEYS:', JSON.stringify(Object.keys(modules1).sort()) === JSON.stringify(Object.keys(modules2).sort()));
`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "entry1.js", "entry2.js", "--splitting", "--outdir", "dist", "--target=bun"],
env: bunEnv,
cwd: dir,
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
expect(buildStderr).toBe("");
await using runProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(lines).toContain("ENTRY1_MODULES: 2");
expect(lines).toContain("ENTRY2_MODULES: 2");
expect(lines).toContain("TEST_MODULES1: 2");
expect(lines).toContain("TEST_MODULES2: 2");
expect(lines).toContain("SAME_KEYS: true");
});
test("--splitting works with import option", async () => {
const dir = tempDirWithFiles("import-glob-splitting-import", {
"entry.js": `
const modules = import.meta.glob('./lib/*.js', { import: 'name' });
export async function loadNames() {
const names = [];
for (const [path, loader] of Object.entries(modules)) {
const name = await loader();
names.push(name);
}
return names;
}
`,
"lib/foo.js": `export const name = "foo"; export default "default-foo";`,
"lib/bar.js": `export const name = "bar"; export default "default-bar";`,
"test.js": `
import { loadNames } from './dist/entry.js';
loadNames().then(names => {
console.log('NAMES:', JSON.stringify(names.sort()));
});
`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "entry.js", "--splitting", "--outdir", "dist", "--target=bun"],
env: bunEnv,
cwd: dir,
});
await buildProc.exited;
await using runProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout.trim()).toContain('NAMES: ["bar","foo"]');
});
test("--splitting works with 'with' option for JS files as text", async () => {
const dir = tempDirWithFiles("import-glob-splitting-with", {
"entry.js": `
const modules = import.meta.glob('./assets/*', { with: { type: 'text' } });
export async function loadTexts() {
const texts = {};
for (const [path, loader] of Object.entries(modules)) {
const mod = await loader();
texts[path] = mod.default || mod;
}
return texts;
}
`,
"assets/hello.txt": `Hello World`,
"assets/goodbye.txt": `Goodbye World`,
"assets/script.js": `console.log("This should be text, not executed!"); export default "js-module";`,
"test.js": `
import { loadTexts } from './dist/entry.js';
loadTexts().then(texts => {
console.log('COUNT:', Object.keys(texts).length);
console.log('SCRIPT_TEXT:', texts['./assets/script.js']);
});
`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "entry.js", "--splitting", "--outdir", "dist", "--target=bun"],
env: bunEnv,
cwd: dir,
});
await buildProc.exited;
await using runProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toContain("COUNT: 3");
expect(stdout).toContain("SCRIPT_TEXT:");
expect(stdout).toContain('console.log("This should be text, not executed!");');
expect(stdout).toContain('export default "js-module";');
const lines = stdout.split("\n");
const shouldNotExecuteLine = lines.findIndex(line => line === "This should be text, not executed!");
expect(shouldNotExecuteLine).toBe(-1);
});
test("base option works with bundler", async () => {
using dir = tempDir("import-glob-bundler-base", {
"index.js": `
const modules = import.meta.glob('./modules/*.js', { base: './src' });
const moduleKeys = Object.keys(modules).sort();
console.log('KEYS:', JSON.stringify(moduleKeys));
for (const [key, loader] of Object.entries(modules)) {
const result = await loader();
console.log(\`\${key}: \${result.default}\`);
}
`,
"src/modules/foo.js": `export default "foo-bundled";`,
"src/modules/bar.js": `export default "bar-bundled";`,
"modules/baz.js": `export default "baz-should-not-match";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--outfile", "bundle.js"],
env: bunEnv,
cwd: dir,
});
const buildExitCode = await buildProc.exited;
expect(buildExitCode).toBe(0);
await using runProc = Bun.spawn({
cmd: [bunExe(), "bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toBe(`KEYS: ["./modules/bar.js","./modules/foo.js"]
./modules/bar.js: bar-bundled
./modules/foo.js: foo-bundled
`);
});
test("base option with parent directory and bundler", async () => {
using dir = tempDir("import-glob-bundler-base-parent", {
"project/index.js": `
const modules = import.meta.glob('./*.js', { base: '../' });
const moduleKeys = Object.keys(modules).sort();
console.log('KEYS:', JSON.stringify(moduleKeys));
for (const [key, loader] of Object.entries(modules)) {
const result = await loader();
console.log(\`\${key}: \${result.default}\`);
}
`,
"module1.js": `export default "module1-value";`,
"module2.js": `export default "module2-value";`,
"project/local.js": `export default "should-not-match";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "project/index.js", "--outfile", "bundle.js"],
env: bunEnv,
cwd: dir,
});
const buildExitCode = await buildProc.exited;
expect(buildExitCode).toBe(0);
await using runProc = Bun.spawn({
cmd: [bunExe(), "bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toBe(`KEYS: ["./module1.js","./module2.js"]
./module1.js: module1-value
./module2.js: module2-value
`);
});
test("negative patterns work with bundler", async () => {
const dir = tempDirWithFiles("import-glob-bundler-negative", {
"index.js": `
const all = import.meta.glob('./dir/*.js');
const filtered = import.meta.glob(['./dir/*.js', '!**/bar.js']);
console.log('ALL:', JSON.stringify(Object.keys(all).sort()));
console.log('FILTERED:', JSON.stringify(Object.keys(filtered).sort()));
for (const [key, loader] of Object.entries(filtered)) {
const result = await loader();
console.log(\`\${key}: \${result.default}\`);
}
`,
"dir/foo.js": `export default "foo";`,
"dir/bar.js": `export default "bar";`,
"dir/baz.js": `export default "baz";`,
});
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--outfile", "bundle.js"],
env: bunEnv,
cwd: dir,
});
const buildExitCode = await buildProc.exited;
expect(buildExitCode).toBe(0);
expect(await Bun.file(path.join(dir, "bundle.js")).text()).not.toContain("glob");
await using runProc = Bun.spawn({
cmd: [bunExe(), "bundle.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[0].split(": ")[1])).toEqual(["./dir/bar.js", "./dir/baz.js", "./dir/foo.js"]);
expect(JSON.parse(lines[1].split(": ")[1])).toEqual(["./dir/baz.js", "./dir/foo.js"]);
expect(lines).toContain("./dir/foo.js: foo");
expect(lines).toContain("./dir/baz.js: baz");
expect(stdout).not.toContain("./dir/bar.js: bar");
});
});
});

View File

@@ -146,3 +146,5 @@ vendor/elysia/test/validator/body.test.ts
vendor/elysia/test/ws/message.test.ts
test/js/node/test/parallel/test-worker-abort-on-uncaught-exception.js
test/js/bun/glob/import-meta-glob.test.ts
test/bundler/bundler_loader.test.ts