diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 3900aaadd0..8fb2da993b 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3873,7 +3873,6 @@ declare module "bun" { * The default loader for this file extension */ loader: Loader; - /** * Defer the execution of this callback until all other modules have been parsed. * diff --git a/src/bake/bake.d.ts b/src/bake/bake.d.ts index 732cf3d41a..e3a5c1f181 100644 --- a/src/bake/bake.d.ts +++ b/src/bake/bake.d.ts @@ -492,13 +492,23 @@ declare module "bun" { app?: Bake.Options | undefined; } - declare interface Plug { + declare interface PluginBuilder { /** * Inject a module into the development server's runtime, to be loaded * before all other user code. */ addPreload(module: string, side: 'client' | 'server'): void; } + + declare interface OnLoadArgs { + /** + * When using server-components, the same bundle has both client and server + * files; A single plugin can operate on files from both module graphs. + * Outside of server-components, this will be "client" when the target is + * set to "browser" and "server" otherwise. + */ + side: 'server' | 'client'; + } } /** Available in server-side files only. */ @@ -557,6 +567,7 @@ declare module "bun:bake/server" { }; }; } + } /** Available in client-side files. */ diff --git a/src/bake/bake.zig b/src/bake/bake.zig index ece698999b..8cb557a7a2 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -380,11 +380,14 @@ pub const Framework = struct { return global.throwInvalidArguments("Missing 'framework.serverComponents.serverRuntimeImportSource'", .{}); }, ), - .server_register_client_reference = refs.track( - try sc.getOptional(global, "serverRegisterClientReferenceExport", ZigString.Slice) orelse { - return global.throwInvalidArguments("Missing 'framework.serverComponents.serverRegisterClientReferenceExport'", .{}); - }, - ), + .server_register_client_reference = if (try sc.getOptional( + global, + "serverRegisterClientReferenceExport", + ZigString.Slice, + )) |slice| + refs.track(slice) + else + "registerClientReference", }; }; const built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = built_in_modules: { @@ -456,9 +459,9 @@ pub const Framework = struct { break :exts &.{}; } } else if (exts_js.isArray()) { - var it_2 = array.arrayIterator(global); + var it_2 = exts_js.arrayIterator(global); var i_2: usize = 0; - const extensions = try arena.alloc([]const u8, len); + const extensions = try arena.alloc([]const u8, array.getLength(global)); while (it_2.next()) |array_item| : (i_2 += 1) { const slice = refs.track(try array_item.toSlice2(global, arena)); if (bun.strings.eqlComptime(slice, "*")) diff --git a/src/bake/hmr-module.ts b/src/bake/hmr-module.ts index 69773c9655..017283fd6a 100644 --- a/src/bake/hmr-module.ts +++ b/src/bake/hmr-module.ts @@ -39,6 +39,7 @@ export class HotModule { _cached_failure: any = undefined; // modules that import THIS module _deps: Map = new Map(); + _onDispose: HotDisposeFunction[] | undefined = undefined; constructor(id: Id) { this.id = id; @@ -55,13 +56,14 @@ export class HotModule { mod._deps.set(this, onReload ? { _callback: onReload, _expectedImports: expectedImports } : undefined); const { exports, __esModule } = mod; const object = __esModule ? exports : (mod._ext_exports ??= { ...exports, default: exports }); - if (expectedImports) { - for (const key of expectedImports) { - if (!(key in object)) { - throw new SyntaxError(`The requested module '${id}' does not provide an export named '${key}'`); - } - } - } + // TODO: restore this validation. not correct due to import cycles which are allowed usually + // if (expectedImports) { + // for (const key of expectedImports) { + // if (!(key in object)) { + // throw new SyntaxError(`The requested module '${id}' does not provide an export named '${key}'`); + // } + // } + // } return object; } @@ -93,14 +95,73 @@ function initImportMeta(m: HotModule): ImportMeta { url: `bun://${m.id}`, main: false, // @ts-ignore - hot: { - accept() { - throw new Error("Not implemented"); - }, - } + get hot() { + const hot = new Hot(m); + Object.defineProperty(this, "hot", { value: hot }); + return hot; + }, }; } +type HotAcceptFunction = (esmExports: any | void) => void; +type HotArrayAcceptFunction = (esmExports: (any | void)[]) => void; +type HotDisposeFunction = (data: any) => void; +type HotEventHandler = (data: any) => void; + +class Hot { + private _module: HotModule; + + constructor(module: HotModule) { + this._module = module; + } + + accept( + arg1: string | readonly string[] | HotAcceptFunction, + arg2: HotAcceptFunction | HotArrayAcceptFunction | undefined, + ) { + console.warn("TODO: implement ImportMetaHot.accept (called from " + JSON.stringify(this._module.id) + ")"); + } + + dispose(cb: HotDisposeFunction) { + (this._module._onDispose ??= []).push(cb); + } + + prune(cb: HotDisposeFunction) { + throw new Error("TODO: implement ImportMetaHot.prune"); + } + + invalidate() { + throw new Error("TODO: implement ImportMetaHot.invalidate"); + } + + on(event: string, cb: HotEventHandler) { + if (isUnsupportedViteEventName(event)) { + throw new Error(`Unsupported event name: ${event}`); + } + + throw new Error("TODO: implement ImportMetaHot.on"); + } + + off(event: string, cb: HotEventHandler) { + throw new Error("TODO: implement ImportMetaHot.off"); + } + + send(event: string, cb: HotEventHandler) { + throw new Error("TODO: implement ImportMetaHot.send"); + } +} + +function isUnsupportedViteEventName(str: string) { + return str === 'vite:beforeUpdate' + || str === 'vite:afterUpdate' + || str === 'vite:beforeFullReload' + || str === 'vite:beforePrune' + || str === 'vite:invalidate' + || str === 'vite:error' + || str === 'vite:ws:disconnect' + || str === 'vite:ws:connect'; +} + /** * Load a module by ID. Use `type` to specify if the module is supposed to be * present, or is something a user is able to dynamically specify. diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 850e52b3a0..c44009e9d0 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -771,6 +771,7 @@ pub const JSBundler = struct { load.namespace, load, load.default_loader, + load.bakeGraph() != .client, ); } @@ -881,6 +882,7 @@ pub const JSBundler = struct { path: *const String, context: *anyopaque, u8, + bool, ) void; extern fn JSBundlerPlugin__matchOnResolve( @@ -921,6 +923,7 @@ pub const JSBundler = struct { namespace: []const u8, context: *anyopaque, default_loader: options.Loader, + is_server_side: bool, ) void { JSC.markBinding(@src()); const tracer = bun.tracy.traceNamed(@src(), "JSBundler.matchOnLoad"); @@ -933,7 +936,7 @@ pub const JSBundler = struct { const path_string = bun.String.createUTF8(path); defer namespace_string.deref(); defer path_string.deref(); - JSBundlerPlugin__matchOnLoad(this, &namespace_string, &path_string, context, @intFromEnum(default_loader)); + JSBundlerPlugin__matchOnLoad(this, &namespace_string, &path_string, context, @intFromEnum(default_loader), is_server_side); } pub fn matchOnResolve( diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp index 8e0a9f3837..47ccf6048b 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.cpp +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -325,7 +325,7 @@ extern "C" bool JSBundlerPlugin__anyMatches(Bun::JSBundlerPlugin* pluginObject, return pluginObject->plugin.anyMatchesCrossThread(pluginObject->vm(), namespaceString, path, isOnLoad); } -extern "C" void JSBundlerPlugin__matchOnLoad(Bun::JSBundlerPlugin* plugin, const BunString* namespaceString, const BunString* path, void* context, uint8_t defaultLoaderId) +extern "C" void JSBundlerPlugin__matchOnLoad(Bun::JSBundlerPlugin* plugin, const BunString* namespaceString, const BunString* path, void* context, uint8_t defaultLoaderId, bool isServerSide) { JSC::JSGlobalObject *globalObject = plugin->globalObject(); WTF::String namespaceStringStr = namespaceString ? namespaceString->toWTFString(BunString::ZeroCopy) : WTF::String(); @@ -346,6 +346,7 @@ extern "C" void JSBundlerPlugin__matchOnLoad(Bun::JSBundlerPlugin* plugin, const arguments.append(JSC::jsString(plugin->vm(), pathStr)); arguments.append(JSC::jsString(plugin->vm(), namespaceStringStr)); arguments.append(JSC::jsNumber(defaultLoaderId)); + arguments.append(JSC::jsBoolean(isServerSide)); call(globalObject, function, callData, plugin, arguments); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index b3a0b91da9..df80522e28 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2412,7 +2412,8 @@ pub const ModuleLoader = struct { else specifier[@min(namespace.len + 1, specifier.len)..]; - return globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), .bun) orelse return JSValue.zero; + return globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), .bun) orelse + return JSValue.zero; } pub fn fetchBuiltinModule(jsc_vm: *VirtualMachine, specifier: bun.String) !?ResolvedSource { diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index b1ca7f16ea..7b248081d6 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -22,7 +22,7 @@ interface BundlerPlugin { /** Binding to `JSBundlerPlugin__addError` */ addError(internalID: number, error: any, which: number): void; addFilter(filter, namespace, number): void; - generateDeferPromise(): Promise; + generateDeferPromise(id: number): Promise; promises: Array> | undefined; } @@ -335,12 +335,12 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa } } -export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId) { +export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId, isServerSide: boolean) { const LOADERS_MAP = $LoaderLabelToId; const loaderName = $LoaderIdToLabel[defaultLoaderId]; const generateDefer = () => this.generateDeferPromise(internalID); - var promiseResult = (async (internalID, path, namespace, defaultLoader, generateDefer) => { + var promiseResult = (async (internalID, path, namespace, isServerSide, defaultLoader, generateDefer) => { var results = this.onLoad.$get(namespace); if (!results) { this.onLoadAsync(internalID, null, null); @@ -356,6 +356,7 @@ export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespac // pluginData loader: defaultLoader, defer: generateDefer, + side: isServerSide ? "server" : "client", }); while ( @@ -407,7 +408,7 @@ export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespac this.onLoadAsync(internalID, null, null); return null; - })(internalID, path, namespace, loaderName, generateDefer); + })(internalID, path, namespace, isServerSide, loaderName, generateDefer); while ( promiseResult && diff --git a/src/js_parser.zig b/src/js_parser.zig index ad1ff24c6f..1d5fc2d2b8 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -8986,64 +8986,62 @@ fn NewParser_( try p.is_import_item.ensureUnusedCapacity(p.allocator, count_excluding_namespace); var remap_count: u32 = 0; // Link the default item to the namespace - if (stmt.default_name) |*name_loc| { - outer: { - const name = p.loadNameFromRef(name_loc.ref.?); - const ref = try p.declareSymbol(.import, name_loc.loc, name); - name_loc.ref = ref; - try p.is_import_item.put(p.allocator, ref, {}); + if (stmt.default_name) |*name_loc| outer: { + const name = p.loadNameFromRef(name_loc.ref.?); + const ref = try p.declareSymbol(.import, name_loc.loc, name); + name_loc.ref = ref; + try p.is_import_item.put(p.allocator, ref, {}); - // ensure every e_import_identifier holds the namespace - if (p.options.features.hot_module_reloading) { - const symbol = &p.symbols.items[ref.inner_index]; - if (symbol.namespace_alias == null) { - symbol.namespace_alias = .{ - .namespace_ref = stmt.namespace_ref, - .alias = "default", - .import_record_index = stmt.import_record_index, - }; - } - } - - if (macro_remap) |*remap| { - if (remap.get("default")) |remapped_path| { - const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path); - try p.macro.refs.put(ref, new_import_id); - - p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace; - p.import_records.items[new_import_id].is_unused = true; - 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; - } - stmt.default_name = null; - remap_count += 1; - break :outer; - } - } - - if (comptime track_symbol_usage_during_parse_pass) { - p.parse_pass_symbol_uses.put(name, .{ - .ref = ref, + // ensure every e_import_identifier holds the namespace + if (p.options.features.hot_module_reloading) { + const symbol = &p.symbols.items[ref.inner_index]; + if (symbol.namespace_alias == null) { + symbol.namespace_alias = .{ + .namespace_ref = stmt.namespace_ref, + .alias = "default", .import_record_index = stmt.import_record_index, - }) catch unreachable; + }; } + } - if (is_macro) { - try p.macro.refs.put(ref, stmt.import_record_index); + if (macro_remap) |*remap| { + if (remap.get("default")) |remapped_path| { + const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path); + try p.macro.refs.put(ref, new_import_id); + + p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace; + p.import_records.items[new_import_id].is_unused = true; + 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; + } stmt.default_name = null; + remap_count += 1; break :outer; } - - if (comptime ParsePassSymbolUsageType != void) { - p.parse_pass_symbol_uses.put(name, .{ - .ref = ref, - .import_record_index = stmt.import_record_index, - }) catch unreachable; - } - - item_refs.putAssumeCapacity(name, name_loc.*); } + + if (comptime track_symbol_usage_during_parse_pass) { + p.parse_pass_symbol_uses.put(name, .{ + .ref = ref, + .import_record_index = stmt.import_record_index, + }) catch unreachable; + } + + if (is_macro) { + try p.macro.refs.put(ref, stmt.import_record_index); + stmt.default_name = null; + break :outer; + } + + if (comptime ParsePassSymbolUsageType != void) { + p.parse_pass_symbol_uses.put(name, .{ + .ref = ref, + .import_record_index = stmt.import_record_index, + }) catch unreachable; + } + + item_refs.putAssumeCapacity(name, name_loc.*); } var end: usize = 0; @@ -12469,7 +12467,6 @@ fn NewParser_( fn declareSymbolMaybeGenerated(p: *P, kind: Symbol.Kind, loc: logger.Loc, name: string, comptime is_generated: bool) !Ref { // p.checkForNonBMPCodePoint(loc, name) - if (comptime !is_generated) { // Forbid declaring a symbol with a reserved word in strict mode if (p.isStrictMode() and name.ptr != arguments_str.ptr and js_lexer.StrictModeReservedWords.has(name)) { @@ -19046,16 +19043,6 @@ fn NewParser_( } }, .s_export_from => |data| { - // When HMR is enabled, we need to transform this into - // import {foo} from "./foo"; - // export {foo}; - - // From: - // export {foo as default} from './foo'; - // To: - // import {default as foo} from './foo'; - // export {foo}; - // "export {foo} from 'path'" const name = p.loadNameFromRef(data.namespace_ref); @@ -19079,7 +19066,7 @@ fn NewParser_( const _name = p.loadNameFromRef(old_ref); - const ref = try p.newSymbol(.other, _name); + const ref = try p.newSymbol(.import, _name); try p.current_scope.generated.push(p.allocator, ref); try p.recordDeclaredSymbol(ref); data.items[j] = item; @@ -19093,11 +19080,10 @@ fn NewParser_( return; } } else { - // This is a re-export and the symbols created here are used to reference for (data.items) |*item| { const _name = p.loadNameFromRef(item.name.ref.?); - const ref = try p.newSymbol(.other, _name); + const ref = try p.newSymbol(.import, _name); try p.current_scope.generated.push(p.allocator, ref); try p.recordDeclaredSymbol(ref); item.name.ref = ref; @@ -23961,19 +23947,37 @@ pub const ConvertESMExportsForHmr = struct { }, .s_export_clause => |st| { for (st.items) |item| { - try ctx.export_props.append(p.allocator, .{ - .key = Expr.init(E.String, .{ - .data = item.alias, - }, stmt.loc), - .value = Expr.initIdentifier(item.name.ref.?, item.name.loc), - }); + const ref = item.name.ref.?; + try ctx.visitRefForBakeModuleExports(p, ref, item.name.loc, false); } return; // do not emit a statement here }, - .s_export_from => { - bun.todoPanic(@src(), "hot-module-reloading instrumentation for 'export {{ ... }} from'", .{}); + .s_export_from => |st| stmt: { + for (st.items) |item| { + const ref = item.name.ref.?; + const symbol = &p.symbols.items[ref.innerIndex()]; + if (symbol.namespace_alias == null) { + symbol.namespace_alias = .{ + .namespace_ref = st.namespace_ref, + .alias = item.original_name, + .import_record_index = st.import_record_index, + }; + } + try ctx.visitRefForBakeModuleExports(p, ref, item.name.loc, true); + } + + const gop = try ctx.imports_seen.getOrPut(p.allocator, st.import_record_index); + if (gop.found_existing) return; + break :stmt Stmt.alloc(S.Import, .{ + .import_record_index = st.import_record_index, + .is_single_line = true, + .default_name = null, + .items = st.items, + .namespace_ref = st.namespace_ref, + .star_name_loc = null, + }, stmt.loc); }, .s_export_star => { bun.todoPanic(@src(), "hot-module-reloading instrumentation for 'export * from'", .{}); @@ -24001,7 +24005,7 @@ pub const ConvertESMExportsForHmr = struct { switch (binding.data) { .b_missing => {}, .b_identifier => |id| { - try ctx.visitRefForKitModuleExports(p, id.ref, binding.loc, is_live_binding); + try ctx.visitRefForBakeModuleExports(p, id.ref, binding.loc, is_live_binding); }, .b_array => |array| { for (array.items) |item| { @@ -24016,16 +24020,24 @@ pub const ConvertESMExportsForHmr = struct { } } - fn visitRefForKitModuleExports( + fn visitRefForBakeModuleExports( ctx: *ConvertESMExportsForHmr, p: anytype, ref: Ref, loc: logger.Loc, - is_live_binding: bool, + is_live_binding_source: bool, ) !void { const symbol = p.symbols.items[ref.inner_index]; - const id = Expr.initIdentifier(ref, loc); - if (is_live_binding) { + std.debug.print("yolo {s}, {s}\n", .{ symbol.original_name, @tagName(symbol.kind) }); + const id = if (symbol.kind == .import) + Expr.init(E.ImportIdentifier, .{ .ref = ref }, loc) + else + Expr.initIdentifier(ref, loc); + if (is_live_binding_source or symbol.kind == .import) { + // TODO: instead of requiring getters for live-bindings, + // a callback propagation system should be considered. + // mostly because here, these might not even be live + // bindings, and re-exports are so, so common. const key = Expr.init(E.String, .{ .data = symbol.original_name, }, loc); @@ -24052,24 +24064,7 @@ pub const ConvertESMExportsForHmr = struct { }, } }, loc), }); - // 'set abc(abc2) { abc = abc2 }' - try ctx.export_props.append(p.allocator, .{ - .kind = .set, - .key = key, - .value = Expr.init(E.Function, .{ .func = .{ - .args = try p.allocator.dupe(G.Arg, &.{.{ - .binding = Binding.alloc(p.allocator, B.Identifier{ .ref = arg1 }, loc), - }}), - .body = .{ - .stmts = try p.allocator.dupe(Stmt, &.{ - Stmt.alloc(S.SExpr, .{ - .value = Expr.assign(id, Expr.initIdentifier(arg1, loc)), - }, loc), - }), - .loc = loc, - }, - } }, loc), - }); + // no setter is added since live bindings are read-only } else { // 'abc,' try ctx.export_props.append(p.allocator, .{ diff --git a/test/bake/dev-server-harness.ts b/test/bake/dev-server-harness.ts index 147b3755a0..04e46ed64c 100644 --- a/test/bake/dev-server-harness.ts +++ b/test/bake/dev-server-harness.ts @@ -293,7 +293,7 @@ class OutputLineStream extends EventEmitter { } } -export function devTest(description: string, options: DevServerTest) { +export function devTest(description: string, options: T): T { // Capture the caller name as part of the test tempdir const callerLocation = snapshotCallerLocation(); const caller = stackTraceFileName(callerLocation); @@ -305,7 +305,7 @@ export function devTest(description: string, options: DevServerTest) { // TODO: Tests are too flaky on Windows. Cannot reproduce locally. if (isWindows) { jest.test.todo(`DevServer > ${basename}.${count}: ${description}`); - return; + return options; } jest.test(`DevServer > ${basename}.${count}: ${description}`, async () => { @@ -377,4 +377,5 @@ export function devTest(description: string, options: DevServerTest) { throw err; } }); + return options; } diff --git a/test/bake/dev/dev-plugins.test.ts b/test/bake/dev/dev-plugins.test.ts index ed13e08135..863ee783e1 100644 --- a/test/bake/dev/dev-plugins.test.ts +++ b/test/bake/dev/dev-plugins.test.ts @@ -43,8 +43,7 @@ devTest("onLoad", { setup(build) { let a = 0; build.onLoad({ filter: /trigger/ }, (args) => { - a += 1; - return { contents: 'export const value = ' + a + ';', loader: 'ts' }; + return { contents: 'export const value = 1;', loader: 'ts' }; }); }, } @@ -65,8 +64,43 @@ devTest("onLoad", { async test(dev) { await dev.fetch("/").expect('value: 1'); await dev.fetch("/").expect('value: 1'); - await dev.write("trigger.ts", "throw new Error('should not be loaded 2');"); - await dev.fetch("/").expect('value: 2'); - await dev.fetch("/").expect('value: 2'); + await dev.fetch("/").expect('value: 1'); }, }); +// devTest("onLoad with watchFile", { +// framework: minimalFramework, +// pluginFile: ` +// import * as path from 'path'; +// export default [ +// { +// name: 'a', +// setup(build) { +// let a = 0; +// build.onLoad({ filter: /trigger/ }, (args) => { +// a += 1; +// return { contents: 'export const value = ' + a + ';', loader: 'ts' }; +// }); +// }, +// } +// ]; +// `, +// files: { +// "trigger.ts": ` +// throw new Error('should not be loaded'); +// `, +// "routes/index.ts": ` +// import { value } from '../trigger.ts'; + +// export default function (req, meta) { +// return new Response('value: ' + value); +// } +// `, +// }, +// async test(dev) { +// await dev.fetch("/").expect('value: 1'); +// await dev.fetch("/").expect('value: 1'); +// await dev.write("trigger.ts", "throw new Error('should not be loaded 2');"); +// await dev.fetch("/").expect('value: 2'); +// await dev.fetch("/").expect('value: 2'); +// }, +// }); diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index bed852bc4b..61eba97e22 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -1,7 +1,7 @@ // Bundle tests are tests concerning bundling bugs that only occur in DevServer. import { devTest, minimalFramework, Step } from "../dev-server-harness"; -devTest("live bindings with `var`", { +const liveBindingTest = devTest("live bindings with `var`", { framework: minimalFramework, files: { "state.ts": ` @@ -41,3 +41,73 @@ devTest("live bindings with `var`", { await dev.fetch("/").expect("Value: -2"); }, }); +devTest("live bindings through export clause", { + framework: minimalFramework, + files: { + "state.ts": ` + export var value = 0; + export function increment() { + value++; + } + `, + "proxy.ts": ` + import { value } from './state'; + export { value as live }; + `, + "routes/index.ts": ` + import { increment } from '../state'; + import { live } from '../proxy'; + export default function(req, meta) { + increment(); + return new Response('State: ' + live); + } + `, + }, + test: liveBindingTest.test, +}); +devTest("live bindings through export from", { + framework: minimalFramework, + files: { + "state.ts": ` + export var value = 0; + export function increment() { + value++; + } + `, + "proxy.ts": ` + export { value as live } from './state'; + `, + "routes/index.ts": ` + import { increment } from '../state'; + import { live } from '../proxy'; + export default function(req, meta) { + increment(); + return new Response('State: ' + live); + } + `, + }, + test: liveBindingTest.test, +}); +devTest("live bindings through export star", { + framework: minimalFramework, + files: { + "state.ts": ` + export var value = 0; + export function increment() { + value++; + } + `, + "proxy.ts": ` + export * from './state'; + `, + "routes/index.ts": ` + import { increment } from '../state'; + import { live } from '../proxy'; + export default function(req, meta) { + increment(); + return new Response('State: ' + live); + } + `, + }, + test: liveBindingTest.test, +}); \ No newline at end of file