Repository navigation
Conversation
…resolution paths A server-side import of an HTML file only became a manifest module plus a browser entry point when the import record was resolved by the bulk pass in resolve_import_records / process_resolve_queue. Records resolved one at a time (an onResolve plugin declining the import, an onResolve plugin returning the HTML file's path) and imports of in-memory `files:` entries bundled the HTML as an ordinary module of the server graph instead, so the import either failed with 'No matching export in "page.html" for import "default"' or, for require() and in-memory files, built into output that never had a manifest. Factor the decision into is_server_html_import, make generate_server_html_module return the manifest module's index, and add enqueue_server_html_import for the one-at-a-time paths: it creates the manifest module in the importing graph and parses the HTML file as a browser entry point unless the browser graph already has it.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 11 minutes. View limit details
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Shared FileMap import resolution src/bundler/bundle_v2.rs, test/bundler/bundler_files.test.ts |
FileMap imports now pass through the shared resolution loop. In-memory paths copy their pretty value from their text, and parse tasks use JSX options from the selected transpiler. Tests cover JSX options and text-loader selection. |
HTML build output and manifest handling test/bundler/bundler_files.test.ts |
Tests cover HTML manifests and generated assets, imports from disk entry points, repeated and distinct pages, loader-based HTML handling, and referenced JSON assets. |
Serving generated pages test/bundler/bundler_files.test.ts |
Tests build and run servers for Bun and Node targets and check responses for generated pages, scripts, and stylesheets. |
Suggested reviewers: jarred-sumner
Priority:
Merge Risk: 
Minimal · previous review d3663
-
No concrete merge-blocking risk is established by the reviewed changes.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description explains the problem, fix, scope, limitations, and verification results. It provides the information required by the repository template, although it uses custom headings instead of the exact template headings.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
|
Status: the branch has main merged in and now covers only the Reproduced on bun 1.4.2 and on a release build of main (179664e). The build is The |
|
I was working on the One thing that may be worth folding in here, or doing as a follow-up: the Branch with an alternative shape of the |
|
Heads up from #38621, which fixes the This branch adds the same assignment to |
…disk
Bun.build({ files }) resolved an import that hits the in-memory file map in
a separate branch of resolve_import_records that duplicated the tail of the
disk resolution and had drifted from it. Two things it did not do:
- importing an .html file into a server build did not turn the import into
an HTML import manifest: the output kept a literal import of the virtual
path and the page's scripts were bundled for the server target
- a url asset referenced from an in-memory .html file (for example
<link rel="manifest" href="./manifest.json">) kept its parsed loader
instead of being copied to the output as a file
A file map hit now only substitutes for the resolver's result and then goes
through the shared steps, keeping the two things that are specific to
virtual files: the jsx options come from the transpiler (there is no
tsconfig.json to consult) and the display path is the map key as-is.
|
Updated 3:12 AM PT - Oct 27th, 2026
✅ @robobun, your commit 3697c110c604b7eac91364ede03cbc497cbc1959 passed in 🧪 To try this PR locally: bunx bun-pr 38605That installs a local version of the PR into your bun-38605 --bun |
…side generate_server_html_module The two call sites that reserve their map slot with get_or_put now fill it the same way their non-HTML siblings do, and the two that use get-then-put call put; generate_server_html_module only creates the module.
|
#38635 now carries the restructure mentioned above (the |
…nifest module The linker wraps a module (or gives it its own chunk) based on the Require / Dynamic kind of the records importing it, and the printer emits the import from the same kind, so replacing the kind with HtmlManifest is what breaks import() and require() of an HTML file. The manifest wiring does not depend on it: the module is found through html_imports.server_source_indices and the browser graph's path map. Do not introduce the rewrite on the paths added here; the existing one on the bulk path is removed separately.
|
Re the Re #38621: thanks, that matches what I saw when probing ( |
Conflicts: - src/bundler/bundle_v2.rs, on_resolve: the non-HTML branch keeps the requested_file_loader call and the is_entry_point field from main. - src/bundler/bundle_v2.rs, resolve_import_records: the separate branch for in-memory files stays removed. The shared steps already use forced_jsx_development from main. - test/bundler/bundler_files.test.ts: keeps the tests of both sides.
…odule resolve_import_records cloned the jsx options of the build for each import record that the `files` map answers. A record whose module is already in the graph or in the resolve queue does not need them. The clone now happens where the parse task of a new module gets its jsx options, as it did before the in-memory branch was merged into the shared steps.
Without a manifest the bundle keeps an import of the page's path. No file is at that path, so the bundle exits at startup with "Cannot find module". The test builds to an outdir and runs the output.
The run_resolver and on_resolve hunks chose manifest or module from the `.html` extension before an onLoad plugin ran. A plugin that takes an .html import in onResolve and loads it as JS then got a manifest with no `index`. bun 1.4.2 gives that import the module of the plugin. Those routes need the choice after onLoad, so they leave this branch. What stays is the stand-in in resolve_import_records: an import that the `files` map answers takes the shared steps. is_server_html_import, enqueue_server_html_import and the changed signature of generate_server_html_module have no caller left that needs them. Main's text is back there. Tests: html-import-manifest.test.ts is main's file again. The merge had left a second readManifests in it that shadowed main's helper. The two plugin tests of bundler_files.test.ts leave with the hunks. New tests: Bun.serve serves the page from a bundle for bun and for node, the `input` of the manifest, import attributes and the `loader` option on an in-memory page, a page on disk with an in-memory url asset, and a standalone HTML build.
With two `break` sites in the resolve loop, the second binding `let mut resolve_result = resolve_result;` became a real copy of the 296-byte result for each import record. One mutable binding takes the value from either site directly. The `files` lookup moves into the loop so that no optional result lives across it. Release builds, 2,001 modules and 22,000 import records, no `files`, medians of 31 interleaved runs: 548.03 M instructions on main, 548.87 M with the second binding, 548.21 M with this commit. The quartile ranges of main and of this commit overlap.
A `files` key keeps "/" separators on Windows. The root has "\\" there, so the path code does not make the key relative and `input` is the whole key. The exact value stays pinned on the other platforms.
files imports through the shared steps (HTML import manifest, url assets)
There was a problem hiding this comment.
Beyond the inline findings, two refactor concerns were checked and ruled out: dropping the old branch's explicit side_effects = HasSideEffects is behavior-preserving because bun_ast::SideEffects defaults to HasSideEffects and ParseTask::init copies the FileMap result's default primary_side_effects_data (src/bundler/bundle_v2.rs:1068-1077, 3106); and the last_error early continue at src/bundler/bundle_v2.rs:7107 now skipping in-memory source-index binding for later records is not observable, since a set last_error clears the resolve queue and the build aborts before link (src/bundler/bundle_v2.rs:6580-6606).
Extended reasoning...
The change folds the Bun.build files in-memory resolution branch in src/bundler/bundle_v2.rs into the shared post-resolve path and adds tests in test/bundler/bundler_files.test.ts; it touches no auth, crypto, or injection surface. Five confirmed findings are posted inline, so approval is not on the table; this note only records two additional refactor concerns that were verified as not affecting behavior.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🔴
src/bundler/bundle_v2.rs— Users whoawait import()an in-memory HTML page under a./key in atarget: "bun"build now get a bundle that throwsReferenceError: require__ is not definedat run time, where the base build ran. At src/bundler/bundle_v2.rs:7263 the shared path now rewrites every in-memory server-side HTML record's kind toImportKind::HtmlManifest, including dynamic import records, so the printer loses the dynamic-import shape. Fix: do not rewrite (or print a manifest-returning promise for)DynamicImportandRequirerecords until the printer handlesHtmlManifestfor those kinds, covering disk and in-memory pages alike; the PR notes this depends on #38621 landing first, and that note holds only if it merges in that order.Why this was flagged
Trigger:
Bun.build({ target: "bun", files: { "./page.html": ..., "/app/server.js": "const page = await import('./page.html')" } }). FileMap::resolve at src/bundler/bundle_v2.rs:989 matches the./page.htmlkey directly, so at 6911-6912is_in_memory = trueand the record takes the shared path. At 7248-7250is_html_entrypointis true and 7263 overwritesimport_record.kindfrom DynamicImport toImportKind::HtmlManifest; 7321-7323 then generates the manifest module. On the base branch the old early block kept the record kind and just parsed the file, so the emitted bundle keptimport("./page.html")and loaded the emitted page at run time. After the change the printed dynamic import references an undefined wrapper and the process throwsReferenceError: require__ is not defined. The PR description states this itself under Downsides and Merge order (#38621 first); nothing in this checkout enforces that order, so merged as-is it is a run-time crash for a previously working input.Verification: The PR description's "Downsides" section states exactly this and the "Merge order" note asks for #38621 to land first, but nothing in this checkout enforces that order and no test pins the dynamic-import case. At src/bundler/bundle_v2.rs:7263
import_record.kind = ImportKind::HtmlManifest;is applied regardless of whether the record's kind wasStmtorDynamic. -
🔴
src/bundler/bundle_v2.rs— Users whose in-memory page both links a JSON file and imports it from a script now get the asset URL string in the script instead of the parsed JSON. The url-asset override at src/bundler/bundle_v2.rs:7235-7242 now runs forfileshits and forces./manifest.jsontoLoader::File; the later script import then hits the path-onlypath_to_source_index_mapat src/bundler/bundle_v2.rs:7252 and is bound to that File module. Fix: key the per-target module lookup by path and loader (or create a separate module when the requested loader differs), so a<link>asset and a JS import of the samefileskey each get their own module; on the base branch the in-memory script received the JSON object. [also at: src/bundler/bundle_v2.rs:6912 - Users ofBun.build({ files })whose page links a.jsonfile that its script also imports now get the asset URL string in the script instead of the parsed object.]Why this was flagged
Trigger:
Bun.build({ entrypoints: ["/app/index.html"], files: { "/app/index.html": '<link rel="manifest" href="./manifest.json"><script src="./app.js"></script>', "/app/manifest.json": '{"name":"app"}', "/app/app.js": 'import m from "./manifest.json"; console.log(m.name)' } }). The HTML source is resolved first; its url record for./manifest.jsonnow comes from the FileMap inside the shared loop (src/bundler/bundle_v2.rs:6904-6912) and reaches the override at src/bundler/bundle_v2.rs:7235-7242, which setsimport_record_loader = Loader::Filebecause the importer loader is Html, kind is Url and Json does notshould_copy_for_bundling(). When app.js is resolved, its import hitsself.path_to_source_index_map(target).get(path.text)at src/bundler/bundle_v2.rs:7252 and getssource_indexof the File module, whose default export is the asset URL string (src/bundler/ParseTask.rs:1377-1445). On the base branch the FileMap branch exited before the override, so manifest.json was a Json module andm.namewas "app"; after mergem.nameis undefined with no build error.Verification: In /home/claude/bun/src/bundler/bundle_v2.rs, line 7252
if let Some(id) = self.path_to_source_index_map(target).get(path.text)finds the File module and bindsimport_record.source_index = Index::init(id)(7257); the loader is not part of the key, so the script receives the asset URL string, andm.nameisundefined. The new tests at test/bundler/bundler_files.test.ts:990-1029 cover only the asset-copy side. -
🟡
src/bundler/bundle_v2.rs— Users who bundle a./-keyed in-memory HTML page into a server build and start the bundle from outside the outdir now exit at startup. src/bundler/bundle_v2.rs:7248 now turns the in-memory page into a manifest, and Bun.serve checks each manifest path against the working directory (src/runtime/server/server_body.rs:719), failing withBundled file "./chunk-<hash>.js" not found. On the base the import stayedimport page from "./index.html", which Bun resolved next to the bundle from any directory. The PR lists this as a downside; it follows from the stated purpose. Fix: keep such bundles startable from any directory, e.g. resolve manifest paths against the bundle's own directory instead of the cwd, which also covers pages on disk.Why this was flagged
Trigger:
Bun.build({ target: "bun", outdir: "dist", files: { "./index.html": ..., "./server.js": 'import page from "./index.html"; Bun.serve({ routes: { "/": page } })' } }), thenbun dist/server.jsfrom the project root. On this branchis_html_entrypointat src/bundler/bundle_v2.rs:7248-7250 is true because the loader is Html and the target is server-side, so the record becomesImportKind::HtmlManifest(7263) andgenerate_server_html_module(7322) replaces the import with a manifest whosepathentries are./index-<hash>.html,./chunk-<hash>.js. At run timefrom_optionsprobes eachpathwithsys::exists_at_type(sys::Fd::cwd(), zpath)(src/runtime/server/server_body.rs:719), throwingBundled file ... not found(server_body.rs:729) when the cwd is not the outdir. On the base the old in-memory branch created a plain Html ParseTask, the output keptimport page from "./index.html", and Bun resolved that relative to the bundle file, so the server started from any directory. No safeguard applies: the PR's test runs the bundle withcwd: distonly.Verification: Trigger: a
target: "bun"build with an in-memory.htmlpage under a./key whose bundle is started with a cwd other than the outdir. The record becomesImportKind::HtmlManifest;from_optionscheckssys::exists_at_type(sys::Fd::cwd(), zpath)(src/runtime/server/server_body.rs:719), throwingBundled file "..." not found. On the base the server output keepsimport page from "./index.html".
On Windows the key keeps "/" separators and the root has "\\". The path code finds no common root for the two forms, so `input` is the whole key. The test now expects that value instead of a suffix match.
|
Replies to the review findings:
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked the three behaviors the removed FileMap block used to set explicitly: side_effects is unchanged because SideEffects::HasSideEffects is the Default that ParseTask::init picks up from the FileMap result (src/ast/loader.rs:255); the external branch at src/bundler/bundle_v2.rs:7125 cannot fire because result_for_key leaves flags at default; and the dev-server branch at :7144 is unreachable for in-memory hits because file_map is only set from the Bun.build completion task (src/runtime/api/js_bundle_completion_task.rs:1338), which has no dev server.
Extended reasoning...
The diff folds the in-memory FileMap lookup into the shared resolve loop in src/bundler/bundle_v2.rs so virtual files take the same post-resolution steps as disk files (HTML manifest, URL-asset loader forcing, browser target for manifests), and adds ~330 lines of tests in test/bundler/bundler_files.test.ts. It touches no security-sensitive surface. One confirmed nit (relative-key manifest input) is posted inline; the ruled-out items above are the refactor's behavior-preservation questions that I verified from the code this run.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
src/bundler/bundle_v2.rs— nit: Users who import an in-memory HTML page under a relativefileskey such as./index.htmlinto atarget: "bun"build now get a manifest whoseinputreads../../.././index.html. The in-memory branch at src/bundler/bundle_v2.rs:7322 hands the raw key to generate_server_html_module as the page's source path, and src/bundler/HTMLImportManifest.rs:411 relativizes it against root_dir as if it were absolute. Fix: for an in-memory source emit the key itself asinputunless it is absolute and under root_dir, at both HTMLImportManifest.rs:274 and :332. The PR's Limits note calls this known; it holds, butinputis typed as the original source path.Why this was flagged
Trigger: Bun.build({ entrypoints: ["./server.js"], target: "bun", files: { "./server.js": 'import page from "./index.html"', "./index.html": "..." } }), the Bun.build JS API with a relative
fileskey. src/bundler/bundle_v2.rs:7248-7264 and :7321-7323 now mark the in-memory import as ImportKind::HtmlManifest and create the server module with Source.path.text set to the key./index.html. When the manifest is written, src/bundler/HTMLImportManifest.rs:274-278 calls source_path_relative_to_root(root_dir, "./index.html"); root_dir is the cwd. longest_common_path_generic at src/paths/resolve_path.rs:335-337 returns "/" for a leading-dot path, and relative_to_common_path then prepends one..per cwd segment, giving../../.././index.html. On the base no manifest was produced for afilesimport at all, so there was noinputto be wrong. Bun.serve itself does not readinput(src/runtime/server/server_body.rs:669-695), so the harm is a wrong public field, not a failed route. The author's Limits note names this and points at #46667 for./keys; nothing in this PR guards it.Verification: The PR description's "Limits" section states "A key that is not an absolute path gives a wrong manifest
input, for example../../.././index.html." src/bundler/HTMLImportManifest.rs:271-279 computesinputviasource_path_relative_to_root(root_dir, sources[...].path.text), giving../../.././index.html. Bun.serve is unaffected. On the base branch this case produced no manifest at all.
|
On the relative key nit: the wrong |
… into robobun/40800912/files-relative-keys This branch is now based on #38605 at d3663aa. The two conflicts are in text that both branches have: the hunk of #38605 in resolve_import_records, and its tests. The result keeps the two places where this branch differs (the fourth argument of FileMap::resolve, and the display path of a ./ key).
|
A note from #46667, which is now stacked on this branch (it contains d3663aa). The entry point is the sibling site of this fix. A Repro on this branch at d3663aa, with import { resolve } from "node:path";
const page = resolve("src/page.tsx");
await Bun.build({
entrypoints: [page],
files: { [page]: "console.log(<div>page from memory</div>);" },
jsx: { runtime: "classic", factory: "myFactory", fragment: "MyFragment" },
});It fails with #38609 has tests for the Why it matters for #46667: for this reason it sends an entry point that is the absolute path of a |
|
Merged main (8a0e49e) into this branch.
|
Problem
Bun.build({ files })gets no manifest. The output keepsimport page from "/app/page.html"and exits withCannot find module '/app/page.html', or serves the page on disk.resolve_import_recordshas its own branch forfiles(src/bundler/bundle_v2.rs:6900on main). It copies the shared steps without the manifest rule (:7301) and the url asset rule (:7283).Fix
fileshit now takes the shared steps. The copy is gone.<link rel="manifest">).onResolveroutes are left out on purpose. bundler: resume the shared resolve pass for imports that went to onResolve plugins #45408 owns them.test/bundler/bundler_files.test.ts, 9 new tests fail on main. Self-reviewed: 40 concerns raised, 36 addressed.Background
filesis the in-memory file map ofBun.build.Bun.servereads it.run_resolverandon_resolve: that broke plugins that load.htmlas JS. A patch inside thefilesbranch keeps the copy.Downsides
./key ran from any directory on 1.4.2. It now serves only from the outdir, like a page on disk.await import()of it throwsReferenceError: require__ is not defineduntil bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link #38621.filesimport costs 22 more instructions. Builds withoutfilesshow no measurable change..textgrows 1,024 bytes.Notes
Merge order
await import()of a page then fails at run time withReferenceError: require_<name> is not defined. A page on disk does that on main. An in-memory page does it with this PR. bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link #38621 removes the rewrite. It merges into this branch with no conflict. On a debug build of that merge,await import()andrequire()of an in-memory page return the manifest../and../keys ofBun.buildfilesagainst the cwd inFileMap::put#46667. bundler: resolve./and../keys ofBun.buildfilesagainst the cwd inFileMap::put#46667 makes a./key an absolute path. Without the manifest, server code that imports such a page exits at startup.onResolveroutes. Two tests for an in-memory page behind anonResolveplugin left this PR. A comment on bundler: resume the shared resolve pass for imports that went to onResolve plugins #45408 has them.What changes for an import that
filesanswersBaselines: bun 1.4.2 and a release build of main (179664e). Both give the same result in each case below.
Cannot find module '<dir>/page.html'. A compiled executable exits 1 withCannot find module '<dir>/page.html' from '/$bunfs/root/app'. With this PR the executable answers 200 for the page, its script and its stylesheet.files../key: the bundle keepsimport page from "./index.html"and loads the emitted page at run time. It starts from any directory.Bun.servelooks for the files of a manifest in the working directory. A bundle with an in-memory page started elsewhere now fails withTypeError: Bundled file "./chunk-<hash>.js" not found. A page on disk does the same on main. Bun.serve: find the files of a built HTML import from the server bundle's folder #46019 wanted to lift that rule and is closed.await import("./index.html")of an in-memory page throwsReferenceError: require__ is not defined.require()of it returns{ default: manifest }. On 1.4.2 both ran for a./key.<link rel="manifest" href="./manifest.json">) is copied when the page or the asset is infiles. Before, thehrefstayed as written and no file was written. A standalone HTML build (compile: true,target: "browser") now inlines the asset as adata:URL.What does not change
tsconfig.jsonbeside a file that the key overrides. Two guard tests pin this. bundler: give files imports the same display path as disk files #38620 and bundler: apply the jsx option to entry points that come from files: #38609 are about these two rules.loaderentry decides how an in-memory.htmlfile is loaded. Three guard tests pinwith { type: "text" },with { type: "file" }andloader: { ".html": "text" }.onResolveplugin. Seven plugin setups give the same outputs on main and on this branch. A plugin that declines the import still fails the build withNo matching export in "page.html" for import "default".Limits
input, for example../../.././index.html. It has one../for each level of the working directory. bundler: resolve./and../keys ofBun.buildfilesagainst the cwd inFileMap::put#46667 covers./keys./separators and the root has\. The path code does not make such a key relative, soinputis the whole key there. This is read from the path code, run with the Windows platform parameter on Linux. It was not run on Windows.filesin a server build is bundled for the server target (bundler: bundle HTML entry points for the browser on every entry path of a server build #38594).Why the
onResolveroutes left this PRThe first version made the manifest in
run_resolverand inon_resolvefrom the.htmlextension, before anonLoadplugin ran. The setup below is a page on disk andtarget: "bun". AnonLoadplugin returns{ contents: 'export default "template of the plugin"', loader: "js" }.onResolvedeclines,onLoadloads as JS{ files }with noindex, and a straypage.jsonResolveanswers in its own namespace,onLoadloads as JSonResolveanswers with the file path,onLoadloads as JS#45408 sends a declined or answered import back through
resolve_import_recordsand chooses afteronLoad.Self-review: the four concerns that stay
onResolveexample of the docs declines every import and fails such a build. Rejected: the table above.FileMapbuilds its result. Not taken: it is the behaviour of main and the subject of bundler: apply the jsx option to entry points that come from files: #38609.await import()andrequire()of an in-memory page. Not added: they fail until bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link #38621 merges.Measurements (release builds of main 179664e and of this branch, linux x64, one CPU,
perf_event_openuser instructions)files, 2,001 modules and 22,000 import records on disk, 31 interleaved runs. The median is 548.03 M on main and 548.21 M here. The quartile ranges overlap (547.90 to 548.21 M, 547.95 to 548.41 M). With 42,000 import records the medians are 949.88 M and 950.01 M, and the ranges overlap.files, 21 interleaved runs: median 489.22 M on main and 489.70 M here. That is 22 instructions for each import record thatfilesanswers. No new allocation: read from the code.breaksites. A second binding of the result then costs one more copy of 296 bytes for each import record (548.87 M). The result now has one binding..textis 82,164,617 bytes on main and 82,165,641 here. The sizes of the four changed functions sum to -90 bytes (llvm-nm -S).Verification
test/bundler/bundler_files.test.ts: 42 pass on a debug build and on a release build of this branch. On a release build of main 9 fail and 33 pass. The 5 other new tests are guards.test/bundler/html-import-manifest.test.tsis not in the diff. The merge with main had left a secondreadManifestsin it that shadowed the helper of main.bundler_html,bundler_html_server,bundler_plugin,bundler_plugin_chain,bundler_barrel,bundler_loader,bundler_bun,standalone,metafile,esbuild/loader,css/doesnt_crash,bun-serve-html,bun-serve-html-manifest,bake/dev/html,bake/dev/plugins.bundler_edgecase(188 pass),bun-build-api(89 pass),cli/check/entry-points(473 pass). The local debug build hit test timeouts in these three files. A debug build of an earlier head of this branch hit the same timeouts.cargo clippy -p bun_bundler. macOS and Windows were not run locally.Related
resolve_import_records. Its two tests for thefilesroute are here.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_files.test.ts