inital groundwork for import defer in parser

This commit is contained in:
Alistair Smith
2025-07-08 11:54:22 -07:00
parent 454316ffc3
commit da12e144af
4 changed files with 231 additions and 1 deletions

View File

@@ -163,6 +163,7 @@ pub const Import = struct {
star_name_loc: ?logger.Loc = null,
import_record_index: u32,
is_single_line: bool = false,
is_deferred: bool = false,
};
pub const Return = struct { value: ?ExprNodeIndex = null };

View File

@@ -134,6 +134,9 @@ pub const ImportRecord = struct {
/// out to be type-only imports after analyzing the whole file.
is_unused: bool = false,
/// True if this import uses the "defer" keyword
is_deferred: bool = false,
/// 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.

View File

@@ -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].is_deferred = stmt.is_deferred;
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].is_deferred = stmt.is_deferred;
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].is_deferred = stmt.is_deferred;
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,12 @@ fn NewParser_(
};
var was_originally_bare_import = false;
// Check for "import defer" syntax
if (p.lexer.isContextualKeyword("defer")) {
stmt.is_deferred = true;
try p.lexer.next();
}
// "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 +10352,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 +10364,7 @@ fn NewParser_(
.namespace_ref = try p.storeNameInRef(p.lexer.identifier),
.star_name_loc = p.lexer.loc(),
.import_record_index = std.math.maxInt(u32),
.is_deferred = stmt.is_deferred,
};
try p.lexer.expect(.t_identifier);
try p.lexer.expectContextualKeyword("from");
@@ -10486,6 +10496,12 @@ fn NewParser_(
},
}
// Validate that deferred imports are only allowed with star imports
if (stmt.is_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;
}
const path = try p.parsePath();
try p.lexer.expectOrInsertSemicolon();

View File

@@ -0,0 +1,210 @@
import { test, expect, describe } from "bun:test";
import { tempDirWithFiles, bunExe, bunEnv } 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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const error = await proc.stderr.text();
const exitCode = await proc.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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const error = await proc.stderr.text();
const exitCode = await proc.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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const output = await proc.stdout.text();
const error = await proc.stderr.text();
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
expect(error).toBe("");
// The order should be: "Before access", then "Module evaluated!", then "After access: 42"
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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const error = await proc.stderr.text();
const exitCode = await proc.exited;
// According to the TC39 spec, modules with top-level await cannot be deferred
// The implementation should either reject this or handle it appropriately
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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const output = await proc.stdout.text();
const error = await proc.stderr.text();
const exitCode = await proc.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 proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const output = await proc.stdout.text();
const error = await proc.stderr.text();
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
expect(error).toBe("");
const lines = output.trim().split("\n");
expect(lines).toContain("a");
expect(lines).toContain("b");
});
});