mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
9 Commits
dylan/pyth
...
ali/import
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32f32d881a | ||
|
|
2912207090 | ||
|
|
a3869f7e19 | ||
|
|
85baf73b8d | ||
|
|
e7c5100f00 | ||
|
|
e6d2cb96ae | ||
|
|
0dfc2345fa | ||
|
|
0b243855be | ||
|
|
da12e144af |
@@ -163,6 +163,7 @@ pub const Import = struct {
|
||||
star_name_loc: ?logger.Loc = null,
|
||||
import_record_index: u32,
|
||||
is_single_line: bool = false,
|
||||
phase: bun.ImportRecord.ImportPhase = .none,
|
||||
};
|
||||
|
||||
pub const Return = struct { value: ?ExprNodeIndex = null };
|
||||
|
||||
@@ -107,6 +107,15 @@ pub const ImportKind = enum(u8) {
|
||||
pub const ImportRecord = struct {
|
||||
pub const Index = bun.GenericIndex(u32, ImportRecord);
|
||||
|
||||
// none = default behaviour
|
||||
// deferred = https://github.com/tc39/proposal-defer-import-eval
|
||||
// source = https://github.com/tc39/proposal-source-phase-imports
|
||||
pub const ImportPhase = enum {
|
||||
none,
|
||||
deferred,
|
||||
source, // Not implemented yet, just laying groundwork
|
||||
};
|
||||
|
||||
range: logger.Range,
|
||||
path: fs.Path,
|
||||
kind: ImportKind,
|
||||
@@ -134,6 +143,8 @@ pub const ImportRecord = struct {
|
||||
/// out to be type-only imports after analyzing the whole file.
|
||||
is_unused: bool = false,
|
||||
|
||||
phase: ImportPhase = .none,
|
||||
|
||||
/// If this is true, the import contains syntax like "* as ns". This is used
|
||||
/// to determine whether modules that have no exports need to be wrapped in a
|
||||
/// CommonJS wrapper or not.
|
||||
|
||||
@@ -8932,6 +8932,7 @@ fn NewParser_(
|
||||
|
||||
stmt.import_record_index = p.addImportRecord(.stmt, path.loc, path.text);
|
||||
p.import_records.items[stmt.import_record_index].was_originally_bare_import = was_originally_bare_import;
|
||||
p.import_records.items[stmt.import_record_index].phase = stmt.phase;
|
||||
|
||||
if (stmt.star_name_loc) |star| {
|
||||
const name = p.loadNameFromRef(stmt.namespace_ref);
|
||||
@@ -8993,6 +8994,7 @@ fn NewParser_(
|
||||
|
||||
p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace;
|
||||
p.import_records.items[new_import_id].is_unused = true;
|
||||
p.import_records.items[new_import_id].phase = stmt.phase;
|
||||
if (comptime only_scan_imports_and_do_not_visit) {
|
||||
p.import_records.items[new_import_id].is_internal = true;
|
||||
p.import_records.items[new_import_id].path.is_disabled = true;
|
||||
@@ -9052,6 +9054,7 @@ fn NewParser_(
|
||||
|
||||
p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace;
|
||||
p.import_records.items[new_import_id].is_unused = true;
|
||||
p.import_records.items[new_import_id].phase = stmt.phase;
|
||||
if (comptime only_scan_imports_and_do_not_visit) {
|
||||
p.import_records.items[new_import_id].is_internal = true;
|
||||
p.import_records.items[new_import_id].path.is_disabled = true;
|
||||
@@ -10318,6 +10321,15 @@ fn NewParser_(
|
||||
};
|
||||
var was_originally_bare_import = false;
|
||||
|
||||
if (p.lexer.isContextualKeyword("defer")) {
|
||||
stmt.phase = .deferred;
|
||||
try p.lexer.next();
|
||||
}
|
||||
|
||||
// if (p.lexer.isContextualKeyword("source")) {
|
||||
// // TODO: import source phase
|
||||
// }
|
||||
|
||||
// "export import foo = bar"
|
||||
if ((opts.is_export or (opts.is_namespace_scope and !opts.is_typescript_declare)) and p.lexer.token != .t_identifier) {
|
||||
try p.lexer.expected(.t_identifier);
|
||||
@@ -10343,7 +10355,7 @@ fn NewParser_(
|
||||
was_originally_bare_import = true;
|
||||
},
|
||||
.t_asterisk => {
|
||||
// "import * as ns from 'path'"
|
||||
// "import * as ns from 'path'" or "import defer * as ns from 'path'"
|
||||
if (!opts.is_module_scope and (!opts.is_namespace_scope or !opts.is_typescript_declare)) {
|
||||
try p.lexer.unexpected();
|
||||
return error.SyntaxError;
|
||||
@@ -10355,6 +10367,7 @@ fn NewParser_(
|
||||
.namespace_ref = try p.storeNameInRef(p.lexer.identifier),
|
||||
.star_name_loc = p.lexer.loc(),
|
||||
.import_record_index = std.math.maxInt(u32),
|
||||
.phase = stmt.phase,
|
||||
};
|
||||
try p.lexer.expect(.t_identifier);
|
||||
try p.lexer.expectContextualKeyword("from");
|
||||
@@ -10380,6 +10393,7 @@ fn NewParser_(
|
||||
.import_record_index = std.math.maxInt(u32),
|
||||
.items = importClause.items,
|
||||
.is_single_line = importClause.is_single_line,
|
||||
.phase = stmt.phase,
|
||||
};
|
||||
try p.lexer.expectContextualKeyword("from");
|
||||
},
|
||||
@@ -10395,7 +10409,7 @@ fn NewParser_(
|
||||
stmt = S.Import{ .namespace_ref = Ref.None, .import_record_index = std.math.maxInt(u32), .default_name = LocRef{
|
||||
.loc = p.lexer.loc(),
|
||||
.ref = try p.storeNameInRef(default_name),
|
||||
} };
|
||||
}, .phase = stmt.phase };
|
||||
try p.lexer.next();
|
||||
|
||||
if (comptime is_typescript_enabled) {
|
||||
@@ -10486,6 +10500,17 @@ fn NewParser_(
|
||||
},
|
||||
}
|
||||
|
||||
// Validate that deferred imports are only allowed with star imports
|
||||
if (stmt.phase == .deferred and stmt.star_name_loc == null) {
|
||||
try p.log.addError(p.source, loc, "The 'defer' keyword can only be used with star imports: import defer * as ns from 'module'");
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
// if (stmt.phase == .source and stmt.star_name_loc == null) {
|
||||
// try p.log.addError(p.source, loc, "The 'source' keyword can only be used with star imports: import source * as ns from 'module'");
|
||||
// return error.SyntaxError;
|
||||
// }
|
||||
|
||||
const path = try p.parsePath();
|
||||
try p.lexer.expectOrInsertSemicolon();
|
||||
|
||||
|
||||
@@ -1427,4 +1427,124 @@ describe("bundler", () => {
|
||||
stdout: '{"inner":{"b":456},"a":123}',
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferStarValid", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer * as ns from './foo'
|
||||
let foo = 234
|
||||
console.log(foo)
|
||||
`,
|
||||
"/foo.js": `export const foo = "UNUSED"`,
|
||||
},
|
||||
dce: true,
|
||||
run: {
|
||||
stdout: "234",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferStarUsed", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer * as ns from './foo'
|
||||
console.log(ns.foo)
|
||||
`,
|
||||
"/foo.js": `export const foo = 123`,
|
||||
},
|
||||
run: {
|
||||
stdout: "123",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferNamedImportError", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer { foo } from './foo'
|
||||
console.log(foo)
|
||||
`,
|
||||
"/foo.js": `export const foo = 123`,
|
||||
},
|
||||
bundleErrors: {
|
||||
"/entry.js": ["The 'defer' keyword can only be used with star imports: import defer * as ns from 'module'"],
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferDefaultImportError", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer foo from './foo'
|
||||
console.log(foo)
|
||||
`,
|
||||
"/foo.js": `export default 123`,
|
||||
},
|
||||
bundleErrors: {
|
||||
"/entry.js": ["The 'defer' keyword can only be used with star imports: import defer * as ns from 'module'"],
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferReExport", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer * as reexport from './reexport'
|
||||
console.log(reexport.value)
|
||||
`,
|
||||
"/reexport.js": `export { value } from './foo'`,
|
||||
"/foo.js": `export const value = 123`,
|
||||
},
|
||||
run: {
|
||||
stdout: "123",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferCircular", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import * as a from './a'
|
||||
console.log(a.getValue())
|
||||
`,
|
||||
"/a.js": /* js */ `
|
||||
import defer * as b from './b'
|
||||
export const aValue = "a"
|
||||
export function getValue() {
|
||||
return b.bValue
|
||||
}
|
||||
`,
|
||||
"/b.js": /* js */ `
|
||||
import defer * as a from './a'
|
||||
export const bValue = "b"
|
||||
export function getA() {
|
||||
return a.aValue
|
||||
}
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
stdout: "b",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferCommonJS", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defer * as ns from './foo'
|
||||
console.log(ns.foo)
|
||||
`,
|
||||
"/foo.js": `exports.foo = 123`,
|
||||
},
|
||||
run: {
|
||||
stdout: "123",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("importstar/ImportDeferExternalModule", {
|
||||
files: {
|
||||
"/entry.js": `import defer * as ext from 'external'; console.log(ext.value)`,
|
||||
},
|
||||
external: ["external"],
|
||||
runtimeFiles: {
|
||||
"/node_modules/external/index.js": `export const value = "external"`,
|
||||
},
|
||||
run: {
|
||||
stdout: "external",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
208
test/js/bun/import-defer.test.ts
Normal file
208
test/js/bun/import-defer.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
describe("import defer", () => {
|
||||
test("should parse import defer syntax", async () => {
|
||||
const dir = tempDirWithFiles("import-defer", {
|
||||
"utils.js": `
|
||||
export const helper = () => "hello";
|
||||
export const counter = { value: 0 };
|
||||
`,
|
||||
"main.js": `
|
||||
import defer * as utils from "./utils.js";
|
||||
|
||||
// Module should be loaded but not evaluated yet
|
||||
export function getUtils() {
|
||||
return utils; // This should trigger evaluation
|
||||
}
|
||||
|
||||
export function checkCounter() {
|
||||
return utils.counter.value;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const { stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(error).toBe("");
|
||||
});
|
||||
|
||||
test("should only allow defer with star imports", async () => {
|
||||
const dir = tempDirWithFiles("import-defer-error", {
|
||||
"utils.js": `export const helper = () => "hello";`,
|
||||
"main.js": `
|
||||
import defer { helper } from "./utils.js"; // This should be an error
|
||||
console.log(helper());
|
||||
`,
|
||||
});
|
||||
|
||||
const { stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(error).toContain("The 'defer' keyword can only be used with star imports");
|
||||
});
|
||||
|
||||
test("should defer module evaluation until property access", async () => {
|
||||
const dir = tempDirWithFiles("import-defer-eval", {
|
||||
"side-effect.js": `
|
||||
console.log("Module evaluated!");
|
||||
export const value = 42;
|
||||
`,
|
||||
"main.js": `
|
||||
import defer * as sideEffect from "./side-effect.js";
|
||||
|
||||
console.log("Before access");
|
||||
const result = sideEffect.value; // Should trigger evaluation here
|
||||
console.log("After access:", result);
|
||||
`,
|
||||
});
|
||||
|
||||
const { stdout, stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await stdout.text();
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(error).toBe("");
|
||||
|
||||
const lines = output.trim().split("\n");
|
||||
expect(lines[0]).toBe("Before access");
|
||||
expect(lines[1]).toBe("Module evaluated!");
|
||||
expect(lines[2]).toBe("After access: 42");
|
||||
});
|
||||
|
||||
test("should not defer modules with top-level await", async () => {
|
||||
const dir = tempDirWithFiles("import-defer-await", {
|
||||
"async-module.js": `
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
export const value = "async";
|
||||
`,
|
||||
"main.js": `
|
||||
import defer * as asyncModule from "./async-module.js"; // Should this be an error?
|
||||
console.log(asyncModule.value);
|
||||
`,
|
||||
});
|
||||
|
||||
const { stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(stderr).not.toBe(""); // TODO: Not sure what the error message should be yet
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("should handle re-exports from deferred modules", async () => {
|
||||
const dir = tempDirWithFiles("import-defer-reexport", {
|
||||
"base.js": `
|
||||
export const baseValue = "base";
|
||||
`,
|
||||
"reexport.js": `
|
||||
export { baseValue } from "./base.js";
|
||||
export const reexportValue = "reexport";
|
||||
`,
|
||||
"main.js": `
|
||||
import defer * as reexport from "./reexport.js";
|
||||
|
||||
console.log(reexport.baseValue);
|
||||
console.log(reexport.reexportValue);
|
||||
`,
|
||||
});
|
||||
|
||||
const { stdout, stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await stdout.text();
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(error).toBe("");
|
||||
expect(output).toContain("base");
|
||||
expect(output).toContain("reexport");
|
||||
});
|
||||
|
||||
test("should handle circular dependencies with deferred imports", async () => {
|
||||
const dir = tempDirWithFiles("import-defer-circular", {
|
||||
"a.js": `
|
||||
import defer * as b from "./b.js";
|
||||
export const aValue = "a";
|
||||
export function getB() {
|
||||
return b.bValue;
|
||||
}
|
||||
`,
|
||||
"b.js": `
|
||||
import defer * as a from "./a.js";
|
||||
export const bValue = "b";
|
||||
export function getA() {
|
||||
return a.aValue;
|
||||
}
|
||||
`,
|
||||
"main.js": `
|
||||
import * as a from "./a.js";
|
||||
import * as b from "./b.js";
|
||||
|
||||
console.log(a.aValue);
|
||||
console.log(b.bValue);
|
||||
console.log(a.getB());
|
||||
console.log(b.getA());
|
||||
`,
|
||||
});
|
||||
|
||||
const { stdout, stderr, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await stdout.text();
|
||||
const error = await stderr.text();
|
||||
const exitCode = await exited;
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(error).toBe("");
|
||||
|
||||
const lines = output.trim().split("\n");
|
||||
expect(lines).toContain("a");
|
||||
expect(lines).toContain("b");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user