From 6f2be77d0a6e7dabbc0341a0c8bfa3a80c5cf866 Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Thu, 9 Mar 2023 12:13:04 +0100 Subject: [PATCH] Merge `EcmascriptChunkUpdate`s before sending them to the client (vercel/turbo#3975) This diff: * introduces the `VersionedContentMerger` trait, which allows for merging the updates of versioned contents within the same chunk group; * implements this for `EcmascriptChunkContent`/`EcmascriptChunkUpdate`, turning them into `EcmascriptMergedChunkContent`/`EcmascriptMergedChunkUpdate`; * creates a new `ChunkList` asset which is capable of merging chunk updates of chunks within the same chunk group, and create such an asset for dynamic chunks (through manifest/loader_item.rs) and chunk group files assets. This fixes a bunch of edge cases related to HMR: * HMR of dynamic imports now works; * Chunks getting added/deleted/renamed now also works with HMR, since we're listening to updates at the chunk group level; * CSS chunks get reloaded in the right order, with respect for precedence. There are still known edge cases with HMR: * CSS chunks added through HMR are not inserted at the right position to respect precedence (WEB-652). * Update aggregation is disabled because we don't want a critical issue to stop all HMR (WEB-582) . This would be fixed by applying aggregated updates when dismissing the error modal, but there are some edge cases with this too (e.g. what happens when an HMR update also causes an error on top of an existing error). --- crates/next-core/js/src/dev/hmr-client.ts | 420 +++-- .../next-core/js/src/entry/app-renderer.tsx | 2 +- crates/next-core/js/src/entry/app/hydrate.tsx | 3 +- crates/next-core/js/src/entry/app/index.d.ts | 1 + .../js/src/entry/app/server-to-client-ssr.tsx | 6 +- .../js/src/entry/app/server-to-client.tsx | 4 +- crates/next-core/js/src/entry/fallback.tsx | 2 +- .../next-core/js/src/entry/next-hydrate.tsx | 2 +- crates/next-core/js/types/globals.d.ts | 4 + crates/next-core/src/app_source.rs | 4 +- .../next-core/src/next_client/transition.rs | 1 + .../src/next_client_chunks/with_chunks.rs | 93 +- .../with_client_chunks.rs | 14 +- crates/next-core/src/next_edge/transition.rs | 1 + crates/next-core/src/page_loader.rs | 27 +- crates/next-core/src/page_source.rs | 5 +- crates/next-core/src/util.rs | 11 +- crates/next-core/src/web_entry_source.rs | 19 +- crates/turbo-tasks/src/trace.rs | 12 +- crates/turbopack-core/src/chunk/dev.rs | 10 + crates/turbopack-core/src/chunk/list/asset.rs | 88 + .../turbopack-core/src/chunk/list/content.rs | 119 ++ crates/turbopack-core/src/chunk/list/mod.rs | 5 + .../src/chunk/list/reference.rs | 74 + .../turbopack-core/src/chunk/list/update.rs | 166 ++ .../turbopack-core/src/chunk/list/version.rs | 64 + crates/turbopack-core/src/chunk/mod.rs | 8 +- crates/turbopack-core/src/version.rs | 20 + crates/turbopack-dev-server/src/html.rs | 28 +- crates/turbopack-dev-server/src/source/mod.rs | 6 - .../src/source/resolve.rs | 2 +- .../src/update/protocol.rs | 7 + .../turbopack-dev-server/src/update/server.rs | 61 +- .../turbopack-dev-server/src/update/stream.rs | 81 +- .../js/src/runtime.dom.js | 61 +- crates/turbopack-ecmascript/js/src/runtime.js | 463 +++++- .../turbopack-ecmascript/js/types/index.d.ts | 5 +- .../js/types/protocol.d.ts | 62 +- .../turbopack-ecmascript/src/chunk/content.rs | 49 +- .../src/chunk/evaluate.rs | 15 + .../src/chunk/manifest/loader_item.rs | 44 +- .../src/chunk/merged/content.rs | 62 + .../src/chunk/merged/merger.rs | 44 + .../src/chunk/merged/mod.rs | 4 + .../src/chunk/merged/update.rs | 255 +++ .../src/chunk/merged/version.rs | 39 + crates/turbopack-ecmascript/src/chunk/mod.rs | 25 +- .../src/chunk/module_factory.rs | 1 + .../src/chunk/optimize.rs | 1 + .../src/chunk/snapshot.rs | 5 - .../turbopack-ecmascript/src/chunk/update.rs | 115 +- .../src/chunk_group_files_asset.rs | 38 +- crates/turbopack-test-utils/src/snapshot.rs | 45 +- .../async_chunk/output/20803_foo_index.js | 2 +- ...snapshot_basic_async_chunk_input_import.js | 2 +- ...t_basic_async_chunk_input_import_93c5e8.js | 2 +- ...ot_basic_async_chunk_input_index_580957.js | 1469 ++++++++++++++++ ...asic_async_chunk_input_index_580957.js.map | 6 + ...ot_basic_async_chunk_input_index_da3760.js | 534 +++++- ...asic_async_chunk_input_index_da3760.js.map | 4 +- .../basic/chunked/output/39e84_foo_index.js | 2 +- ...apshot_basic_chunked_input_index_1d9ecb.js | 1456 ++++++++++++++++ ...ot_basic_chunked_input_index_1d9ecb.js.map | 6 + ...apshot_basic_chunked_input_index_e8535e.js | 531 +++++- ...apshot_basic_shebang_input_index_718090.js | 1456 ++++++++++++++++ ...ot_basic_shebang_input_index_718090.js.map | 6 + ...apshot_basic_shebang_input_index_a8bbcc.js | 531 +++++- .../basic/shebang/output/d1787_foo_index.js | 2 +- ...shot_comptime_define_input_index_68d56d.js | 1477 ++++++++++++++++ ..._comptime_define_input_index_68d56d.js.map | 6 + ...shot_comptime_define_input_index_f522f3.js | 531 +++++- ..._absolute-uri-import_input_index_24fbf6.js | 531 +++++- ..._absolute-uri-import_input_index_73a15e.js | 1453 ++++++++++++++++ ...olute-uri-import_input_index_73a15e.js.map | 6 + .../8697f_foo_style.module.css_354da7._.js | 2 +- ...sts_snapshot_css_css_input_index_47659c.js | 533 +++++- ...sts_snapshot_css_css_input_index_c93332.js | 1471 ++++++++++++++++ ...snapshot_css_css_input_index_c93332.js.map | 8 + .../output/63a02_@emotion_react_index.js | 2 +- .../63a02_@emotion_react_jsx-dev-runtime.js | 2 +- .../output/63a02_@emotion_styled_index.js | 2 +- ...shot_emotion_emotion_input_index_549658.js | 1476 ++++++++++++++++ ..._emotion_emotion_input_index_549658.js.map | 6 + ...shot_emotion_emotion_input_index_d9fc1b.js | 531 +++++- ...s_tests_snapshot_env_env_input_93aa94._.js | 535 +++++- ...s_tests_snapshot_env_env_input_bb8ee7._.js | 1462 ++++++++++++++++ ...sts_snapshot_env_env_input_bb8ee7._.js.map | 6 + ...entrry_runtime_entry_input_index_09dc6c.js | 531 +++++- ...entrry_runtime_entry_input_index_19edf2.js | 1453 ++++++++++++++++ ...ry_runtime_entry_input_index_19edf2.js.map | 6 + ...shot_example_example_input_index_0a08ce.js | 1453 ++++++++++++++++ ..._example_example_input_index_0a08ce.js.map | 6 + ...shot_example_example_input_index_29e0b9.js | 531 +++++- ...ot_export-alls_cjs-2_input_index_d5f22a.js | 1479 +++++++++++++++++ ...xport-alls_cjs-2_input_index_d5f22a.js.map | 12 + ...ot_export-alls_cjs-2_input_index_d9c332.js | 537 +++++- ...port-alls_cjs-script_input_index_b23628.js | 535 +++++- ...port-alls_cjs-script_input_index_ed5b85.js | 1474 ++++++++++++++++ ...-alls_cjs-script_input_index_ed5b85.js.map | 10 + ...shot_import-meta_cjs_input_index_3e3da0.js | 533 +++++- ...shot_import-meta_cjs_input_index_746b39.js | 1464 ++++++++++++++++ ..._import-meta_cjs_input_index_746b39.js.map | 8 + ...rt-meta_esm-multiple_input_index_090618.js | 533 +++++- ...rt-meta_esm-multiple_input_index_8687d1.js | 1471 ++++++++++++++++ ...eta_esm-multiple_input_index_8687d1.js.map | 8 + ...ort-meta_esm-mutable_input_index_3487d2.js | 1464 ++++++++++++++++ ...meta_esm-mutable_input_index_3487d2.js.map | 8 + ...ort-meta_esm-mutable_input_index_dc37f8.js | 533 +++++- ...port-meta_esm-object_input_index_040a52.js | 533 +++++- ...port-meta_esm-object_input_index_73c2df.js | 1464 ++++++++++++++++ ...-meta_esm-object_input_index_73c2df.js.map | 8 + ...shot_import-meta_esm_input_index_c633a8.js | 533 +++++- ...shot_import-meta_esm_input_index_fe8e61.js | 1464 ++++++++++++++++ ..._import-meta_esm_input_index_fe8e61.js.map | 8 + ...shot_import-meta_url_input_index_5f69bf.js | 1470 ++++++++++++++++ ..._import-meta_url_input_index_5f69bf.js.map | 8 + ...shot_import-meta_url_input_index_f01bae.js | 535 +++++- ...shot_imports_dynamic_input_index_56419a.js | 1467 ++++++++++++++++ ..._imports_dynamic_input_index_56419a.js.map | 6 + ...shot_imports_dynamic_input_index_6f9eb3.js | 534 +++++- ..._imports_dynamic_input_index_6f9eb3.js.map | 4 +- ...shot_imports_dynamic_input_vercel.mjs._.js | 2 +- ...ports_dynamic_input_vercel.mjs_93c5e8._.js | 2 +- ...napshot_imports_json_input_index_881b1b.js | 1468 ++++++++++++++++ ...hot_imports_json_input_index_881b1b.js.map | 6 + ...napshot_imports_json_input_index_e460e9.js | 533 +++++- ...ts_resolve_error_cjs_input_index_707ee1.js | 1458 ++++++++++++++++ ...esolve_error_cjs_input_index_707ee1.js.map | 6 + ...ts_resolve_error_cjs_input_index_fb56eb.js | 531 +++++- ...ts_resolve_error_esm_input_index_af6491.js | 1461 ++++++++++++++++ ...esolve_error_esm_input_index_af6491.js.map | 6 + ...ts_resolve_error_esm_input_index_ee6078.js | 531 +++++- ...s_static-and-dynamic_input_index_507785.js | 1479 +++++++++++++++++ ...atic-and-dynamic_input_index_507785.js.map | 8 + ...s_static-and-dynamic_input_index_899ad5.js | 536 +++++- ...atic-and-dynamic_input_index_899ad5.js.map | 8 +- ...s_static-and-dynamic_input_vercel.mjs._.js | 2 +- ...c-and-dynamic_input_vercel.mjs_93c5e8._.js | 2 +- ...pshot_imports_static_input_index_82c953.js | 533 +++++- ...pshot_imports_static_input_index_9fc270.js | 1460 ++++++++++++++++ ...t_imports_static_input_index_9fc270.js.map | 6 + ...de_protocol_external_input_index_4e764f.js | 531 +++++- ...de_protocol_external_input_index_69be78.js | 1455 ++++++++++++++++ ...rotocol_external_input_index_69be78.js.map | 6 + .../output/63a02_styled-components_index.js | 2 +- ...ts_styled_components_input_index_01a621.js | 1462 ++++++++++++++++ ...tyled_components_input_index_01a621.js.map | 6 + ...ts_styled_components_input_index_0496ed.js | 531 +++++- .../output/63a02_react_jsx-dev-runtime.js | 2 +- .../7b7bf_third_party_component_index.js | 2 +- ...nsforms_input_packages_app_index_2a30e9.js | 531 +++++- ...nsforms_input_packages_app_index_38ad57.js | 1458 ++++++++++++++++ ...rms_input_packages_app_index_38ad57.js.map | 6 + ...ansforms_input_packages_component_index.js | 2 +- ...swc_helpers_src__class_call_check.mjs._.js | 2 +- ...ransforms_preset_env_input_index_097653.js | 531 +++++- ...ransforms_preset_env_input_index_62f043.js | 1461 ++++++++++++++++ ...forms_preset_env_input_index_62f043.js.map | 6 + ...ipt_jsconfig-baseurl_input_index_18c34e.js | 1468 ++++++++++++++++ ...jsconfig-baseurl_input_index_18c34e.js.map | 8 + ...ipt_jsconfig-baseurl_input_index_3d21b6.js | 533 +++++- ...sconfig-baseurl_input_index.ts_540b0c._.js | 1468 ++++++++++++++++ ...fig-baseurl_input_index.ts_540b0c._.js.map | 8 + ...sconfig-baseurl_input_index.ts_814f4c._.js | 533 +++++- 164 files changed, 60271 insertions(+), 2614 deletions(-) create mode 100644 crates/turbopack-core/src/chunk/list/asset.rs create mode 100644 crates/turbopack-core/src/chunk/list/content.rs create mode 100644 crates/turbopack-core/src/chunk/list/mod.rs create mode 100644 crates/turbopack-core/src/chunk/list/reference.rs create mode 100644 crates/turbopack-core/src/chunk/list/update.rs create mode 100644 crates/turbopack-core/src/chunk/list/version.rs create mode 100644 crates/turbopack-ecmascript/src/chunk/merged/content.rs create mode 100644 crates/turbopack-ecmascript/src/chunk/merged/merger.rs create mode 100644 crates/turbopack-ecmascript/src/chunk/merged/mod.rs create mode 100644 crates/turbopack-ecmascript/src/chunk/merged/update.rs create mode 100644 crates/turbopack-ecmascript/src/chunk/merged/version.rs create mode 100644 crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js create mode 100644 crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js create mode 100644 crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js create mode 100644 crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js create mode 100644 crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js create mode 100644 crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js create mode 100644 crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js create mode 100644 crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js create mode 100644 crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js create mode 100644 crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js create mode 100644 crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js create mode 100644 crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js create mode 100644 crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js create mode 100644 crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js create mode 100644 crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js create mode 100644 crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js create mode 100644 crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js create mode 100644 crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js create mode 100644 crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js create mode 100644 crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map create mode 100644 crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js create mode 100644 crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map diff --git a/crates/next-core/js/src/dev/hmr-client.ts b/crates/next-core/js/src/dev/hmr-client.ts index 42853880ce32c..4654a1af2acf7 100644 --- a/crates/next-core/js/src/dev/hmr-client.ts +++ b/crates/next-core/js/src/dev/hmr-client.ts @@ -1,6 +1,10 @@ import type { + ChunkListUpdate, + ChunkUpdate, ClientMessage, - HmrUpdateEntry, + EcmascriptMergedChunkUpdate, + EcmascriptMergedUpdate, + EcmascriptModuleEntry, Issue, ResourceIdentifier, ServerMessage, @@ -56,8 +60,6 @@ export function connect({ assetPrefix }: ClientOptions) { subscribeToChunkUpdate(chunkPath, callback); } } - - subscribeToInitialCssChunksUpdates(assetPrefix); } type UpdateCallbackSet = { @@ -100,116 +102,334 @@ function handleSocketConnected() { } } -type AggregatedUpdates = { - added: Record; - modified: Record; - deleted: Set; -}; - -// we aggregate all updates until the issues are resolved -const chunksWithUpdates: Map = new Map(); +// we aggregate all pending updates until the issues are resolved +const chunkListsWithPendingUpdates: Map< + ResourceKey, + { update: ChunkListUpdate; resource: ResourceIdentifier } +> = new Map(); function aggregateUpdates( msg: ServerMessage, - hasCriticalIssues: boolean + aggregate: boolean ): ServerMessage { const key = resourceKey(msg.resource); - const aggregated = chunksWithUpdates.get(key); - - if (msg.type === "issues" && aggregated == null && hasCriticalIssues) { - // add an empty record to make sure we don't call `onBuildOk` - chunksWithUpdates.set(key, { - added: {}, - modified: {}, - deleted: new Set(), - }); - } + let aggregated = chunkListsWithPendingUpdates.get(key); if (msg.type === "issues" && aggregated != null) { - if (!hasCriticalIssues) { - chunksWithUpdates.delete(key); + if (!aggregate) { + chunkListsWithPendingUpdates.delete(key); } return { ...msg, type: "partial", - instruction: { - type: "EcmascriptChunkUpdate", - added: aggregated.added, - modified: aggregated.modified, - deleted: Array.from(aggregated.deleted), - }, + instruction: aggregated.update, }; } if (msg.type !== "partial") return msg; if (aggregated == null) { - if (hasCriticalIssues) { - chunksWithUpdates.set(key, { - added: msg.instruction.added, - modified: msg.instruction.modified, - deleted: new Set(msg.instruction.deleted), + if (aggregate) { + chunkListsWithPendingUpdates.set(key, { + resource: msg.resource, + update: msg.instruction, }); } return msg; } - for (const [moduleId, entry] of Object.entries(msg.instruction.added)) { - const removedDeleted = aggregated.deleted.delete(moduleId); - if (aggregated.modified[moduleId] != null) { - console.error( - `impossible state aggregating updates: module "${moduleId}" was added, but previously modified` - ); - location.reload(); + aggregated = { + resource: msg.resource, + update: mergeChunkListUpdates(aggregated.update, msg.instruction), + }; + + if (aggregate) { + chunkListsWithPendingUpdates.set(key, aggregated); + } else { + // Once we receive a partial update with no critical issues, we can stop aggregating updates. + // The aggregated update will be applied. + chunkListsWithPendingUpdates.delete(key); + } + + return { + ...msg, + instruction: aggregated.update, + }; +} + +function mergeChunkListUpdates( + updateA: ChunkListUpdate, + updateB: ChunkListUpdate +): ChunkListUpdate { + let chunks; + if (updateA.chunks != null) { + if (updateB.chunks == null) { + chunks = updateA.chunks; + } else { + chunks = mergeChunkListChunks(updateA.chunks, updateB.chunks); } + } else if (updateB.chunks != null) { + chunks = updateB.chunks; + } - if (removedDeleted) { - aggregated.modified[moduleId] = entry; + let merged; + if (updateA.merged != null) { + if (updateB.merged == null) { + merged = updateA.merged; } else { - aggregated.added[moduleId] = entry; + // Since `merged` is an array of updates, we need to merge them all into + // one, consistent update. + // Since there can only be `EcmascriptMergeUpdates` in the array, there is + // no need to key on the `type` field. + let update = updateA.merged[0]; + for (let i = 1; i < updateA.merged.length; i++) { + update = mergeChunkListEcmascriptMergedUpdates( + update, + updateA.merged[i] + ); + } + + for (let i = 0; i < updateB.merged.length; i++) { + update = mergeChunkListEcmascriptMergedUpdates( + update, + updateB.merged[i] + ); + } + + merged = [update]; } + } else if (updateB.merged != null) { + merged = updateB.merged; } - for (const [moduleId, entry] of Object.entries(msg.instruction.modified)) { - if (aggregated.added[moduleId] != null) { - aggregated.added[moduleId] = entry; + return { + type: "ChunkListUpdate", + chunks, + merged, + }; +} + +function mergeChunkListChunks( + chunksA: Record, + chunksB: Record +): Record { + const chunks: Record = {}; + + for (const [chunkPath, chunkUpdateA] of Object.entries(chunksA)) { + const chunkUpdateB = chunksB[chunkPath]; + if (chunkUpdateB != null) { + const mergedUpdate = mergeChunkUpdates(chunkUpdateA, chunkUpdateB); + if (mergedUpdate != null) { + chunks[chunkPath] = mergedUpdate; + } } else { - aggregated.modified[moduleId] = entry; + chunks[chunkPath] = chunkUpdateA; } + } - if (aggregated.deleted.has(moduleId)) { - console.error( - `impossible state aggregating updates: module "${moduleId}" was modified, but previously deleted` - ); - location.reload(); + for (const [chunkPath, chunkUpdateB] of Object.entries(chunksB)) { + if (chunks[chunkPath] == null) { + chunks[chunkPath] = chunkUpdateB; } } - for (const moduleId of msg.instruction.deleted) { - delete aggregated.added[moduleId]; - delete aggregated.modified[moduleId]; - aggregated.deleted.add(moduleId); + return chunks; +} + +function mergeChunkUpdates( + updateA: ChunkUpdate, + updateB: ChunkUpdate +): ChunkUpdate | undefined { + if ( + (updateA.type === "added" && updateB.type === "deleted") || + (updateA.type === "deleted" && updateB.type === "added") + ) { + return undefined; } - if (!hasCriticalIssues) { - chunksWithUpdates.delete(key); - } else { - chunksWithUpdates.set(key, aggregated); + if (updateA.type === "partial") { + invariant(updateA.instruction, "Partial updates are unsupported"); + } + + if (updateB.type === "partial") { + invariant(updateB.instruction, "Partial updates are unsupported"); } + return undefined; +} + +function mergeChunkListEcmascriptMergedUpdates( + mergedA: EcmascriptMergedUpdate, + mergedB: EcmascriptMergedUpdate +): EcmascriptMergedUpdate { + const entries = mergeEcmascriptChunkEntries(mergedA.entries, mergedB.entries); + const chunks = mergeEcmascriptChunksUpdates(mergedA.chunks, mergedB.chunks); + return { - ...msg, - instruction: { - type: "EcmascriptChunkUpdate", - added: aggregated.added, - modified: aggregated.modified, - deleted: Array.from(aggregated.deleted), - }, + type: "EcmascriptMergedUpdate", + entries, + chunks, }; } +function mergeEcmascriptChunkEntries( + entriesA: Record | undefined, + entriesB: Record | undefined +): Record { + return { ...entriesA, ...entriesB }; +} + +function mergeEcmascriptChunksUpdates( + chunksA: Record | undefined, + chunksB: Record | undefined +): Record | undefined { + if (chunksA == null) { + return chunksB; + } + + if (chunksB == null) { + return chunksA; + } + + const chunks: Record = {}; + + for (const [chunkPath, chunkUpdateA] of Object.entries(chunksA)) { + const chunkUpdateB = chunksB[chunkPath]; + if (chunkUpdateB != null) { + const mergedUpdate = mergeEcmascriptChunkUpdates( + chunkUpdateA, + chunkUpdateB + ); + if (mergedUpdate != null) { + chunks[chunkPath] = mergedUpdate; + } + } else { + chunks[chunkPath] = chunkUpdateA; + } + } + + for (const [chunkPath, chunkUpdateB] of Object.entries(chunksB)) { + if (chunks[chunkPath] == null) { + chunks[chunkPath] = chunkUpdateB; + } + } + + if (Object.keys(chunks).length === 0) { + return undefined; + } + + return chunks; +} + +function mergeEcmascriptChunkUpdates( + updateA: EcmascriptMergedChunkUpdate, + updateB: EcmascriptMergedChunkUpdate +): EcmascriptMergedChunkUpdate | undefined { + if (updateA.type === "added" && updateB.type === "deleted") { + // These two completely cancel each other out. + return undefined; + } + + if (updateA.type === "deleted" && updateB.type === "added") { + const added = []; + const deleted = []; + const deletedModules = new Set(updateA.modules ?? []); + const addedModules = new Set(updateB.modules ?? []); + + for (const moduleId of addedModules) { + if (!deletedModules.has(moduleId)) { + added.push(moduleId); + } + } + + for (const moduleId of deletedModules) { + if (!addedModules.has(moduleId)) { + deleted.push(moduleId); + } + } + + if (added.length === 0 && deleted.length === 0) { + return undefined; + } + + return { + type: "partial", + added, + deleted, + }; + } + + if (updateA.type === "partial" && updateB.type === "partial") { + const added = new Set([...(updateA.added ?? []), ...(updateB.added ?? [])]); + const deleted = new Set([ + ...(updateA.deleted ?? []), + ...(updateB.deleted ?? []), + ]); + + if (updateB.added != null) { + for (const moduleId of updateB.added) { + deleted.delete(moduleId); + } + } + + if (updateB.deleted != null) { + for (const moduleId of updateB.deleted) { + added.delete(moduleId); + } + } + + return { + type: "partial", + added: [...added], + deleted: [...deleted], + }; + } + + if (updateA.type === "added" && updateB.type === "partial") { + const modules = new Set([ + ...(updateA.modules ?? []), + ...(updateB.added ?? []), + ]); + + for (const moduleId of updateB.deleted ?? []) { + modules.delete(moduleId); + } + + return { + type: "added", + modules: [...modules], + }; + } + + if (updateA.type === "partial" && updateB.type === "deleted") { + // We could eagerly return `updateB` here, but this would potentially be + // incorrect if `updateA` has added modules. + + const modules = new Set(updateB.modules ?? []); + + if (updateA.added != null) { + for (const moduleId of updateA.added) { + modules.delete(moduleId); + } + } + + return { + type: "deleted", + modules: [...modules], + }; + } + + // Any other update combination is invalid. + + return undefined; +} + +function invariant(never: never, message: string): never { + throw new Error(`Invariant: ${message}`); +} + const CRITICAL = ["bug", "error", "fatal"]; function compareByList(list: any[], a: any, b: any) { @@ -282,9 +502,14 @@ function handleSocketMessage(msg: ServerMessage) { sortIssues(msg.issues); const hasCriticalIssues = handleIssues(msg); - const aggregatedMsg = aggregateUpdates(msg, hasCriticalIssues); - const runHooks = chunksWithUpdates.size === 0; + // TODO(WEB-582) Disable update aggregation for now. + const aggregate = /* hasCriticalIssues */ false; + const aggregatedMsg = aggregateUpdates(msg, aggregate); + + if (aggregate) return; + + const runHooks = chunkListsWithPendingUpdates.size === 0; if (aggregatedMsg.type !== "issues") { if (runHooks) onBeforeRefresh(); @@ -354,6 +579,16 @@ function triggerUpdate(msg: ServerMessage) { for (const callback of callbackSet.callbacks) { callback(msg); } + + if (msg.type === "notFound") { + // This indicates that the resource which we subscribed to either does not exist or + // has been deleted. In either case, we should clear all update callbacks, so if a + // new subscription is created for the same resource, it will send a new "subscribe" + // message to the server. + // No need to send an "unsubscribe" message to the server, it will have already + // dropped the update stream before sending the "notFound" message. + updateCallbackSets.delete(key); + } } catch (err) { console.error( `An error occurred during the update of resource \`${msg.resource.path}\``, @@ -362,46 +597,3 @@ function triggerUpdate(msg: ServerMessage) { location.reload(); } } - -// Unlike ES chunks, CSS chunks cannot contain the logic to accept updates. -// They must be reloaded here instead. -function subscribeToInitialCssChunksUpdates(assetPrefix: string) { - const initialCssChunkLinks: NodeListOf = - document.head.querySelectorAll(`link[rel="stylesheet"]`); - - initialCssChunkLinks.forEach((link) => { - subscribeToCssChunkUpdates(assetPrefix, link); - }); -} - -export function subscribeToCssChunkUpdates( - assetPrefix: string, - link: HTMLLinkElement -) { - const cssChunkPrefix = `${assetPrefix}/`; - - const href = link.href; - if (href == null) { - return; - } - - const { pathname, origin } = new URL(href); - if (origin !== location.origin || !pathname.startsWith(cssChunkPrefix)) { - return; - } - - const chunkPath = pathname.slice(cssChunkPrefix.length); - subscribeToChunkUpdate(chunkPath, (update) => { - switch (update.type) { - case "restart": { - console.info(`Reloading CSS chunk \`${chunkPath}\``); - link.replaceWith(link); - break; - } - case "partial": - throw new Error(`partial CSS chunk updates are not supported`); - default: - throw new Error(`unknown update type \`${update}\``); - } - }); -} diff --git a/crates/next-core/js/src/entry/app-renderer.tsx b/crates/next-core/js/src/entry/app-renderer.tsx index ec135ce31efd0..ebb67f12f6cc1 100644 --- a/crates/next-core/js/src/entry/app-renderer.tsx +++ b/crates/next-core/js/src/entry/app-renderer.tsx @@ -37,7 +37,7 @@ import { headersFromEntries } from "@vercel/turbopack-next/internal/headers"; import { parse, ParsedUrlQuery } from "node:querystring"; globalThis.__next_require__ = (data) => { - const [, , ssr_id] = JSON.parse(data); + const [, , , ssr_id] = JSON.parse(data); return __turbopack_require__(ssr_id); }; globalThis.__next_chunk_load__ = () => Promise.resolve(); diff --git a/crates/next-core/js/src/entry/app/hydrate.tsx b/crates/next-core/js/src/entry/app/hydrate.tsx index 7e9eacb1dba10..2be819e8ff676 100644 --- a/crates/next-core/js/src/entry/app/hydrate.tsx +++ b/crates/next-core/js/src/entry/app/hydrate.tsx @@ -19,7 +19,8 @@ window.next = { }; globalThis.__next_require__ = (data) => { - const [client_id] = JSON.parse(data); + const [client_id, chunks, chunkListPath] = JSON.parse(data); + __turbopack_register_chunk_list__(chunkListPath, chunks); return __turbopack_require__(client_id); }; globalThis.__next_chunk_load__ = __turbopack_load__; diff --git a/crates/next-core/js/src/entry/app/index.d.ts b/crates/next-core/js/src/entry/app/index.d.ts index df1093e038400..69e3db64322ae 100644 --- a/crates/next-core/js/src/entry/app/index.d.ts +++ b/crates/next-core/js/src/entry/app/index.d.ts @@ -5,3 +5,4 @@ export = Anything; export const __turbopack_module_id__: string | number; export const chunks: string[]; +export const chunkListPath: string; diff --git a/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx b/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx index 8b7dfa3a36c2d..41e0ab50cf1c1 100644 --- a/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx +++ b/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx @@ -5,6 +5,8 @@ import { createProxy } from "next/dist/build/webpack/loaders/next-flight-loader/ import { __turbopack_module_id__ as id } from "CLIENT_MODULE"; // @ts-expect-error CLIENT_CHUNKS is provided by rust -import client_id, { chunks } from "CLIENT_CHUNKS"; +import client_id, { chunks, chunkListPath } from "CLIENT_CHUNKS"; -export default createProxy(JSON.stringify([client_id, chunks, id])); +export default createProxy( + JSON.stringify([client_id, chunks, chunkListPath, id]) +); diff --git a/crates/next-core/js/src/entry/app/server-to-client.tsx b/crates/next-core/js/src/entry/app/server-to-client.tsx index 30c569c4df514..142d9e32732df 100644 --- a/crates/next-core/js/src/entry/app/server-to-client.tsx +++ b/crates/next-core/js/src/entry/app/server-to-client.tsx @@ -1,6 +1,6 @@ import { createProxy } from "next/dist/build/webpack/loaders/next-flight-loader/module-proxy"; // @ts-expect-error CLIENT_CHUNKS is provided by rust -import client_id, { chunks } from "CLIENT_CHUNKS"; +import client_id, { chunks, chunkListPath } from "CLIENT_CHUNKS"; -export default createProxy(JSON.stringify([client_id, chunks])); +export default createProxy(JSON.stringify([client_id, chunks, chunkListPath])); diff --git a/crates/next-core/js/src/entry/fallback.tsx b/crates/next-core/js/src/entry/fallback.tsx index 0ce5f210ba903..5aefc76bd0ca4 100644 --- a/crates/next-core/js/src/entry/fallback.tsx +++ b/crates/next-core/js/src/entry/fallback.tsx @@ -18,7 +18,7 @@ subscribeToUpdate( }, }, (update) => { - if (update.type === "restart") { + if (update.type === "restart" || update.type === "notFound") { location.reload(); } } diff --git a/crates/next-core/js/src/entry/next-hydrate.tsx b/crates/next-core/js/src/entry/next-hydrate.tsx index b7fa676454533..3fd0b24361cfd 100644 --- a/crates/next-core/js/src/entry/next-hydrate.tsx +++ b/crates/next-core/js/src/entry/next-hydrate.tsx @@ -97,7 +97,7 @@ function subscribeToPageManifest({ assetPrefix }: { assetPrefix: string }) { path: "_next/static/development/_devPagesManifest.json", }, (update) => { - if (["restart", "partial"].includes(update.type)) { + if (["restart", "notFound", "partial"].includes(update.type)) { return; } diff --git a/crates/next-core/js/types/globals.d.ts b/crates/next-core/js/types/globals.d.ts index a89ab2815f8a9..d134a3e9714c7 100644 --- a/crates/next-core/js/types/globals.d.ts +++ b/crates/next-core/js/types/globals.d.ts @@ -1,6 +1,10 @@ declare global { function __turbopack_require__(name: any): any; function __turbopack_load__(path: string): any; + function __turbopack_register_chunk_list__( + chunkListPath: string, + chunksPaths: string[] + ): any; function __webpack_require__(name: any): any; var __webpack_public_path__: string | undefined; var __DEV_MIDDLEWARE_MATCHERS: any[]; diff --git a/crates/next-core/src/app_source.rs b/crates/next-core/src/app_source.rs index 409292cba2b58..33cae92b70365 100644 --- a/crates/next-core/src/app_source.rs +++ b/crates/next-core/src/app_source.rs @@ -411,7 +411,7 @@ async fn create_app_source_for_directory( segments: layouts, } => { let LayoutSegment { target, .. } = *segment.await?; - let pathname = pathname_for_path(server_root, url, false); + let pathname = pathname_for_path(server_root, url, false, false); let params_matcher = NextParamsMatcherVc::new(pathname); sources.push(create_node_rendered_source( @@ -443,7 +443,7 @@ async fn create_app_source_for_directory( route, .. } => { - let pathname = pathname_for_path(server_root, url, false); + let pathname = pathname_for_path(server_root, url, false, false); let params_matcher = NextParamsMatcherVc::new(pathname); sources.push(create_node_api_source( diff --git a/crates/next-core/src/next_client/transition.rs b/crates/next-core/src/next_client/transition.rs index 6d8d8441a1f7b..bf63b4139eae0 100644 --- a/crates/next-core/src/next_client/transition.rs +++ b/crates/next-core/src/next_client/transition.rs @@ -95,6 +95,7 @@ impl Transition for NextClientTransition { asset: asset.into(), chunking_context: self.client_chunking_context, base_path: self.server_root.join("_next"), + server_root: self.server_root, runtime_entries: Some(runtime_entries), }; diff --git a/crates/next-core/src/next_client_chunks/with_chunks.rs b/crates/next-core/src/next_client_chunks/with_chunks.rs index 3f2d26b2a8f00..9fb7d9f049b2b 100644 --- a/crates/next-core/src/next_client_chunks/with_chunks.rs +++ b/crates/next-core/src/next_client_chunks/with_chunks.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use anyhow::{bail, Result}; +use indoc::formatdoc; use serde_json::Value; use turbo_tasks::{primitives::StringVc, TryJoinIterExt, ValueToString, ValueToStringVc}; use turbo_tasks_fs::FileSystemPathVc; @@ -13,14 +14,15 @@ use turbopack::ecmascript::{ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ - Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkVc, ChunkableAsset, - ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkableAssetVc, ChunkingContextVc, - ChunkingType, ChunkingTypeOptionVc, + Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkListReferenceVc, ChunkVc, ChunkableAsset, + ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkableAssetVc, ChunkingContext, + ChunkingContextVc, ChunkingType, ChunkingTypeOptionVc, }, ident::AssetIdentVc, reference::{AssetReference, AssetReferenceVc, AssetReferencesVc}, resolve::{ResolveResult, ResolveResultVc}, }; +use turbopack_ecmascript::utils::stringify_js_pretty; use super::in_chunking_context_asset::InChunkingContextAsset; @@ -93,6 +95,25 @@ struct WithChunksChunkItem { inner: WithChunksAssetVc, } +#[turbo_tasks::value_impl] +impl WithChunksChunkItemVc { + #[turbo_tasks::function] + async fn chunk_list_path(self) -> Result { + let this = self.await?; + Ok(this.inner_context.chunk_list_path(this.inner.ident())) + } + + #[turbo_tasks::function] + async fn chunk_group(self) -> Result { + let this = self.await?; + let inner = this.inner.await?; + Ok(ChunkGroupVc::from_asset( + inner.asset.into(), + this.inner_context, + )) + } +} + #[turbo_tasks::value_impl] impl EcmascriptChunkItem for WithChunksChunkItem { #[turbo_tasks::function] @@ -101,29 +122,40 @@ impl EcmascriptChunkItem for WithChunksChunkItem { } #[turbo_tasks::function] - async fn content(&self) -> Result { - let inner = self.inner.await?; - let group = ChunkGroupVc::from_asset(inner.asset.into(), self.inner_context); + async fn content(self_vc: WithChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; + let group = self_vc.chunk_group(); let chunks = group.chunks().await?; let server_root = inner.server_root.await?; let mut client_chunks = Vec::new(); + + let chunk_list_path = self_vc.chunk_list_path().await?; + let chunk_list_path = if let Some(path) = server_root.get_path_to(&chunk_list_path) { + path + } else { + bail!("could not get path to chunk list"); + }; + for chunk_path in chunks.iter().map(|c| c.path()).try_join().await? { if let Some(path) = server_root.get_path_to(&chunk_path) { client_chunks.push(Value::String(path.to_string())); } } - let module_id = stringify_js(&*inner.asset.as_chunk_item(self.inner_context).id().await?); + let module_id = stringify_js(&*inner.asset.as_chunk_item(this.inner_context).id().await?); Ok(EcmascriptChunkItemContent { - inner_code: format!( - "__turbopack_esm__({{ - default: () => {}, - chunks: () => chunks -}}); -const chunks = {}; -", + inner_code: formatdoc! { + r#" + __turbopack_esm__({{ + default: () => {}, + chunks: () => {}, + chunkListPath: () => {}, + }}); + "#, module_id, - Value::Array(client_chunks) - ) + stringify_js_pretty(&client_chunks), + stringify_js(&chunk_list_path), + } .into(), ..Default::default() } @@ -139,18 +171,27 @@ impl ChunkItem for WithChunksChunkItem { } #[turbo_tasks::function] - async fn references(&self) -> Result { - let inner = self.inner.await?; - Ok(AssetReferencesVc::cell(vec![WithChunksAssetReference { - asset: InChunkingContextAsset { - asset: inner.asset, - chunking_context: self.inner_context, + async fn references(self_vc: WithChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; + Ok(AssetReferencesVc::cell(vec![ + WithChunksAssetReference { + asset: InChunkingContextAsset { + asset: inner.asset, + chunking_context: this.inner_context, + } + .cell() + .into(), } .cell() .into(), - } - .cell() - .into()])) + ChunkListReferenceVc::new( + inner.server_root, + self_vc.chunk_group(), + self_vc.chunk_list_path(), + ) + .into(), + ])) } } diff --git a/crates/next-core/src/next_client_component/with_client_chunks.rs b/crates/next-core/src/next_client_component/with_client_chunks.rs index 82f90787d544b..037059c9d609a 100644 --- a/crates/next-core/src/next_client_component/with_client_chunks.rs +++ b/crates/next-core/src/next_client_component/with_client_chunks.rs @@ -21,6 +21,7 @@ use turbopack_core::{ reference::{AssetReference, AssetReferenceVc, AssetReferencesVc}, resolve::{ResolveResult, ResolveResultVc}, }; +use turbopack_ecmascript::utils::stringify_js_pretty; use crate::next_client_chunks::in_chunking_context_asset::InChunkingContextAsset; @@ -129,15 +130,17 @@ impl EcmascriptChunkItem for WithClientChunksChunkItem { let module_id = stringify_js(&*inner.asset.as_chunk_item(self.context).id().await?); Ok(EcmascriptChunkItemContent { inner_code: formatdoc!( + // We store the chunks in a binding, otherwise a new array would be created every + // time the export binding is read. r#" __turbopack_esm__({{ default: () => __turbopack_import__({}), - chunks: () => chunks + chunks: () => chunks, }}); const chunks = {}; "#, module_id, - stringify_js(&client_chunks) + stringify_js_pretty(&client_chunks), ) .into(), ..Default::default() @@ -154,13 +157,14 @@ impl ChunkItem for WithClientChunksChunkItem { } #[turbo_tasks::function] - async fn references(&self) -> Result { - let inner = self.inner.await?; + async fn references(self_vc: WithClientChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; Ok(AssetReferencesVc::cell(vec![ WithClientChunksAssetReference { asset: InChunkingContextAsset { asset: inner.asset, - chunking_context: self.context, + chunking_context: this.context, } .cell() .into(), diff --git a/crates/next-core/src/next_edge/transition.rs b/crates/next-core/src/next_edge/transition.rs index d816dc4611862..f42fe982245bb 100644 --- a/crates/next-core/src/next_edge/transition.rs +++ b/crates/next-core/src/next_edge/transition.rs @@ -112,6 +112,7 @@ impl Transition for NextEdgeTransition { asset: new_asset.into(), chunking_context: self.edge_chunking_context, base_path: self.output_path, + server_root: self.output_path, runtime_entries: None, }; diff --git a/crates/next-core/src/page_loader.rs b/crates/next-core/src/page_loader.rs index 8c766c71f01e2..9e61a8987d13a 100644 --- a/crates/next-core/src/page_loader.rs +++ b/crates/next-core/src/page_loader.rs @@ -2,7 +2,7 @@ use std::io::Write; use anyhow::{bail, Result}; use indexmap::indexmap; -use turbo_tasks::{primitives::StringVc, Value}; +use turbo_tasks::{primitives::StringVc, TryJoinIterExt, Value}; use turbo_tasks_fs::{rope::RopeBuilder, File, FileContent, FileSystemPathVc}; use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, @@ -116,17 +116,28 @@ impl Asset for PageLoaderAsset { let this = &*self_vc.await?; let chunks = self_vc.get_page_chunks().await?; - - let mut data = Vec::with_capacity(chunks.len()); - for chunk in chunks.iter() { - let path = chunk.path().await?; - data.push(serde_json::Value::String(path.path.clone())); - } + let server_root = this.server_root.await?; + + let chunk_paths: Vec<_> = chunks + .iter() + .map(|chunk| { + let server_root = server_root.clone(); + async move { + Ok(server_root + .get_path_to(&*chunk.path().await?) + .map(|path| path.to_string())) + } + }) + .try_join() + .await? + .into_iter() + .flatten() + .collect(); let content = format!( "__turbopack_load_page_chunks__({}, {})\n", stringify_js(&this.pathname.await?), - serde_json::Value::Array(data) + stringify_js(&chunk_paths) ); Ok(AssetContentVc::from(File::from(content))) diff --git a/crates/next-core/src/page_source.rs b/crates/next-core/src/page_source.rs index 4a28dedf1a5da..b8a069b30e3db 100644 --- a/crates/next-core/src/page_source.rs +++ b/crates/next-core/src/page_source.rs @@ -346,7 +346,7 @@ async fn create_page_source_for_file( Value::new(ClientContextType::Pages { pages_dir }), ); - let pathname = pathname_for_path(server_root, server_path, true); + let pathname = pathname_for_path(server_root, server_path, true, false); let route_matcher = NextParamsMatcherVc::new(pathname); Ok(if is_api_path { @@ -370,8 +370,9 @@ async fn create_page_source_for_file( runtime_entries, ) } else { + let data_pathname = pathname_for_path(server_root, server_path, true, true); let data_route_matcher = - NextPrefixSuffixParamsMatcherVc::new(pathname, "_next/data/development/", ".json"); + NextPrefixSuffixParamsMatcherVc::new(data_pathname, "_next/data/development/", ".json"); let ssr_entry = SsrEntry { context: server_context, diff --git a/crates/next-core/src/util.rs b/crates/next-core/src/util.rs index 0b56f35914bb1..15f6a30f46898 100644 --- a/crates/next-core/src/util.rs +++ b/crates/next-core/src/util.rs @@ -28,6 +28,7 @@ pub async fn pathname_for_path( server_root: FileSystemPathVc, server_path: FileSystemPathVc, has_extension: bool, + data: bool, ) -> Result { let server_path_value = &*server_path.await?; let path = if let Some(path) = server_root.await?.get_path_to(server_path_value) { @@ -46,10 +47,14 @@ pub async fn pathname_for_path( } else { path }; - let path = if path == "index" { - "" + let path = if data { + path } else { - path.strip_suffix("/index").unwrap_or(path) + if path == "index" { + "" + } else { + path.strip_suffix("/index").unwrap_or(path) + } }; Ok(StringVc::cell(path.to_string())) diff --git a/crates/next-core/src/web_entry_source.rs b/crates/next-core/src/web_entry_source.rs index 976fd79a73002..51fc056303446 100644 --- a/crates/next-core/src/web_entry_source.rs +++ b/crates/next-core/src/web_entry_source.rs @@ -66,18 +66,23 @@ pub async fn create_web_entry_source( }) .try_join() .await?; - let chunks: Vec<_> = entries + + let chunk_groups: Vec<_> = entries .into_iter() .flatten() .enumerate() .map(|(i, module)| async move { if let Some(ecmascript) = EcmascriptModuleAssetVc::resolve_from(module).await? { - Ok(ecmascript - .as_evaluated_chunk(chunking_context, (i == 0).then_some(runtime_entries))) + let chunk = ecmascript + .as_evaluated_chunk(chunking_context, (i == 0).then_some(runtime_entries)); + let chunk_group = ChunkGroupVc::from_chunk(chunk); + Ok(chunk_group) } else if let Some(chunkable) = ChunkableAssetVc::resolve_from(module).await? { // TODO this is missing runtime code, so it's probably broken and we should also // add an ecmascript chunk with the runtime code - Ok(chunkable.as_chunk(chunking_context)) + Ok(ChunkGroupVc::from_chunk( + chunkable.as_chunk(chunking_context), + )) } else { // TODO convert into a serve-able asset Err(anyhow!( @@ -89,11 +94,7 @@ pub async fn create_web_entry_source( .try_join() .await?; - let entry_asset = DevHtmlAssetVc::new( - server_root.join("index.html"), - chunks.into_iter().map(ChunkGroupVc::from_chunk).collect(), - ) - .into(); + let entry_asset = DevHtmlAssetVc::new(server_root.join("index.html"), chunk_groups).into(); let graph = if eager_compile { AssetGraphContentSourceVc::new_eager(server_root, entry_asset) diff --git a/crates/turbo-tasks/src/trace.rs b/crates/turbo-tasks/src/trace.rs index 518031e7604fc..1d47432375f3b 100644 --- a/crates/turbo-tasks/src/trace.rs +++ b/crates/turbo-tasks/src/trace.rs @@ -115,7 +115,7 @@ impl TraceRawVcs for Vec { } } -impl TraceRawVcs for HashSet { +impl TraceRawVcs for HashSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -123,7 +123,7 @@ impl TraceRawVcs for HashSet { } } -impl TraceRawVcs for AutoSet { +impl TraceRawVcs for AutoSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -139,7 +139,7 @@ impl TraceRawVcs for BTreeSet { } } -impl TraceRawVcs for IndexSet { +impl TraceRawVcs for IndexSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -147,7 +147,7 @@ impl TraceRawVcs for IndexSet { } } -impl TraceRawVcs for HashMap { +impl TraceRawVcs for HashMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); @@ -156,7 +156,7 @@ impl TraceRawVcs for HashMap { } } -impl TraceRawVcs for AutoMap { +impl TraceRawVcs for AutoMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); @@ -174,7 +174,7 @@ impl TraceRawVcs for BTreeMap { } } -impl TraceRawVcs for IndexMap { +impl TraceRawVcs for IndexMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); diff --git a/crates/turbopack-core/src/chunk/dev.rs b/crates/turbopack-core/src/chunk/dev.rs index ec33aaa7b64b9..0e336823e1297 100644 --- a/crates/turbopack-core/src/chunk/dev.rs +++ b/crates/turbopack-core/src/chunk/dev.rs @@ -222,6 +222,11 @@ impl ChunkingContext for DevChunkingContext { Ok(root_path.join(&name)) } + #[turbo_tasks::function] + fn chunk_list_path(self_vc: DevChunkingContextVc, ident: AssetIdentVc) -> FileSystemPathVc { + self_vc.chunk_path(ident.with_modifier(chunk_list_modifier()), ".json") + } + #[turbo_tasks::function] async fn can_be_in_same_chunk(&self, asset_a: AssetVc, asset_b: AssetVc) -> Result { let parent_dir = asset_a.ident().path().parent().await?; @@ -259,3 +264,8 @@ impl ChunkingContext for DevChunkingContext { Ok(DevChunkingContextVc::new(Value::new(context)).into()) } } + +#[turbo_tasks::function] +fn chunk_list_modifier() -> StringVc { + StringVc::cell("chunk list".to_string()) +} diff --git a/crates/turbopack-core/src/chunk/list/asset.rs b/crates/turbopack-core/src/chunk/list/asset.rs new file mode 100644 index 0000000000000..439256dad380f --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/asset.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use turbo_tasks_fs::FileSystemPathVc; + +use super::content::ChunkListContentVc; +use crate::{ + asset::{Asset, AssetContentVc, AssetVc}, + chunk::{ChunkGroupVc, ChunkReferenceVc, ChunksVc}, + ident::AssetIdentVc, + reference::AssetReferencesVc, + version::{VersionedContent, VersionedContentVc}, +}; + +/// An asset that represents a list of chunks that exist together in a chunk +/// group, and should be *updated* together. +/// +/// A chunk list has no actual content: all it does is merge updates from its +/// chunks into a single update when possible. This is useful for keeping track +/// of changes that affect more than one chunk, or affect the chunk group, e.g.: +/// * moving a module from one chunk to another; +/// * changing a chunk's path. +#[turbo_tasks::value(shared)] +pub(super) struct ChunkListAsset { + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl ChunkListAssetVc { + /// Creates a new [`ChunkListAsset`]. + #[turbo_tasks::function] + pub fn new( + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, + ) -> Self { + ChunkListAsset { + server_root, + chunk_group, + path, + } + .cell() + } + + #[turbo_tasks::function] + async fn get_chunks(self) -> Result { + Ok(self.await?.chunk_group.chunks()) + } + + #[turbo_tasks::function] + async fn content(self) -> Result { + let this = &*self.await?; + Ok(ChunkListContentVc::new( + this.server_root, + this.chunk_group.chunks(), + )) + } +} + +#[turbo_tasks::value_impl] +impl Asset for ChunkListAsset { + #[turbo_tasks::function] + fn ident(&self) -> AssetIdentVc { + AssetIdentVc::from_path(self.path) + } + + #[turbo_tasks::function] + async fn references(&self) -> Result { + let chunks = self.chunk_group.chunks().await?; + + let mut references = Vec::with_capacity(chunks.len()); + for chunk in chunks.iter() { + references.push(ChunkReferenceVc::new(*chunk).into()); + } + + Ok(AssetReferencesVc::cell(references)) + } + + #[turbo_tasks::function] + fn content(self_vc: ChunkListAssetVc) -> AssetContentVc { + self_vc.content().content() + } + + #[turbo_tasks::function] + fn versioned_content(self_vc: ChunkListAssetVc) -> VersionedContentVc { + self_vc.content().into() + } +} diff --git a/crates/turbopack-core/src/chunk/list/content.rs b/crates/turbopack-core/src/chunk/list/content.rs new file mode 100644 index 0000000000000..7e6e05b7adfc9 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/content.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use indexmap::IndexMap; +use turbo_tasks::{IntoTraitRef, TryJoinIterExt}; +use turbo_tasks_fs::{FileContent, FileSystemPathReadRef, FileSystemPathVc}; + +use super::{ + update::update_chunk_list, + version::{ChunkListVersion, ChunkListVersionVc}, +}; +use crate::{ + asset::{Asset, AssetContent, AssetContentVc}, + chunk::ChunksVc, + version::{ + MergeableVersionedContent, MergeableVersionedContentVc, UpdateVc, VersionVc, + VersionedContent, VersionedContentMerger, VersionedContentVc, VersionedContentsVc, + }, +}; + +/// Contents of a [`super::asset::ChunkListAsset`]. +#[turbo_tasks::value] +pub(super) struct ChunkListContent { + pub server_root: FileSystemPathReadRef, + pub chunks_contents: IndexMap, +} + +#[turbo_tasks::value_impl] +impl ChunkListContentVc { + /// Creates a new [`ChunkListContent`]. + #[turbo_tasks::function] + pub async fn new(server_root: FileSystemPathVc, chunks: ChunksVc) -> Result { + let server_root = server_root.await?; + Ok(ChunkListContent { + server_root: server_root.clone(), + chunks_contents: chunks + .await? + .iter() + .map(|chunk| { + let server_root = server_root.clone(); + async move { + Ok(( + server_root + .get_path_to(&*chunk.ident().path().await?) + .map(|path| path.to_string()), + chunk.versioned_content(), + )) + } + }) + .try_join() + .await? + .into_iter() + .filter_map(|(path, content)| path.map(|path| (path, content))) + .collect(), + } + .cell()) + } + + /// Computes the version of this content. + #[turbo_tasks::function] + pub async fn version(self) -> Result { + let this = self.await?; + + let mut by_merger = IndexMap::<_, Vec<_>>::new(); + let mut by_path = IndexMap::<_, _>::new(); + + for (chunk_path, chunk_content) in &this.chunks_contents { + if let Some(mergeable) = + MergeableVersionedContentVc::resolve_from(chunk_content).await? + { + let merger = mergeable.get_merger().resolve().await?; + by_merger.entry(merger).or_default().push(*chunk_content); + } else { + by_path.insert( + chunk_path.clone(), + chunk_content.version().into_trait_ref().await?, + ); + } + } + + let by_merger = by_merger + .into_iter() + .map(|(merger, contents)| { + let merger = merger; + async move { + Ok(( + merger, + merger + .merge(VersionedContentsVc::cell(contents)) + .version() + .into_trait_ref() + .await?, + )) + } + }) + .try_join() + .await? + .into_iter() + .collect(); + + Ok(ChunkListVersion { by_path, by_merger }.cell()) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContent for ChunkListContent { + #[turbo_tasks::function] + fn content(&self) -> AssetContentVc { + AssetContentVc::cell(AssetContent::File(FileContent::NotFound.into())) + } + + #[turbo_tasks::function] + fn version(self_vc: ChunkListContentVc) -> VersionVc { + self_vc.version().into() + } + + #[turbo_tasks::function] + fn update(self_vc: ChunkListContentVc, from_version: VersionVc) -> UpdateVc { + update_chunk_list(self_vc, from_version) + } +} diff --git a/crates/turbopack-core/src/chunk/list/mod.rs b/crates/turbopack-core/src/chunk/list/mod.rs new file mode 100644 index 0000000000000..829280a02890e --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod asset; +pub(crate) mod content; +pub(crate) mod reference; +pub(crate) mod update; +pub(crate) mod version; diff --git a/crates/turbopack-core/src/chunk/list/reference.rs b/crates/turbopack-core/src/chunk/list/reference.rs new file mode 100644 index 0000000000000..cf9a811ac0bfc --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/reference.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use turbo_tasks::{primitives::StringVc, ValueToString, ValueToStringVc}; +use turbo_tasks_fs::FileSystemPathVc; + +use super::asset::ChunkListAssetVc; +use crate::{ + chunk::{ + ChunkGroupVc, ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkingContextVc, + ChunkingType, ChunkingTypeOptionVc, + }, + reference::{AssetReference, AssetReferenceVc}, + resolve::{ResolveResult, ResolveResultVc}, +}; + +/// A reference to a [`ChunkListAsset`]. +/// +/// This is the only way to create a [`ChunkListAsset`]. The asset itself will +/// live under the provided path. +/// +/// [`ChunkListAsset`]: super::asset::ChunkListAsset +#[turbo_tasks::value] +pub struct ChunkListReference { + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl ChunkListReferenceVc { + /// Creates a new [`ChunkListReference`]. + #[turbo_tasks::function] + pub fn new( + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, + ) -> Self { + ChunkListReference { + server_root, + chunk_group, + path, + } + .cell() + } +} + +#[turbo_tasks::value_impl] +impl ValueToString for ChunkListReference { + #[turbo_tasks::function] + async fn to_string(&self) -> Result { + Ok(StringVc::cell(format!( + "referenced chunk list {}", + self.path.to_string().await? + ))) + } +} + +#[turbo_tasks::value_impl] +impl AssetReference for ChunkListReference { + #[turbo_tasks::function] + fn resolve_reference(&self) -> ResolveResultVc { + ResolveResult::asset( + ChunkListAssetVc::new(self.server_root, self.chunk_group, self.path).into(), + ) + .cell() + } +} + +#[turbo_tasks::value_impl] +impl ChunkableAssetReference for ChunkListReference { + #[turbo_tasks::function] + fn chunking_type(&self, _context: ChunkingContextVc) -> ChunkingTypeOptionVc { + ChunkingTypeOptionVc::cell(Some(ChunkingType::Separate)) + } +} diff --git a/crates/turbopack-core/src/chunk/list/update.rs b/crates/turbopack-core/src/chunk/list/update.rs new file mode 100644 index 0000000000000..4bbbefc0cc7e2 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/update.rs @@ -0,0 +1,166 @@ +use anyhow::Result; +use indexmap::IndexMap; +use serde::Serialize; +use turbo_tasks::{ + primitives::{JsonValueReadRef, JsonValueVc}, + TraitRef, +}; + +use super::{content::ChunkListContentVc, version::ChunkListVersionVc}; +use crate::version::{ + MergeableVersionedContent, MergeableVersionedContentVc, PartialUpdate, TotalUpdate, Update, + UpdateVc, VersionVc, VersionedContent, VersionedContentMerger, VersionedContentsVc, +}; + +/// Update of a chunk list from one version to another. +#[derive(Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +struct ChunkListUpdate<'a> { + /// A map from chunk path to a corresponding update of that chunk. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + chunks: IndexMap<&'a str, ChunkUpdate>, + /// List of merged updates since the last version. + #[serde(skip_serializing_if = "Vec::is_empty")] + merged: Vec, +} + +/// Update of a chunk from one version to another. +#[derive(Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +enum ChunkUpdate { + /// The chunk was updated and must be reloaded. + Total, + /// The chunk was updated and can be merged with the previous version. + Partial { instruction: JsonValueReadRef }, + /// The chunk was added. + Added, + /// The chunk was deleted. + Deleted, +} + +impl<'a> ChunkListUpdate<'a> { + /// Returns `true` if this update is empty. + fn is_empty(&self) -> bool { + let ChunkListUpdate { chunks, merged } = self; + chunks.is_empty() && merged.is_empty() + } +} + +/// Computes the update of a chunk list from one version to another. +#[turbo_tasks::function] +pub(super) async fn update_chunk_list( + content: ChunkListContentVc, + from_version: VersionVc, +) -> Result { + let to_version = content.version(); + let from_version = if let Some(from) = ChunkListVersionVc::resolve_from(from_version).await? { + from + } else { + // It's likely `from_version` is `NotFoundVersion`. + return Ok(Update::Total(TotalUpdate { + to: to_version.into(), + }) + .cell()); + }; + + let to = to_version.await?; + let from = from_version.await?; + + // When to and from point to the same value we can skip comparing them. + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). + if from.ptr_eq(&to) { + return Ok(Update::None.cell()); + } + + let content = content.await?; + + // There are two kind of updates nested within a chunk list update: + // * merged updates; and + // * single chunk updates. + // In order to compute merged updates, we first need to group mergeable chunks + // by common mergers. Then, we compute the update of each group separately. + // Single chunk updates are computed separately and only require a stable chunk + // path to identify the chunk across versions. + let mut by_merger = IndexMap::<_, Vec<_>>::new(); + let mut by_path = IndexMap::<_, _>::new(); + + for (chunk_path, chunk_content) in &content.chunks_contents { + if let Some(mergeable) = MergeableVersionedContentVc::resolve_from(chunk_content).await? { + let merger = mergeable.get_merger().resolve().await?; + by_merger.entry(merger).or_default().push(*chunk_content); + } else { + by_path.insert(chunk_path, chunk_content); + } + } + + let mut chunks = IndexMap::<_, _>::new(); + + for (chunk_path, from_chunk_version) in &from.by_path { + if let Some(chunk_content) = by_path.remove(chunk_path) { + let chunk_update = chunk_content + .update(TraitRef::cell(from_chunk_version.clone())) + .await?; + + match &*chunk_update { + Update::Total(_) => { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Total); + } + Update::Partial(partial) => { + chunks.insert( + chunk_path.as_ref(), + ChunkUpdate::Partial { + instruction: partial.instruction.await?, + }, + ); + } + Update::None => {} + } + } else { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Deleted); + } + } + + for chunk_path in by_path.keys() { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Added); + } + + let mut merged = vec![]; + + for (merger, chunks_contents) in by_merger { + if let Some(from_version) = from.by_merger.get(&merger) { + let content = merger.merge(VersionedContentsVc::cell(chunks_contents)); + + let chunk_update = content.update(TraitRef::cell(from_version.clone())).await?; + + match &*chunk_update { + // Getting a total or not found update from a merger is unexpected. If it happens, + // we have no better option than to short-circuit the update. + Update::Total(_) => { + return Ok(Update::Total(TotalUpdate { + to: to_version.into(), + }) + .cell()); + } + Update::Partial(partial) => { + merged.push(partial.instruction.await?); + } + Update::None => {} + } + } + } + let update = ChunkListUpdate { chunks, merged }; + + let update = if update.is_empty() { + Update::None + } else { + Update::Partial(PartialUpdate { + to: to_version.into(), + instruction: JsonValueVc::cell(serde_json::to_value(&update)?), + }) + }; + + Ok(update.into()) +} diff --git a/crates/turbopack-core/src/chunk/list/version.rs b/crates/turbopack-core/src/chunk/list/version.rs new file mode 100644 index 0000000000000..a73dbec807546 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/version.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use indexmap::IndexMap; +use turbo_tasks::{primitives::StringVc, TraitRef, TryJoinIterExt}; +use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; + +use crate::version::{Version, VersionVc, VersionedContentMergerVc}; + +/// The version of a [`ChunkListContent`]. +/// +/// [`ChunkListContent`]: super::content::ChunkListContent +#[turbo_tasks::value(shared)] +pub(super) struct ChunkListVersion { + /// A map from chunk path to its version. + #[turbo_tasks(trace_ignore)] + pub by_path: IndexMap>, + /// A map from chunk merger to the version of the merged contents of chunks. + #[turbo_tasks(trace_ignore)] + pub by_merger: IndexMap>, +} + +#[turbo_tasks::value_impl] +impl Version for ChunkListVersion { + #[turbo_tasks::function] + async fn id(&self) -> Result { + let by_path = { + let mut by_path = self + .by_path + .iter() + .map(|(path, version)| async move { + let id = TraitRef::cell(version.clone()).id().await?.clone_value(); + Ok((path, id)) + }) + .try_join() + .await?; + by_path.sort(); + by_path + }; + let by_merger = { + let mut by_merger = self + .by_merger + .iter() + .map(|(_merger, version)| async move { + Ok(TraitRef::cell(version.clone()).id().await?.clone_value()) + }) + .try_join() + .await?; + by_merger.sort(); + by_merger + }; + let mut hasher = Xxh3Hash64Hasher::new(); + hasher.write_value(by_path.len()); + for (path, id) in by_path { + hasher.write_value(path); + hasher.write_value(id); + } + hasher.write_value(by_merger.len()); + for id in by_merger { + hasher.write_value(id); + } + let hash = hasher.finish(); + let hex_hash = encode_hex(hash); + Ok(StringVc::cell(hex_hash)) + } +} diff --git a/crates/turbopack-core/src/chunk/mod.rs b/crates/turbopack-core/src/chunk/mod.rs index e17267cbea021..e5594310357b0 100644 --- a/crates/turbopack-core/src/chunk/mod.rs +++ b/crates/turbopack-core/src/chunk/mod.rs @@ -1,5 +1,6 @@ pub mod chunk_in_group; pub mod dev; +pub(crate) mod list; pub mod optimize; use std::{ @@ -24,6 +25,7 @@ use turbo_tasks::{ use turbo_tasks_fs::FileSystemPathVc; use turbo_tasks_hash::DeterministicHash; +pub use self::list::reference::{ChunkListReference, ChunkListReferenceVc}; use self::{chunk_in_group::ChunkInGroupVc, optimize::optimize}; use crate::{ asset::{Asset, AssetVc, AssetsVc}, @@ -35,7 +37,7 @@ use crate::{ /// A module id, which can be a number or string #[turbo_tasks::value(shared)] -#[derive(Debug, Clone, Hash, DeterministicHash)] +#[derive(Debug, Clone, Hash, Ord, PartialOrd, DeterministicHash)] #[serde(untagged)] pub enum ModuleId { Number(u32), @@ -83,6 +85,10 @@ pub trait ChunkingContext { fn chunk_path(&self, ident: AssetIdentVc, extension: &str) -> FileSystemPathVc; + /// Returns the path to the chunk list file for the given entry module + /// ident. + fn chunk_list_path(&self, ident: AssetIdentVc) -> FileSystemPathVc; + fn can_be_in_same_chunk(&self, asset_a: AssetVc, asset_b: AssetVc) -> BoolVc; fn asset_path(&self, content_hash: &str, extension: &str) -> FileSystemPathVc; diff --git a/crates/turbopack-core/src/version.rs b/crates/turbopack-core/src/version.rs index b5dc6bc6515ea..e421540425ed2 100644 --- a/crates/turbopack-core/src/version.rs +++ b/crates/turbopack-core/src/version.rs @@ -127,6 +127,26 @@ pub trait Version { fn id(&self) -> StringVc; } +/// This trait allows multiple `VersionedContent` to declare which +/// [`VersionedContentMerger`] implementation should be used for merging. +/// +/// [`MergeableVersionedContent`] which return the same merger will be merged +/// together. +#[turbo_tasks::value_trait] +pub trait MergeableVersionedContent: VersionedContent { + fn get_merger(&self) -> VersionedContentMergerVc; +} + +/// A [`VersionedContentMerger`] merges multiple [`VersionedContent`] into a +/// single one. +#[turbo_tasks::value_trait] +pub trait VersionedContentMerger { + fn merge(&self, contents: VersionedContentsVc) -> VersionedContentVc; +} + +#[turbo_tasks::value(transparent)] +pub struct VersionedContents(Vec); + #[turbo_tasks::value] pub struct NotFoundVersion; diff --git a/crates/turbopack-dev-server/src/html.rs b/crates/turbopack-dev-server/src/html.rs index 38d4b87be5b73..4608b0b694637 100644 --- a/crates/turbopack-dev-server/src/html.rs +++ b/crates/turbopack-dev-server/src/html.rs @@ -1,6 +1,6 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use mime_guess::mime::TEXT_HTML_UTF_8; -use turbo_tasks::{debug::ValueDebug, primitives::StringVc}; +use turbo_tasks::primitives::StringVc; use turbo_tasks_fs::{File, FileSystemPathVc}; use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; use turbopack_core::{ @@ -8,7 +8,7 @@ use turbopack_core::{ chunk::{Chunk, ChunkGroupVc, ChunkReferenceVc}, ident::AssetIdentVc, reference::AssetReferencesVc, - version::{Update, UpdateVc, Version, VersionVc, VersionedContent, VersionedContentVc}, + version::{Version, VersionVc, VersionedContent, VersionedContentVc}, }; /// The HTML entry point of the dev server. @@ -183,28 +183,6 @@ impl VersionedContent for DevHtmlAssetContent { fn version(self_vc: DevHtmlAssetContentVc) -> VersionVc { self_vc.version().into() } - - #[turbo_tasks::function] - async fn update(self_vc: DevHtmlAssetContentVc, from_version: VersionVc) -> Result { - let from_version = DevHtmlAssetVersionVc::resolve_from(from_version) - .await? - .context("version must be an `DevHtmlAssetVersionVc`")?; - let to_version = self_vc.version(); - - let to = to_version.await?; - let from = from_version.await?; - - if to.content.chunk_paths == from.content.chunk_paths { - return Ok(Update::None.into()); - } - - Err(anyhow!( - "cannot update `DevHtmlAssetContentVc` from version {:?} to version {:?}: the \ - versions contain different chunks, which is not yet supported", - from_version.dbg().await?, - to_version.dbg().await?, - )) - } } #[turbo_tasks::value] diff --git a/crates/turbopack-dev-server/src/source/mod.rs b/crates/turbopack-dev-server/src/source/mod.rs index b08ee62c47d78..e5b5c36eb5576 100644 --- a/crates/turbopack-dev-server/src/source/mod.rs +++ b/crates/turbopack-dev-server/src/source/mod.rs @@ -202,12 +202,6 @@ pub struct NeededData { pub vary: ContentSourceDataVary, } -impl From for ContentSourceContentVc { - fn from(content: VersionedContentVc) -> Self { - ContentSourceContentVc::static_content(content) - } -} - /// Additional info passed to the ContentSource. It was extracted from the http /// request. /// diff --git a/crates/turbopack-dev-server/src/source/resolve.rs b/crates/turbopack-dev-server/src/source/resolve.rs index 3b549ddac471a..5207dd7d8443f 100644 --- a/crates/turbopack-dev-server/src/source/resolve.rs +++ b/crates/turbopack-dev-server/src/source/resolve.rs @@ -85,7 +85,7 @@ pub async fn resolve_source_request( } current_asset_path = new_asset_path; data = ContentSourceData::default(); - } // _ => , + } ContentSourceContent::NotFound => { break Ok(ResolveSourceRequestResult::NotFound.cell()) } diff --git a/crates/turbopack-dev-server/src/update/protocol.rs b/crates/turbopack-dev-server/src/update/protocol.rs index 3168f46a58f68..880fb4d6ee9d3 100644 --- a/crates/turbopack-dev-server/src/update/protocol.rs +++ b/crates/turbopack-dev-server/src/update/protocol.rs @@ -56,6 +56,12 @@ impl<'a> ClientUpdateInstruction<'a> { Self::new(resource, ClientUpdateInstructionType::Restart, issues) } + /// Returns a [`ClientUpdateInstruction`] that indicates that the resource + /// was not found. + pub fn not_found(resource: &'a ResourceIdentifier) -> Self { + Self::new(resource, ClientUpdateInstructionType::NotFound, &[]) + } + pub fn partial( resource: &'a ResourceIdentifier, instruction: &'a Value, @@ -85,6 +91,7 @@ impl<'a> ClientUpdateInstruction<'a> { #[serde(tag = "type", rename_all = "camelCase")] pub enum ClientUpdateInstructionType<'a> { Restart, + NotFound, Partial { instruction: &'a Value }, Issues, } diff --git a/crates/turbopack-dev-server/src/update/server.rs b/crates/turbopack-dev-server/src/update/server.rs index d4719ac194199..e5da3430de5c1 100644 --- a/crates/turbopack-dev-server/src/update/server.rs +++ b/crates/turbopack-dev-server/src/update/server.rs @@ -85,7 +85,7 @@ impl UpdateServer

{ } } Some((resource, update)) = streams.next() => { - Self::send_update(&mut client, resource, &update).await?; + Self::send_update(&mut client, &mut streams, resource, &update).await?; } else => break } @@ -96,35 +96,46 @@ impl UpdateServer

{ async fn send_update( client: &mut UpdateClient, + streams: &mut StreamMap, resource: ResourceIdentifier, - update: &UpdateStreamItem, + item: &UpdateStreamItem, ) -> Result<()> { - let issues = update - .issues - .iter() - .map(|p| (&**p).into()) - .collect::>>(); - - match &*update.update { - Update::Partial(partial) => { - let partial_instruction = partial.instruction.await?; + match item { + UpdateStreamItem::NotFound => { + // If the resource was not found, we remove the stream and indicate that to the + // client. + streams.remove(&resource); client - .send(ClientUpdateInstruction::partial( - &resource, - &partial_instruction, - &issues, - )) + .send(ClientUpdateInstruction::not_found(&resource)) .await?; } - Update::Total(_total) => { - client - .send(ClientUpdateInstruction::restart(&resource, &issues)) - .await?; - } - Update::None => { - client - .send(ClientUpdateInstruction::issues(&resource, &issues)) - .await?; + UpdateStreamItem::Found { update, issues } => { + let issues = issues + .iter() + .map(|p| (&**p).into()) + .collect::>>(); + match &**update { + Update::Partial(partial) => { + let partial_instruction = partial.instruction.await?; + client + .send(ClientUpdateInstruction::partial( + &resource, + &partial_instruction, + &issues, + )) + .await?; + } + Update::Total(_total) => { + client + .send(ClientUpdateInstruction::restart(&resource, &issues)) + .await?; + } + Update::None => { + client + .send(ClientUpdateInstruction::issues(&resource, &issues)) + .await?; + } + } } } diff --git a/crates/turbopack-dev-server/src/update/stream.rs b/crates/turbopack-dev-server/src/update/stream.rs index 0bd652fde3c2e..977348bc9a5f4 100644 --- a/crates/turbopack-dev-server/src/update/stream.rs +++ b/crates/turbopack-dev-server/src/update/stream.rs @@ -41,16 +41,25 @@ async fn get_update_stream_item( let content = get_content(); match &*content.await? { - ResolveSourceRequestResult::Static(static_content, _) => { - let resolved_content = static_content.await?.content; + ResolveSourceRequestResult::Static(static_content_vc, _) => { + let static_content = static_content_vc.await?; + + // This can happen when a chunk is removed from the asset graph. + if static_content.status_code == 404 { + return Ok(UpdateStreamItem::NotFound.cell()); + } + + let resolved_content = static_content.content; let from = from.get(); let update = resolved_content.update(from); let mut plain_issues = peek_issues(update).await?; extend_issues(&mut plain_issues, peek_issues(content).await?); - Ok(UpdateStreamItem { - update: update.await?, + let update = update.await?; + + Ok(UpdateStreamItem::Found { + update, issues: plain_issues, } .cell()) @@ -71,7 +80,7 @@ async fn get_update_stream_item( Update::None.cell() }; - Ok(UpdateStreamItem { + Ok(UpdateStreamItem::Found { update: update.await?, issues: plain_issues, } @@ -148,28 +157,42 @@ impl UpdateStream { let mut last_had_issues = false; - let stream = ReceiverStream::new(rx).filter_map(move |update| { - let has_issues = !update.issues.is_empty(); - let issues_changed = has_issues != last_had_issues; - last_had_issues = has_issues; + let stream = ReceiverStream::new(rx).filter_map(move |item| { + let (has_issues, issues_changed) = + if let UpdateStreamItem::Found { issues, .. } = &*item { + let has_issues = !issues.is_empty(); + let issues_changed = has_issues != last_had_issues; + last_had_issues = has_issues; + (has_issues, issues_changed) + } else { + (false, false) + }; async move { - match &*update.update { - Update::Partial(PartialUpdate { to, .. }) - | Update::Total(TotalUpdate { to }) => { - version_state - .set(*to) - .await - .expect("failed to update version"); - - Some(update) + match &*item { + UpdateStreamItem::NotFound => { + // Propagate not found updates so we can drop this update stream. + Some(item) } - // Do not propagate empty updates. - Update::None => { - if has_issues || issues_changed { - Some(update) - } else { - None + UpdateStreamItem::Found { update, .. } => { + match &**update { + Update::Partial(PartialUpdate { to, .. }) + | Update::Total(TotalUpdate { to }) => { + version_state + .set(*to) + .await + .expect("failed to update version"); + + Some(item) + } + // Do not propagate empty updates. + Update::None => { + if has_issues || issues_changed { + Some(item) + } else { + None + } + } } } } @@ -192,7 +215,11 @@ impl Stream for UpdateStream { } #[turbo_tasks::value(serialization = "none")] -pub struct UpdateStreamItem { - pub update: UpdateReadRef, - pub issues: Vec, +#[derive(Debug)] +pub enum UpdateStreamItem { + NotFound, + Found { + update: UpdateReadRef, + issues: Vec, + }, } diff --git a/crates/turbopack-ecmascript/js/src/runtime.dom.js b/crates/turbopack-ecmascript/js/src/runtime.dom.js index 3916e66eef60a..82d4aef9a799f 100644 --- a/crates/turbopack-ecmascript/js/src/runtime.dom.js +++ b/crates/turbopack-ecmascript/js/src/runtime.dom.js @@ -2,7 +2,7 @@ /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -33,5 +33,64 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; diff --git a/crates/turbopack-ecmascript/js/src/runtime.js b/crates/turbopack-ecmascript/js/src/runtime.js index 10d5db91aa5f9..c3a79272dcc73 100644 --- a/crates/turbopack-ecmascript/js/src/runtime.js +++ b/crates/turbopack-ecmascript/js/src/runtime.js @@ -20,8 +20,11 @@ /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -73,6 +76,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -347,6 +373,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -488,7 +515,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -499,18 +526,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -527,7 +556,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -559,35 +588,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -600,10 +638,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -637,24 +680,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -677,22 +723,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -782,17 +984,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -895,10 +1115,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -923,18 +1153,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -944,18 +1238,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); } +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -976,6 +1309,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -983,7 +1317,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-ecmascript/js/types/index.d.ts b/crates/turbopack-ecmascript/js/types/index.d.ts index c47e55a70631b..e2ae613e9e1c8 100644 --- a/crates/turbopack-ecmascript/js/types/index.d.ts +++ b/crates/turbopack-ecmascript/js/types/index.d.ts @@ -73,10 +73,13 @@ interface Runtime { cache: Record; instantiateRuntimeModule: (moduleId: ModuleId) => Module; + registerChunkList: (chunkPath: ChunkPath, chunkPaths: ChunkPath[]) => void; } interface RuntimeBackend { - loadChunk: (chunkPath: ChunkPath, from: ModuleId) => Promise; + loadChunk: (chunkPath: ChunkPath, from?: ModuleId) => Promise; + reloadChunk?: (chunkPath: ChunkPath) => Promise; + unloadChunk?: (chunkPath: ChunkPath) => void; restart: () => void; } diff --git a/crates/turbopack-ecmascript/js/types/protocol.d.ts b/crates/turbopack-ecmascript/js/types/protocol.d.ts index a4271bfd62bd9..b269a2341833f 100644 --- a/crates/turbopack-ecmascript/js/types/protocol.d.ts +++ b/crates/turbopack-ecmascript/js/types/protocol.d.ts @@ -7,9 +7,12 @@ export type ServerMessage = { | { type: "restart"; } + | { + type: "notFound"; + } | { type: "partial"; - instruction: EcmascriptChunkUpdate; + instruction: PartialUpdate; } | { type: "issues"; @@ -21,14 +24,59 @@ type UnknownType = { type: "future-type-marker-do-not-use-or-you-will-be-fired"; }; -export type EcmascriptChunkUpdate = { - type: "EcmascriptChunkUpdate"; - added: Record; - modified: Record; - deleted: ModuleId[]; +export type PartialUpdate = + | ChunkListUpdate + | { + type: never; + }; + +export type ChunkListUpdate = { + type: "ChunkListUpdate"; + chunks?: Record; + merged?: MergedChunkUpdate[]; +}; + +export type ChunkUpdate = + | { + type: "added"; + } + | { type: "deleted" } + | { type: "total" } + // We currently don't have any chunks that can be updated partially that can't + // be merged either. So these updates would go into `MergedChunkUpdate` instead. + | { type: "partial"; instruction: never }; + +export type MergedChunkUpdate = + | EcmascriptMergedUpdate + | { + type: never; + }; + +export type EcmascriptMergedUpdate = { + type: "EcmascriptMergedUpdate"; + entries?: Record; + chunks?: Record; }; -export type HmrUpdateEntry = { +export type EcmascriptMergedChunkUpdate = + | { + type: "added"; + modules?: ModuleId[]; + } + | { + type: "deleted"; + modules?: ModuleId[]; + } + | { + type: "partial"; + added?: ModuleId[]; + deleted?: ModuleId[]; + } + | { + type: never; + }; + +export type EcmascriptModuleEntry = { code: ModuleFactoryString; url: string; map?: string; diff --git a/crates/turbopack-ecmascript/src/chunk/content.rs b/crates/turbopack-ecmascript/src/chunk/content.rs index 3f1177815181b..b094fede660b9 100644 --- a/crates/turbopack-ecmascript/src/chunk/content.rs +++ b/crates/turbopack-ecmascript/src/chunk/content.rs @@ -15,15 +15,18 @@ use turbopack_core::{ environment::{ChunkLoading, EnvironmentVc}, reference::AssetReferenceVc, source_map::{GenerateSourceMap, GenerateSourceMapVc, OptionSourceMapVc, SourceMapVc}, - version::{UpdateVc, VersionVc, VersionedContent, VersionedContentVc}, + version::{ + MergeableVersionedContent, MergeableVersionedContentVc, UpdateVc, VersionVc, + VersionedContent, VersionedContentMergerVc, VersionedContentVc, + }, }; use super::{ evaluate::EcmascriptChunkContentEvaluateVc, item::{EcmascriptChunkItemVc, EcmascriptChunkItems, EcmascriptChunkItemsVc}, + merged::merger::EcmascriptChunkContentMergerVc, placeable::{EcmascriptChunkPlaceableVc, EcmascriptChunkPlaceablesVc}, snapshot::EcmascriptChunkContentEntriesSnapshotReadRef, - update::update_ecmascript_chunk, version::{EcmascriptChunkVersion, EcmascriptChunkVersionVc}, }; use crate::utils::stringify_js; @@ -174,7 +177,7 @@ impl EcmascriptChunkContentVc { #[turbo_tasks::value_impl] impl EcmascriptChunkContentVc { #[turbo_tasks::function] - pub(super) async fn version(self) -> Result { + pub(super) async fn own_version(self) -> Result { let this = self.await?; let chunk_server_path = if let Some(path) = this.output_root.get_path_to(&this.chunk_path) { path @@ -234,12 +237,29 @@ impl EcmascriptChunkContentVc { .map(|id| async move { let id = id.await?; let id = stringify_js(&id); - Ok(format!(r#"instantiateRuntimeModule({id});"#)) as Result<_> + Ok(format!(r#" instantiateRuntimeModule({id});"#)) as Result<_> }) .try_join() .await? .join("\n"); + let chunk_list_register = evaluate + .chunk_list_path + .as_deref() + .map(|path| { + format!( + r#"registerChunkList({}, {});"#, + stringify_js(&path), + stringify_js( + &evaluate + .ecma_chunks_server_paths + .iter() + .chain(&evaluate.other_chunks_server_paths) + .collect::>() + ) + ) + }) + .unwrap_or_else(String::new); // Add a runnable to the chunk that requests the entry module to ensure it gets // executed when the chunk is evaluated. // The condition stops the entry module from being executed while chunks it @@ -249,9 +269,10 @@ impl EcmascriptChunkContentVc { writedoc!( code, r#" - , ({{ loadedChunks, instantiateRuntimeModule }}) => {{ - if(!(true{condition})) return true; - {entries_instantiations} + , ({{ loadedChunks, instantiateRuntimeModule, registerChunkList }}) => {{ + if (!(true{condition})) return true; + {chunk_list_register} + {entries_instantiations} }} "# )?; @@ -322,12 +343,20 @@ impl VersionedContent for EcmascriptChunkContent { #[turbo_tasks::function] fn version(self_vc: EcmascriptChunkContentVc) -> VersionVc { - self_vc.version().into() + self_vc.own_version().into() } #[turbo_tasks::function] - fn update(self_vc: EcmascriptChunkContentVc, from_version: VersionVc) -> UpdateVc { - update_ecmascript_chunk(self_vc, from_version) + fn update(_self_vc: EcmascriptChunkContentVc, _from_version: VersionVc) -> Result { + bail!("EcmascriptChunkContent is not updateable") + } +} + +#[turbo_tasks::value_impl] +impl MergeableVersionedContent for EcmascriptChunkContent { + #[turbo_tasks::function] + fn get_merger(&self) -> VersionedContentMergerVc { + EcmascriptChunkContentMergerVc::new().into() } } diff --git a/crates/turbopack-ecmascript/src/chunk/evaluate.rs b/crates/turbopack-ecmascript/src/chunk/evaluate.rs index cf55359f1204b..2b266fee1f549 100644 --- a/crates/turbopack-ecmascript/src/chunk/evaluate.rs +++ b/crates/turbopack-ecmascript/src/chunk/evaluate.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use turbo_tasks_fs::FileSystemPathVc; use turbopack_core::chunk::{ chunk_in_group::ChunkInGroupVc, Chunk, ChunkGroupVc, ChunkingContext, ChunkingContextVc, ModuleIdVc, @@ -20,6 +21,9 @@ pub struct EcmascriptChunkEvaluate { /// All chunks of this chunk group need to be ready for execution to start. /// When None, it will use a chunk group created from the current chunk. pub chunk_group: Option, + /// The path to the chunk list asset. This will be used to register the + /// chunk list when this chunk is evaluated. + pub chunk_list_path: Option, } #[turbo_tasks::value_impl] @@ -33,6 +37,7 @@ impl EcmascriptChunkEvaluateVc { let &EcmascriptChunkEvaluate { evaluate_entries, chunk_group, + chunk_list_path, } = &*self.await?; let chunk_group = chunk_group.unwrap_or_else(|| ChunkGroupVc::from_chunk(origin_chunk.into())); @@ -63,10 +68,19 @@ impl EcmascriptChunkEvaluateVc { .iter() .map(|entry| entry.as_chunk_item(context).id()) .collect(); + let chunk_list_path = if let Some(chunk_list_path) = chunk_list_path { + let chunk_list_path = chunk_list_path.await?; + output_root + .get_path_to(&chunk_list_path) + .map(|path| path.to_string()) + } else { + None + }; Ok(EcmascriptChunkContentEvaluate { ecma_chunks_server_paths, other_chunks_server_paths, entry_modules_ids, + chunk_list_path, } .cell()) } @@ -77,4 +91,5 @@ pub(super) struct EcmascriptChunkContentEvaluate { pub ecma_chunks_server_paths: Vec, pub other_chunks_server_paths: Vec, pub entry_modules_ids: Vec, + pub chunk_list_path: Option, } diff --git a/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs b/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs index 6d42c59f598da..b4d4f3ded8ce0 100644 --- a/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs +++ b/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs @@ -6,7 +6,10 @@ use turbo_tasks::{primitives::StringVc, ValueToString}; use turbo_tasks_fs::FileSystemPathVc; use turbopack_core::{ asset::Asset, - chunk::{Chunk, ChunkItem, ChunkItemVc, ChunkableAsset, ChunkingContext, ChunkingContextVc}, + chunk::{ + Chunk, ChunkItem, ChunkItemVc, ChunkListReferenceVc, ChunkableAsset, ChunkingContext, + ChunkingContextVc, + }, ident::AssetIdentVc, reference::AssetReferencesVc, }; @@ -28,11 +31,6 @@ fn modifier() -> StringVc { StringVc::cell("loader".to_string()) } -#[turbo_tasks::function] -fn chunk_list_modifier() -> StringVc { - StringVc::cell("chunks list".to_string()) -} - /// The manifest loader item is shipped in the same chunk that uses the dynamic /// `import()` expression. Its responsibility is to load the manifest chunk from /// the server. The dynamic import has been rewritten to import this manifest @@ -59,12 +57,9 @@ impl ManifestLoaderItemVc { } #[turbo_tasks::function] - async fn chunks_list_path(self) -> Result { + async fn chunk_list_path(self) -> Result { let this = &*self.await?; - Ok(this.context.chunk_path( - this.manifest.ident().with_modifier(chunk_list_modifier()), - ".json", - )) + Ok(this.context.chunk_list_path(this.manifest.ident())) } } @@ -78,11 +73,20 @@ impl ChunkItem for ManifestLoaderItem { #[turbo_tasks::function] async fn references(self_vc: ManifestLoaderItemVc) -> Result { let this = &*self_vc.await?; - Ok(AssetReferencesVc::cell(vec![ManifestChunkAssetReference { - manifest: this.manifest, - } - .cell() - .into()])) + Ok(AssetReferencesVc::cell(vec![ + ManifestChunkAssetReference { + manifest: this.manifest, + } + .cell() + .into(), + // This creates the chunk list corresponding to the manifest chunk's chunk group. + ChunkListReferenceVc::new( + this.context.output_root(), + this.manifest.chunk_group(), + self_vc.chunk_list_path(), + ) + .into(), + ])) } } @@ -143,6 +147,7 @@ impl EcmascriptChunkItem for ManifestLoaderItem { return __turbopack_load__({chunk_server_path}).then(() => {{ return __turbopack_require__({item_id}); }}).then((chunks_paths) => {{ + __turbopack_register_chunk_list__({chunk_list_path}, chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }}).then(() => {{ return __turbopack_import__({dynamic_id}); @@ -151,7 +156,12 @@ impl EcmascriptChunkItem for ManifestLoaderItem { "#, chunk_server_path = stringify_js(chunk_server_path), item_id = stringify_js(item_id), - dynamic_id = stringify_js(dynamic_id) + dynamic_id = stringify_js(dynamic_id), + chunk_list_path = stringify_js( + output_root + .get_path_to(&*self_vc.chunk_list_path().await?) + .ok_or(anyhow!("chunk list path is not in output root"))? + ) )?; Ok(EcmascriptChunkItemContent { diff --git a/crates/turbopack-ecmascript/src/chunk/merged/content.rs b/crates/turbopack-ecmascript/src/chunk/merged/content.rs new file mode 100644 index 0000000000000..0f8dd9754bc67 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/content.rs @@ -0,0 +1,62 @@ +use anyhow::{bail, Result}; +use turbo_tasks::TryJoinIterExt; +use turbopack_core::{ + asset::AssetContentVc, + version::{UpdateVc, VersionVc, VersionedContent, VersionedContentVc}, +}; + +use super::{ + update::update_ecmascript_merged_chunk, + version::{EcmascriptMergedChunkVersion, EcmascriptMergedChunkVersionVc}, +}; +use crate::chunk::content::EcmascriptChunkContentVc; + +/// Composite [`EcmascriptChunkContent`] that is the result of merging multiple +/// EcmaScript chunk's contents together through the +/// [`EcmascriptChunkContentMerger`]. +/// +/// [`EcmascriptChunkContentMerger`]: super::merger::EcmascriptChunkContentMerger +#[turbo_tasks::value(serialization = "none", shared)] +pub(super) struct EcmascriptMergedChunkContent { + pub contents: Vec, +} + +#[turbo_tasks::value_impl] +impl EcmascriptMergedChunkContentVc { + #[turbo_tasks::function] + pub async fn version(self) -> Result { + Ok(EcmascriptMergedChunkVersion { + versions: self + .await? + .contents + .iter() + .map(|content| async move { content.own_version().await }) + .try_join() + .await?, + } + .cell()) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContent for EcmascriptMergedChunkContent { + #[turbo_tasks::function] + fn content(_self_vc: EcmascriptMergedChunkContentVc) -> Result { + bail!("EcmascriptMergedChunkContent does not have content") + } + + #[turbo_tasks::function] + fn version(self_vc: EcmascriptMergedChunkContentVc) -> VersionVc { + self_vc.version().into() + } + + #[turbo_tasks::function] + async fn update( + self_vc: EcmascriptMergedChunkContentVc, + from_version: VersionVc, + ) -> Result { + Ok(update_ecmascript_merged_chunk(self_vc, from_version) + .await? + .cell()) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/merger.rs b/crates/turbopack-ecmascript/src/chunk/merged/merger.rs new file mode 100644 index 0000000000000..ddff0f06e2b67 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/merger.rs @@ -0,0 +1,44 @@ +use anyhow::{bail, Result}; +use turbo_tasks::TryJoinIterExt; +use turbopack_core::version::{ + VersionedContentMerger, VersionedContentMergerVc, VersionedContentVc, VersionedContentsVc, +}; + +use super::content::EcmascriptMergedChunkContent; +use crate::chunk::content::EcmascriptChunkContentVc; + +/// Merges multiple [`EcmascriptChunkContent`] into a single +/// [`EcmascriptMergedChunkContent`]. This is useful for generating a single +/// update for multiple ES chunks updating all at the same time. +#[turbo_tasks::value] +pub(crate) struct EcmascriptChunkContentMerger; + +#[turbo_tasks::value_impl] +impl EcmascriptChunkContentMergerVc { + /// Creates a new [`EcmascriptChunkContentMerger`]. + #[turbo_tasks::function] + pub fn new() -> Self { + Self::cell(EcmascriptChunkContentMerger) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContentMerger for EcmascriptChunkContentMerger { + #[turbo_tasks::function] + async fn merge(&self, contents: VersionedContentsVc) -> Result { + let contents = contents + .await? + .iter() + .map(|content| async move { + if let Some(content) = EcmascriptChunkContentVc::resolve_from(content).await? { + Ok(content) + } else { + bail!("expected EcmascriptChunkContentVc") + } + }) + .try_join() + .await?; + + Ok(EcmascriptMergedChunkContent { contents }.cell().into()) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/mod.rs b/crates/turbopack-ecmascript/src/chunk/merged/mod.rs new file mode 100644 index 0000000000000..38431505fc60f --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod content; +pub(crate) mod merger; +pub(crate) mod update; +pub(crate) mod version; diff --git a/crates/turbopack-ecmascript/src/chunk/merged/update.rs b/crates/turbopack-ecmascript/src/chunk/merged/update.rs new file mode 100644 index 0000000000000..7d7601b609298 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/update.rs @@ -0,0 +1,255 @@ +use anyhow::Result; +use indexmap::{IndexMap, IndexSet}; +use serde::Serialize; +use turbo_tasks::{primitives::JsonValueVc, TryJoinIterExt}; +use turbo_tasks_fs::rope::Rope; +use turbopack_core::{ + chunk::{ModuleId, ModuleIdReadRef}, + code_builder::CodeReadRef, + version::{PartialUpdate, TotalUpdate, Update, VersionVc}, +}; + +use super::{content::EcmascriptMergedChunkContentVc, version::EcmascriptMergedChunkVersionVc}; +use crate::chunk::{ + update::{update_ecmascript_chunk, EcmascriptChunkUpdate}, + version::EcmascriptChunkVersionReadRef, +}; + +#[derive(Serialize, Default)] +#[serde(tag = "type", rename_all = "camelCase")] +struct EcmascriptMergedUpdate<'a> { + /// A map from module id to latest module entry. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + entries: IndexMap, + /// A map from chunk path to the chunk update. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + chunks: IndexMap<&'a str, EcmascriptMergedChunkUpdate>, +} + +impl EcmascriptMergedUpdate<'_> { + fn is_empty(&self) -> bool { + self.entries.is_empty() && self.chunks.is_empty() + } +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum EcmascriptMergedChunkUpdate { + Added(EcmascriptMergedChunkAdded), + Deleted(EcmascriptMergedChunkDeleted), + Partial(EcmascriptMergedChunkPartial), +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkAdded { + #[serde(skip_serializing_if = "IndexSet::is_empty")] + modules: IndexSet, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkDeleted { + // Technically, this is redundant, since the client will already know all + // modules in the chunk from the previous version. However, it's useful for + // merging updates without access to an initial state. + #[serde(skip_serializing_if = "IndexSet::is_empty")] + modules: IndexSet, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkPartial { + #[serde(skip_serializing_if = "IndexSet::is_empty")] + added: IndexSet, + #[serde(skip_serializing_if = "IndexSet::is_empty")] + deleted: IndexSet, +} + +#[derive(Serialize)] +struct EcmascriptModuleEntry { + code: Rope, + url: String, + map: Option, +} + +impl EcmascriptModuleEntry { + fn new(id: &ModuleId, code: CodeReadRef, chunk_path: &str) -> Self { + /// serde_qs can't serialize a lone enum when it's [serde::untagged]. + #[derive(Serialize)] + struct Id<'a> { + id: &'a ModuleId, + } + let id = serde_qs::to_string(&Id { id }).unwrap(); + EcmascriptModuleEntry { + // Cloning a rope is cheap. + code: code.source_code().clone(), + url: format!("/{}?{}", chunk_path, &id), + map: code + .has_source_map() + .then(|| format!("/__turbopack_sourcemap__/{}.map?{}", chunk_path, &id)), + } + } +} + +/// Helper structure to get a module's hash from multiple different chunk +/// versions, without having to actually merge the versions into a single +/// hashmap, which would be expensive. +struct MergedModuleMap { + versions: Vec, +} + +impl MergedModuleMap { + /// Creates a new `MergedModuleMap` from the given versions. + fn new(versions: Vec) -> Self { + Self { versions } + } + + /// Returns the hash of the module with the given id, or `None` if the + /// module is not present in any of the versions. + fn get(&self, id: &ModuleId) -> Option { + for version in &self.versions { + if let Some(hash) = version.module_factories_hashes.get(id) { + return Some(*hash); + } + } + None + } +} + +pub(super) async fn update_ecmascript_merged_chunk( + content: EcmascriptMergedChunkContentVc, + from_version: VersionVc, +) -> Result { + let to_merged_version = content.version(); + let from_merged_version = + if let Some(from) = EcmascriptMergedChunkVersionVc::resolve_from(from_version).await? { + from + } else { + // It's likely `from_version` is `NotFoundVersion`. + return Ok(Update::Total(TotalUpdate { + to: to_merged_version.into(), + })); + }; + + let to = to_merged_version.await?; + let from = from_merged_version.await?; + + // When to and from point to the same value we can skip comparing them. + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). + if from.ptr_eq(&to) { + return Ok(Update::None); + } + + let mut from_versions_by_chunk_path: IndexMap<_, _> = from + .versions + .iter() + .map(|version| (&*version.chunk_server_path, version)) + .collect(); + + let merged_module_map = MergedModuleMap::new(from.versions.iter().cloned().collect()); + + let content = content.await?; + let to_contents = content + .contents + .iter() + .map(|content| async move { Ok((*content, content.await?)) }) + .try_join() + .await?; + + let mut merged_update = EcmascriptMergedUpdate::default(); + + for (content, content_ref) in &to_contents { + let Some(chunk_server_path) = content_ref + .output_root + .get_path_to(&content_ref.chunk_path) else { + continue; + }; + + let chunk_update = if let Some(from_version) = + from_versions_by_chunk_path.remove(chunk_server_path) + { + // The chunk was present in the previous version, so we must update it. + let update = update_ecmascript_chunk(*content, from_version).await?; + + match update { + EcmascriptChunkUpdate::None => { + // Nothing changed, so we can skip this chunk. + continue; + } + EcmascriptChunkUpdate::Partial(chunk_partial) => { + // The chunk was updated. + let mut partial = EcmascriptMergedChunkPartial::default(); + + for (module_id, (module_hash, module_code)) in chunk_partial.added { + partial.added.insert(module_id.clone()); + + if merged_module_map.get(&module_id) != Some(module_hash) { + let entry = EcmascriptModuleEntry::new( + &*module_id, + module_code.clone(), + chunk_server_path, + ); + merged_update.entries.insert(module_id, entry); + } + } + + partial.deleted.extend(chunk_partial.deleted.into_keys()); + + for (module_id, module_code) in chunk_partial.modified { + let entry = + EcmascriptModuleEntry::new(&*module_id, module_code, chunk_server_path); + merged_update.entries.insert(module_id, entry); + } + + EcmascriptMergedChunkUpdate::Partial(partial) + } + } + } else { + // The chunk was added in this version. + let mut added = EcmascriptMergedChunkAdded::default(); + + for entry in &content_ref.module_factories { + added.modules.insert(entry.id.clone()); + + if merged_module_map.get(&entry.id) != Some(entry.hash) { + merged_update.entries.insert( + entry.id.clone(), + EcmascriptModuleEntry::new( + &entry.id, + entry.code.clone(), + chunk_server_path, + ), + ); + } + } + + EcmascriptMergedChunkUpdate::Added(added) + }; + + merged_update.chunks.insert(chunk_server_path, chunk_update); + } + + // Deleted chunks. + for (chunk_server_path, chunk_version) in from_versions_by_chunk_path { + let hashes = &chunk_version.module_factories_hashes; + merged_update.chunks.insert( + chunk_server_path, + EcmascriptMergedChunkUpdate::Deleted(EcmascriptMergedChunkDeleted { + modules: hashes.keys().cloned().collect(), + }), + ); + } + + let update = if merged_update.is_empty() { + Update::None + } else { + Update::Partial(PartialUpdate { + to: to_merged_version.into(), + instruction: JsonValueVc::cell(serde_json::to_value(&merged_update)?), + }) + }; + + Ok(update) +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/version.rs b/crates/turbopack-ecmascript/src/chunk/merged/version.rs new file mode 100644 index 0000000000000..8e9684d8ed504 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/version.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use turbo_tasks::{primitives::StringVc, ReadRef, TryJoinIterExt}; +use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; +use turbopack_core::version::{Version, VersionVc}; + +use crate::chunk::version::EcmascriptChunkVersionReadRef; + +/// The version of a [`super::content::EcmascriptMergedChunkContent`]. This is +/// essentially a composite [`EcmascriptChunkVersion`]. +#[turbo_tasks::value(serialization = "none", shared)] +pub(super) struct EcmascriptMergedChunkVersion { + #[turbo_tasks(trace_ignore)] + pub(super) versions: Vec, +} + +#[turbo_tasks::value_impl] +impl Version for EcmascriptMergedChunkVersion { + #[turbo_tasks::function] + async fn id(&self) -> Result { + let mut hasher = Xxh3Hash64Hasher::new(); + hasher.write_value(self.versions.len()); + let sorted_ids = { + let mut sorted_ids = self + .versions + .iter() + .map(|version| async move { ReadRef::cell(version.clone()).id().await }) + .try_join() + .await?; + sorted_ids.sort(); + sorted_ids + }; + for id in sorted_ids { + hasher.write_value(id); + } + let hash = hasher.finish(); + let hex_hash = encode_hex(hash); + Ok(StringVc::cell(hex_hash)) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/mod.rs b/crates/turbopack-ecmascript/src/chunk/mod.rs index e8e18f1dee172..76a7f742a810e 100644 --- a/crates/turbopack-ecmascript/src/chunk/mod.rs +++ b/crates/turbopack-ecmascript/src/chunk/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod context; pub(crate) mod evaluate; pub(crate) mod item; pub(crate) mod manifest; +pub(crate) mod merged; pub(crate) mod module_factory; pub(crate) mod optimize; pub(crate) mod placeable; @@ -24,8 +25,8 @@ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ optimize::{ChunkOptimizerVc, OptimizableChunk, OptimizableChunkVc}, - Chunk, ChunkGroupReferenceVc, ChunkItem, ChunkReferenceVc, ChunkVc, ChunkingContext, - ChunkingContextVc, + Chunk, ChunkGroupReferenceVc, ChunkGroupVc, ChunkItem, ChunkListReferenceVc, + ChunkReferenceVc, ChunkVc, ChunkingContext, ChunkingContextVc, }, ident::{AssetIdent, AssetIdentVc}, introspect::{ @@ -111,6 +112,7 @@ impl EcmascriptChunkVc { EcmascriptChunkEvaluate { evaluate_entries: entries, chunk_group: None, + chunk_list_path: Some(context.chunk_list_path(main_entry.ident())), } .cell(), ), @@ -393,6 +395,25 @@ impl Asset for EcmascriptChunk { for chunk_group in content.async_chunk_groups.iter() { references.push(ChunkGroupReferenceVc::new(*chunk_group).into()); } + if let Some(evaluate) = this.evaluate { + let EcmascriptChunkEvaluate { + chunk_list_path, + chunk_group, + .. + } = *evaluate.await?; + if let Some(chunk_list_path) = chunk_list_path { + let chunk_group = + chunk_group.unwrap_or_else(|| ChunkGroupVc::from_chunk(self_vc.into())); + references.push( + ChunkListReferenceVc::new( + this.context.output_root(), + chunk_group, + chunk_list_path, + ) + .into(), + ); + } + } references.push(EcmascriptChunkSourceMapAssetReferenceVc::new(self_vc).into()); Ok(AssetReferencesVc::cell(references)) diff --git a/crates/turbopack-ecmascript/src/chunk/module_factory.rs b/crates/turbopack-ecmascript/src/chunk/module_factory.rs index 8fb82390491aa..11bd34bd442cf 100644 --- a/crates/turbopack-ecmascript/src/chunk/module_factory.rs +++ b/crates/turbopack-ecmascript/src/chunk/module_factory.rs @@ -17,6 +17,7 @@ pub(super) async fn module_factory(content: EcmascriptChunkItemContentVc) -> Res "v: __turbopack_export_value__", "c: __turbopack_cache__", "l: __turbopack_load__", + "k: __turbopack_register_chunk_list__", "j: __turbopack_cjs__", "p: process", "g: global", diff --git a/crates/turbopack-ecmascript/src/chunk/optimize.rs b/crates/turbopack-ecmascript/src/chunk/optimize.rs index a3e93bf9abc8a..19725d67878f2 100644 --- a/crates/turbopack-ecmascript/src/chunk/optimize.rs +++ b/crates/turbopack-ecmascript/src/chunk/optimize.rs @@ -399,6 +399,7 @@ async fn optimize_ecmascript( EcmascriptChunkEvaluate { evaluate_entries: evaluate.evaluate_entries, chunk_group: Some(chunk_group), + chunk_list_path: evaluate.chunk_list_path, } .cell(), ), diff --git a/crates/turbopack-ecmascript/src/chunk/snapshot.rs b/crates/turbopack-ecmascript/src/chunk/snapshot.rs index 0f0359da427fe..85da26c2d21ae 100644 --- a/crates/turbopack-ecmascript/src/chunk/snapshot.rs +++ b/crates/turbopack-ecmascript/src/chunk/snapshot.rs @@ -2,7 +2,6 @@ use std::{fmt::Write, io::Write as _, slice::Iter}; use anyhow::Result; use turbo_tasks::{primitives::StringVc, TryJoinIterExt, ValueToString}; -use turbo_tasks_fs::rope::Rope; use turbo_tasks_hash::hash_xxh3_hash64; use turbopack_core::{ chunk::{ChunkItem, ModuleId, ModuleIdReadRef}, @@ -115,10 +114,6 @@ impl EcmascriptChunkContentEntry { pub fn code(&self) -> &Code { &self.code } - - pub fn source_code(&self) -> &Rope { - self.code.source_code() - } } #[turbo_tasks::value_impl] diff --git a/crates/turbopack-ecmascript/src/chunk/update.rs b/crates/turbopack-ecmascript/src/chunk/update.rs index 93b891ea660cb..80a08a767ffd2 100644 --- a/crates/turbopack-ecmascript/src/chunk/update.rs +++ b/crates/turbopack-ecmascript/src/chunk/update.rs @@ -1,121 +1,72 @@ use anyhow::Result; -use indexmap::{IndexMap, IndexSet}; -use serde::Serialize; -use turbo_tasks::primitives::JsonValueVc; -use turbo_tasks_fs::rope::Rope; -use turbopack_core::{ - chunk::ModuleId, - version::{PartialUpdate, TotalUpdate, Update, UpdateVc, VersionVc}, -}; +use indexmap::IndexMap; +use turbopack_core::{chunk::ModuleIdReadRef, code_builder::CodeReadRef}; -use super::{ - content::EcmascriptChunkContentVc, snapshot::EcmascriptChunkContentEntry, - version::EcmascriptChunkVersionVc, -}; +use super::{content::EcmascriptChunkContentVc, version::EcmascriptChunkVersionReadRef}; -#[derive(Serialize)] -#[serde(tag = "type")] -struct EcmascriptChunkUpdate<'a> { - added: IndexMap<&'a ModuleId, HmrUpdateEntry<'a>>, - modified: IndexMap<&'a ModuleId, HmrUpdateEntry<'a>>, - deleted: IndexSet<&'a ModuleId>, +pub(super) enum EcmascriptChunkUpdate { + None, + Partial(EcmascriptChunkPartialUpdate), +} + +pub(super) struct EcmascriptChunkPartialUpdate { + pub added: IndexMap, + pub deleted: IndexMap, + pub modified: IndexMap, } -#[turbo_tasks::function] pub(super) async fn update_ecmascript_chunk( content: EcmascriptChunkContentVc, - from_version: VersionVc, -) -> Result { - let to_version = content.version(); - let from_version = - if let Some(from) = EcmascriptChunkVersionVc::resolve_from(from_version).await? { - from - } else { - return Ok(Update::Total(TotalUpdate { - to: to_version.into(), - }) - .cell()); - }; - - let to = to_version.await?; - let from = from_version.await?; + from: &EcmascriptChunkVersionReadRef, +) -> Result { + let to = content.own_version().await?; // When to and from point to the same value we can skip comparing them. - // This will happen since `cell_local` will not clone the value, but only make - // the local cell point to the same immutable value (Arc). + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). if from.ptr_eq(&to) { - return Ok(Update::None.cell()); + return Ok(EcmascriptChunkUpdate::None); } let content = content.await?; - let chunk_path = &content.chunk_path.path; // TODO(alexkirsz) This should probably be stored as a HashMap already. let mut module_factories: IndexMap<_, _> = content .module_factories .iter() - .map(|entry| (entry.id(), entry)) + .map(|entry| (entry.id.clone(), entry)) .collect(); - let mut added = IndexMap::new(); - let mut modified = IndexMap::new(); - let mut deleted = IndexSet::new(); + let mut added = IndexMap::default(); + let mut modified = IndexMap::default(); + let mut deleted = IndexMap::default(); - for (id, hash) in &from.module_factories_hashes { - let id = &**id; + for (id, from_hash) in &from.module_factories_hashes { + let id = id; if let Some(entry) = module_factories.remove(id) { - if entry.hash != *hash { - modified.insert(id, HmrUpdateEntry::new(entry, chunk_path)); + if entry.hash != *from_hash { + modified.insert(id.clone(), entry.code.clone()); } } else { - deleted.insert(id); + deleted.insert(id.clone(), *from_hash); } } // Remaining entries are added for (id, entry) in module_factories { - added.insert(id, HmrUpdateEntry::new(entry, chunk_path)); + if !from.module_factories_hashes.contains_key(&id) { + added.insert(id, (entry.hash, entry.code.clone())); + } } let update = if added.is_empty() && modified.is_empty() && deleted.is_empty() { - Update::None + EcmascriptChunkUpdate::None } else { - let chunk_update = EcmascriptChunkUpdate { + EcmascriptChunkUpdate::Partial(EcmascriptChunkPartialUpdate { added, modified, deleted, - }; - - Update::Partial(PartialUpdate { - to: to_version.into(), - instruction: JsonValueVc::cell(serde_json::to_value(&chunk_update)?), }) }; - Ok(update.into()) -} - -#[derive(serde::Serialize)] -struct HmrUpdateEntry<'a> { - code: &'a Rope, - url: String, - map: Option, -} - -impl<'a> HmrUpdateEntry<'a> { - fn new(entry: &'a EcmascriptChunkContentEntry, chunk_path: &str) -> Self { - /// serde_qs can't serialize a lone enum when it's [serde::untagged]. - #[derive(Serialize)] - struct Id<'a> { - id: &'a ModuleId, - } - let id = serde_qs::to_string(&Id { id: &entry.id }).unwrap(); - HmrUpdateEntry { - code: entry.source_code(), - url: format!("/{}?{}", chunk_path, &id), - map: entry - .code - .has_source_map() - .then(|| format!("/__turbopack_sourcemap__/{}.map?{}", chunk_path, &id)), - } - } + Ok(update) } diff --git a/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs b/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs index 285c83e3e7e41..6fd59285a043e 100644 --- a/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs +++ b/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs @@ -5,7 +5,7 @@ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkReferenceVc, ChunkVc, ChunkableAsset, - ChunkableAssetVc, ChunkingContextVc, ChunksVc, + ChunkableAssetVc, ChunkingContext, ChunkingContextVc, }, ident::AssetIdentVc, reference::AssetReferencesVc, @@ -33,13 +33,14 @@ pub struct ChunkGroupFilesAsset { pub asset: ChunkableAssetVc, pub chunking_context: ChunkingContextVc, pub base_path: FileSystemPathVc, + pub server_root: FileSystemPathVc, pub runtime_entries: Option, } #[turbo_tasks::value_impl] impl ChunkGroupFilesAssetVc { #[turbo_tasks::function] - async fn chunks(self) -> Result { + async fn chunk_group(self) -> Result { let this = self.await?; let chunk_group = if let Some(ecma) = EcmascriptModuleAssetVc::resolve_from(this.asset).await? { @@ -49,7 +50,13 @@ impl ChunkGroupFilesAssetVc { } else { ChunkGroupVc::from_asset(this.asset, this.chunking_context) }; - Ok(chunk_group.chunks()) + Ok(chunk_group) + } + + #[turbo_tasks::function] + async fn chunk_list_path(self) -> Result { + let this = &*self.await?; + Ok(this.chunking_context.chunk_list_path(this.asset.ident())) } } @@ -118,7 +125,7 @@ impl EcmascriptChunkItem for ChunkGroupFilesChunkItem { #[turbo_tasks::function] async fn content(&self) -> Result { - let chunks = self.inner.chunks(); + let chunks = self.inner.chunk_group().chunks(); let base_path = self.inner.await?.base_path.await?; let chunks_paths = chunks .await? @@ -151,16 +158,17 @@ impl ChunkItem for ChunkGroupFilesChunkItem { #[turbo_tasks::function] async fn references(&self) -> Result { - let chunks = self.inner.chunks(); - - Ok(AssetReferencesVc::cell( - chunks - .await? - .iter() - .copied() - .map(ChunkReferenceVc::new) - .map(Into::into) - .collect(), - )) + let chunk_group = self.inner.chunk_group(); + let chunks = chunk_group.chunks(); + + let references: Vec<_> = chunks + .await? + .iter() + .copied() + .map(ChunkReferenceVc::new) + .map(Into::into) + .collect(); + + Ok(AssetReferencesVc::cell(references)) } } diff --git a/crates/turbopack-test-utils/src/snapshot.rs b/crates/turbopack-test-utils/src/snapshot.rs index c69ad42a6a493..8f20ab4b936cb 100644 --- a/crates/turbopack-test-utils/src/snapshot.rs +++ b/crates/turbopack-test-utils/src/snapshot.rs @@ -102,32 +102,33 @@ pub async fn diff(path: FileSystemPathVc, actual: AssetContentVc) -> Result<()> let path_str = &path.await?.path; let expected = path.read().into(); - let actual = match get_contents(actual, path).await? { - Some(s) => s, - None => bail!("could not generate {} contents", path_str), - }; + let actual = get_contents(actual, path).await?; let expected = get_contents(expected, path).await?; - if Some(&actual) != expected.as_ref() { - if *UPDATE { - let content = File::from(actual).into(); - path.write(content).await?; - println!("updated contents of {}", path_str); - } else { - if expected.is_none() { - eprintln!("new file {path_str} detected:"); + if actual != expected { + if let Some(actual) = actual { + if *UPDATE { + let content = File::from(actual).into(); + path.write(content).await?; + println!("updated contents of {}", path_str); } else { - eprintln!("contents of {path_str} did not match:"); + if expected.is_none() { + eprintln!("new file {path_str} detected:"); + } else { + eprintln!("contents of {path_str} did not match:"); + } + let expected = expected.unwrap_or_default(); + let diff = TextDiff::from_lines(&expected, &actual); + eprintln!( + "{}", + diff.unified_diff() + .context_radius(3) + .header("expected", "actual") + ); + bail!("contents of {path_str} did not match"); } - let expected = expected.unwrap_or_default(); - let diff = TextDiff::from_lines(&expected, &actual); - eprintln!( - "{}", - diff.unified_diff() - .context_radius(3) - .header("expected", "actual") - ); - bail!("contents of {path_str} did not match"); + } else { + bail!("{path_str} was not generated"); } } diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js index 8936f5ee92f3e..5c8cf0cff709d 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/20803_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js index 4c1a8c94a8439..4b84efd15d5e0 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$async_chunk$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js index cc76de64f9ddf..e38e1d37d0fba 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js", diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js new file mode 100644 index 0000000000000..e0b0b4edce2c7 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js @@ -0,0 +1,1469 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(({ foo })=>{ + foo(true); +}); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map new file mode 100644 index 0000000000000..f8106d6faf06c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, + {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js index 64981fc659c69..9fdda376a123b 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)"); @@ -13,15 +14,16 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(({ foo })=>{ foo(true); }); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)"); } ]); @@ -33,7 +35,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +66,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +149,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +205,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +502,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +644,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +655,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +685,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +717,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +767,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +809,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +852,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -850,17 +1113,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1244,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1282,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1367,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1438,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1446,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map index 1e02192df84b2..f8106d6faf06c 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map @@ -1,6 +1,6 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, - {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, + {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js index ef5532ab97bc7..f548771395b8c 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/39e84_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js new file mode 100644 index 0000000000000..87538d9dfddf4 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js @@ -0,0 +1,1456 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/39e84_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index.js_f5704b._.json", ["output/39e84_foo_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map new file mode 100644 index 0000000000000..366fb314a6311 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js"],"sourcesContent":["import { foo } from \"foo\";\n\nfoo(true);\n"],"names":[],"mappings":";;;AAEA,iMAAI,IAAI"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js index 394b22ddc1ff0..5c8ce976d6d6e 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -8,8 +8,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js") && loadedChunks.has("output/39e84_foo_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js") && loadedChunks.has("output/39e84_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js","output/39e84_foo_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)"); } ]); @@ -21,7 +22,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -52,6 +53,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -76,8 +136,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -129,6 +192,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -403,6 +489,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -544,7 +631,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -555,18 +642,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -583,7 +672,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -615,35 +704,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -656,10 +754,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -693,24 +796,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -733,22 +839,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -838,17 +1100,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -951,10 +1231,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -979,18 +1269,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1000,18 +1354,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1032,6 +1425,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1039,7 +1433,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js new file mode 100644 index 0000000000000..2f0b46a068428 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js @@ -0,0 +1,1456 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/d1787_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index.js_f5704b._.json", ["output/d1787_foo_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map new file mode 100644 index 0000000000000..dd1aee300d2f6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js"],"sourcesContent":["#!/usr/bin/env node\n\nimport { foo } from \"foo\";\n\nfoo(true);\n"],"names":[],"mappings":";;;AAIA,iMAAI,IAAI"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js index edd948886d2d9..859359be317e8 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -8,8 +8,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js") && loadedChunks.has("output/d1787_foo_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js") && loadedChunks.has("output/d1787_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js","output/d1787_foo_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)"); } ]); @@ -21,7 +22,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -52,6 +53,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -76,8 +136,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -129,6 +192,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -403,6 +489,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -544,7 +631,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -555,18 +642,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -583,7 +672,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -615,35 +704,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -656,10 +754,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -693,24 +796,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -733,22 +839,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -838,17 +1100,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -951,10 +1231,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -979,18 +1269,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1000,18 +1354,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1032,6 +1425,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1039,7 +1433,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js index 807d9d378e5a7..b8a18c6ca079f 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/d1787_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js new file mode 100644 index 0000000000000..caa4327b5aaa7 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js @@ -0,0 +1,1477 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +if ("TURBOPACK compile-time truthy", 1) { + console.log('DEFINED_VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('DEFINED_VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('A.VERY.LONG.DEFINED.VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('something'); +} +if ("TURBOPACK compile-time falsy", 0) { + "TURBOPACK unreachable"; +} +var p = process; +console.log(A.VERY.LONG.DEFINED.VALUE); +console.log(DEFINED_VALUE); +console.log(p.env.NODE_ENV); +if ("TURBOPACK compile-time falsy", 0) { + "TURBOPACK unreachable"; +} +p.env.NODE_ENV == 'production' ? console.log('production') : console.log('development'); +p.env.NODE_ENV != 'production' && console.log('development'); +p.env.NODE_ENV == 'production' && console.log('production'); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map new file mode 100644 index 0000000000000..a3fac3e0e9d61 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js"],"sourcesContent":["if (DEFINED_VALUE) {\n console.log('DEFINED_VALUE');\n}\n\nif (DEFINED_TRUE) {\n console.log('DEFINED_VALUE');\n}\n\nif (A.VERY.LONG.DEFINED.VALUE) {\n console.log('A.VERY.LONG.DEFINED.VALUE');\n}\n\nif (process.env.NODE_ENV) {\n console.log('something');\n}\n\nif (process.env.NODE_ENV === 'production') {\n console.log('production');\n}\n\nvar p = process;\n\n// TODO: replacement is not implemented yet\nconsole.log(A.VERY.LONG.DEFINED.VALUE);\nconsole.log(DEFINED_VALUE);\nconsole.log(p.env.NODE_ENV);\n\nif (p.env.NODE_ENV === 'production') {\n console.log('production');\n}\n\n// TODO tenary is not implemented yet\np.env.NODE_ENV == 'production' ? console.log('production') : console.log('development');\n\n// TODO short-circuit is not implemented yet\np.env.NODE_ENV != 'production' && console.log('development');\np.env.NODE_ENV == 'production' && console.log('production');\n"],"names":[],"mappings":"AAAA,wCAAmB;IACjB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAAkB;IAChB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAA+B;IAC7B,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAA0B;IACxB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED;;CAEC;AAED,IAAI,IAAI;AAGR,QAAQ,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK;AACrC,QAAQ,GAAG,CAAC;AACZ,QAAQ,GAAG,CAAC,EAAE,GAAG,CAAC,QAAQ;AAE1B;;CAEC;AAGD,EAAE,GAAG,CAAC,QAAQ,IAAI,eAAe,QAAQ,GAAG,CAAC,gBAAgB,QAAQ,GAAG,CAAC,cAAc;AAGvF,EAAE,GAAG,CAAC,QAAQ,IAAI,gBAAgB,QAAQ,GAAG,CAAC;AAC9C,EAAE,GAAG,CAAC,QAAQ,IAAI,gBAAgB,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 29, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js index f33a8213037fb..8c6e09f077940 100644 --- a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js", { -"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { if ("TURBOPACK compile-time truthy", 1) { console.log('DEFINED_VALUE'); @@ -29,8 +29,9 @@ p.env.NODE_ENV != 'production' && console.log('development'); p.env.NODE_ENV == 'production' && console.log('production'); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)"); } ]); @@ -42,7 +43,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -73,6 +74,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -97,8 +157,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -150,6 +213,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -424,6 +510,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -565,7 +652,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -576,18 +663,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -604,7 +693,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -636,35 +725,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -677,10 +775,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -714,24 +817,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -754,22 +860,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -859,17 +1121,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -972,10 +1252,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1000,18 +1290,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1021,18 +1375,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1053,6 +1446,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1060,7 +1454,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js index 87fcc749c36c9..57fabe450701b 100644 --- a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js","output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.css"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js new file mode 100644 index 0000000000000..f8e58e7d4359c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.css"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map new file mode 100644 index 0000000000000..0d68ee697ae09 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js b/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js index c0d9dea4ec77f..3ff660ac11b99 100644 --- a/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/8697f_foo_style.module.css_354da7._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__({ "foo-module-style": "foo-module-style__style__abf9e738", diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js index 76ce5d581a998..921e31bf882d5 100644 --- a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__({ "another-composed-module-style": "another-composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], @@ -10,7 +10,7 @@ __turbopack_export_value__({ }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"); @@ -23,8 +23,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js") && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js") && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_css_css_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js","output/8697f_foo_style.module.css_354da7._.js","output/8697f_foo_style.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.css","output/8697f_foo_style.module_b5a149.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.module_b5a149.css"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)"); } ]); @@ -36,7 +37,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -67,6 +68,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -91,8 +151,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -144,6 +207,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -418,6 +504,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -559,7 +646,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -570,18 +657,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -598,7 +687,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -630,35 +719,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -671,10 +769,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -708,24 +811,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -748,22 +854,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -853,17 +1115,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -966,10 +1246,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -994,18 +1284,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1015,18 +1369,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1047,6 +1440,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1054,7 +1448,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js new file mode 100644 index 0000000000000..fc083bccefb2a --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js @@ -0,0 +1,1471 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__({ + "another-composed-module-style": "another-composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], + "composed-module-style": "composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], + "inner": "inner__style__9bcf751c", + "module-style": "module-style__style__9bcf751c", +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_css_css_input_index.js_f5704b._.json", ["output/8697f_foo_style.module.css_354da7._.js","output/8697f_foo_style.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.css","output/8697f_foo_style.module_b5a149.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.module_b5a149.css"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map new file mode 100644 index 0000000000000..33116170b9c94 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"],"sourcesContent":["__turbopack_export_value__({\n \"another-composed-module-style\": \"another-composed-module-style__style__9bcf751c\" + \" \" + __turbopack_import__(\"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)\")[\"foo-module-style\"],\n \"composed-module-style\": \"composed-module-style__style__9bcf751c\" + \" \" + __turbopack_import__(\"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)\")[\"foo-module-style\"],\n \"inner\": \"inner__style__9bcf751c\",\n \"module-style\": \"module-style__style__9bcf751c\",\n});\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/css/css/input/index.js"],"sourcesContent":["import \"foo/style.css\";\nimport \"foo\";\nimport \"./style.css\";\nimport fooStyle from \"foo/style.module.css\";\nimport style from \"./style.module.css\";\n\nconsole.log(style, fooStyle);\n"],"names":[],"mappings":";;;;;;;;AAMA,QAAQ,GAAG"}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js index bef20d3f18344..8cdc40a1e8274 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_react_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/react/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js index 25e470b2c2fca..89fd0a2915b77 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_react_jsx-dev-runtime.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/react/jsx-dev-runtime.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js index c2ec3e7d7308f..f3cef3d4abc40 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_styled_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/styled/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js new file mode 100644 index 0000000000000..de4295390dbd8 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js @@ -0,0 +1,1476 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$styled$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +const StyledButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$styled$2f$index$2e$js__$28$ecmascript$29__["default"]("button", { + target: "e1r1p2t30", + label: "StyledButton" +})("background:blue;", "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKiBAanN4SW1wb3J0U291cmNlIEBlbW90aW9uL3JlYWN0ICovXG5cbmltcG9ydCB7IGpzeCB9IGZyb20gXCJAZW1vdGlvbi9yZWFjdFwiO1xuaW1wb3J0IHN0eWxlZCBmcm9tIFwiQGVtb3Rpb24vc3R5bGVkXCI7XG5cbmNvbnN0IFN0eWxlZEJ1dHRvbiA9IHN0eWxlZC5idXR0b25gXG4gIGJhY2tncm91bmQ6IGJsdWU7XG5gO1xuXG5mdW5jdGlvbiBDbGFzc05hbWVCdXR0b24oeyBjaGlsZHJlbiB9KSB7XG4gIHJldHVybiAoXG4gICAgPGJ1dHRvblxuICAgICAgY2xhc3NOYW1lPXtjc3NgXG4gICAgICAgIGJhY2tncm91bmQ6IGJsdWU7XG4gICAgICBgfVxuICAgID5cbiAgICAgIHtjaGlsZHJlbn1cbiAgICA8L2J1dHRvbj5cbiAgKTtcbn1cblxuY29uc29sZS5sb2coU3R5bGVkQnV0dG9uLCBDbGFzc05hbWVCdXR0b24pO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUtxQiJ9 */"); +function ClassNameButton({ children }) { + return __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__["jsxDEV"]("button", { + className: css` + background: blue; + `, + children: children + }, void 0, false, { + fileName: "", + lineNumber: 12, + columnNumber: 5 + }, this); +} +console.log(StyledButton, ClassNameButton); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index.js_f5704b._.json", ["output/63a02_@emotion_react_jsx-dev-runtime.js","output/63a02_@emotion_react_index.js","output/63a02_@emotion_styled_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map new file mode 100644 index 0000000000000..a098fa9af6b76 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js"],"sourcesContent":["/** @jsxImportSource @emotion/react */\n\nimport { jsx } from \"@emotion/react\";\nimport styled from \"@emotion/styled\";\n\nconst StyledButton = styled.button`\n background: blue;\n`;\n\nfunction ClassNameButton({ children }) {\n return (\n \n {children}\n \n );\n}\n\nconsole.log(StyledButton, ClassNameButton);\n"],"names":[],"mappings":";;;;;;;AAKA,MAAM;;;;AAIN,SAAS,gBAAgB,EAAE,SAAQ,EAAE,EAAE;IACrC,OACE,0LAAC;QACC,WAAW,GAAG,CAAC;;MAEf,CAAC;kBAEA;;;;;;AAGP;AAEA,QAAQ,GAAG,CAAC,cAAc"}}, + {"offset": {"line": 28, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js index 0e0b94d8b0b40..e14fe60721bea 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js", { -"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)"); @@ -28,8 +28,9 @@ function ClassNameButton({ children }) { console.log(StyledButton, ClassNameButton); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js") && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js") && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index.js_f5704b._.json", ["output/63a02_@emotion_react_jsx-dev-runtime.js","output/63a02_@emotion_react_index.js","output/63a02_@emotion_styled_index.js","output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)"); } ]); @@ -41,7 +42,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -72,6 +73,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -96,8 +156,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -149,6 +212,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -423,6 +509,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -564,7 +651,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -575,18 +662,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -603,7 +692,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -635,35 +724,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -676,10 +774,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -713,24 +816,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -753,22 +859,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -858,17 +1120,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -971,10 +1251,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -999,18 +1289,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1020,18 +1374,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1052,6 +1445,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1059,7 +1453,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js index 0e5f3d4594542..12a7d91bc63cb 100644 --- a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { const env = process.env = {...process.env}; @@ -8,15 +8,16 @@ env["FOO"] = foo; env["FOOBAR"] = foobar; })()), -"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log(process.env.FOOBAR); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_env_env_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js"); -instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); } ]); (() => { @@ -27,7 +28,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -58,6 +59,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -82,8 +142,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -135,6 +198,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -409,6 +495,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -550,7 +637,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -561,18 +648,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -589,7 +678,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -621,35 +710,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -662,10 +760,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -699,24 +802,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -739,22 +845,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -844,17 +1106,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -957,10 +1237,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -985,18 +1275,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1006,18 +1360,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1038,6 +1431,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1045,7 +1439,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js new file mode 100644 index 0000000000000..705d5236fb2de --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js @@ -0,0 +1,1462 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +const env = process.env = {...process.env}; + +env["FOO"] = foo; +env["FOOBAR"] = foobar; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log(process.env.FOOBAR); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_env_env_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js"); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map new file mode 100644 index 0000000000000..ecb3f5cd6c158 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/env/env/input/index.js"],"sourcesContent":["console.log(process.env.FOOBAR);\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC,QAAQ,GAAG,CAAC,MAAM"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js index 97c189e2dd069..f164e68f00b3a 100644 --- a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js", { -"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log("hello world"); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index.js_f5704b._.json", ["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js new file mode 100644 index 0000000000000..5d08d899032d8 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log("hello world"); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map new file mode 100644 index 0000000000000..2cc43c7bce8c6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js"],"sourcesContent":["console.log(\"hello world\");\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js new file mode 100644 index 0000000000000..f455e8348de0c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log("hello world"); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_example_example_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map new file mode 100644 index 0000000000000..0a1d13ff0cbc9 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/example/example/input/index.js"],"sourcesContent":["console.log(\"hello world\");\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js index 3143934f40067..24df4ed2ffeae 100644 --- a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js", { -"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log("hello world"); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_example_example_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js new file mode 100644 index 0000000000000..7f2e473ad04b4 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js @@ -0,0 +1,1479 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +exports.hello = "World"; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map new file mode 100644 index 0000000000000..424ae172c321a --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map @@ -0,0 +1,12 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js"],"sourcesContent":["// commonjs.js\nexports.hello = \"World\";\n\n"],"names":[],"mappings":"AACA,QAAQ,KAAK,GAAG"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 27, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js"],"sourcesContent":["// a.js\nimport * as B from \"./b\";\nconsole.log(B);"],"names":[],"mappings":";;;AAEA,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js index 10759444a5734..80ebccc733b25 100644 --- a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js @@ -1,11 +1,11 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js", { -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { exports.hello = "World"; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)"); @@ -14,7 +14,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb ; })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)"); @@ -23,7 +23,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb ; })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -31,8 +31,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)"); } ]); @@ -44,7 +45,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -75,6 +76,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -99,8 +159,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -152,6 +215,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -426,6 +512,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -567,7 +654,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -578,18 +665,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -606,7 +695,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -638,35 +727,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -679,10 +777,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -716,24 +819,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -756,22 +862,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -861,17 +1123,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -974,10 +1254,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1002,18 +1292,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1023,18 +1377,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1055,6 +1448,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1062,7 +1456,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js index fe91a986eabcf..c6a9c61c81555 100644 --- a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js", { -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { module.exports = { foo: 1, @@ -8,7 +8,7 @@ module.exports = { }; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)"); @@ -18,7 +18,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb console.log('Hoist test'); })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -26,8 +26,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)"); } ]); @@ -39,7 +40,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -70,6 +71,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -94,8 +154,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -147,6 +210,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -421,6 +507,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -562,7 +649,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -573,18 +660,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -601,7 +690,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -633,35 +722,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -674,10 +772,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -711,24 +814,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -751,22 +857,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -856,17 +1118,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -969,10 +1249,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -997,18 +1287,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1018,18 +1372,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1050,6 +1443,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1057,7 +1451,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js new file mode 100644 index 0000000000000..427ad1af3099c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js @@ -0,0 +1,1474 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +module.exports = { + foo: 1, + bar: 2 +}; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log('Hoist test'); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map new file mode 100644 index 0000000000000..9153a98a4b25d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs"],"sourcesContent":["module.exports = { foo: 1, bar: 2 }"],"names":[],"mappings":"AAAA,OAAO,OAAO,GAAG;IAAE,KAAK;IAAG,KAAK;AAAE"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js"],"sourcesContent":["\nexport * from './exported.cjs'\n\nconsole.log('Hoist test')"],"names":[],"mappings":";;;;;AAGA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js"],"sourcesContent":["import * as foo from './mod.js';\n\nconsole.log(foo)"],"names":[],"mappings":";;;AAEA,QAAQ,GAAG"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js index 6773850011b4e..feb312b7a39ea 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__.url); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$cjs$2f$input$2f$mod$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js new file mode 100644 index 0000000000000..4b26fe5e75923 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__.url); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$cjs$2f$input$2f$mod$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map new file mode 100644 index 0000000000000..7a578bd5ccc12 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs"],"sourcesContent":["console.log(import.meta.url);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG,CAAC,8BAAY,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js index 2f214f72134ed..3fa1e091e0624 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs" @@ -16,15 +16,16 @@ foo(); bar(); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$multiple$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)"); } ]); @@ -36,7 +37,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -67,6 +68,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -91,8 +151,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -144,6 +207,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -418,6 +504,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -559,7 +646,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -570,18 +657,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -598,7 +687,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -630,35 +719,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -671,10 +769,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -708,24 +811,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -748,22 +854,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -853,17 +1115,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -966,10 +1246,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -994,18 +1284,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1015,18 +1369,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1047,6 +1440,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1054,7 +1448,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js new file mode 100644 index 0000000000000..66c8598f008a0 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js @@ -0,0 +1,1471 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +function foo() { + console.log(__TURBOPACK__import$2e$meta__.url); +} +function bar() { + console.log(__TURBOPACK__import$2e$meta__.url); +} +foo(); +bar(); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$multiple$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map new file mode 100644 index 0000000000000..9716b8f531c0e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs"],"sourcesContent":["function foo() {\n console.log(import.meta.url);\n}\nfunction bar() {\n console.log(import.meta.url);\n}\n\nfoo();\nbar();\n"],"names":[],"mappings":";;;;AAAA,SAAS,MAAM;IACb,QAAQ,GAAG,CAAC,8BAAY,GAAG;AAC7B;AACA,SAAS,MAAM;IACb,QAAQ,GAAG,CAAC,8BAAY,GAAG;AAC7B;AAEA;AACA"}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js new file mode 100644 index 0000000000000..36b2b517b8d5d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +__TURBOPACK__import$2e$meta__.foo = 1; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$mutable$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map new file mode 100644 index 0000000000000..e954bf2efdd13 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs"],"sourcesContent":["import.meta.foo = 1;\n"],"names":[],"mappings":";;;;AAAA,8BAAY,GAAG,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js index cbe976b62c871..a7ba9eda1bf26 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { __TURBOPACK__import$2e$meta__.foo = 1; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$mutable$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js index 96518a6b9a4b1..2edbd0f88b040 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$object$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js new file mode 100644 index 0000000000000..efd0eaed44f06 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$object$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map new file mode 100644 index 0000000000000..87f763dce3d88 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs"],"sourcesContent":["console.log(import.meta);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js index ddc4030a65b39..eccf4fc573d09 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__.url); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js new file mode 100644 index 0000000000000..e1cbbc1ecc5de --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__.url); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map new file mode 100644 index 0000000000000..000fb952f2abc --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs"],"sourcesContent":["console.log(import.meta.url);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG,CAAC,8BAAY,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js new file mode 100644 index 0000000000000..1ef6e29879a30 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js @@ -0,0 +1,1470 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/import-meta/url/static/05254cf29a922ae2.txt"); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +const assetUrl = new URL(__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)"), location.origin); +console.log(assetUrl); +fetch(assetUrl).then((res)=>res.text()).then(console.log); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$url$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map new file mode 100644 index 0000000000000..1628a96f8f796 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs"],"sourcesContent":["const assetUrl = new URL('./asset.txt', import.meta.url);\n\nconsole.log(assetUrl);\nfetch(assetUrl)\n .then(res => res.text())\n .then(console.log);\n"],"names":[],"mappings":";;;;AAAA,MAAM,WAAW,IAAI;AAErB,QAAQ,GAAG,CAAC;AACZ,MAAM,UACH,IAAI,CAAC,CAAA,MAAO,IAAI,IAAI,IACpB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 15, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js index 4eda5243bff72..17a3d628c9ef7 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js @@ -1,10 +1,10 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/import-meta/url/static/05254cf29a922ae2.txt"); })()), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs" @@ -15,15 +15,16 @@ console.log(assetUrl); fetch(assetUrl).then((res)=>res.text()).then(console.log); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$url$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)"); } ]); @@ -35,7 +36,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -66,6 +67,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -90,8 +150,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -143,6 +206,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -417,6 +503,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -558,7 +645,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -569,18 +656,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -597,7 +686,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -629,35 +718,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -670,10 +768,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -707,24 +810,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -747,22 +853,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -852,17 +1114,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -965,10 +1245,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -993,18 +1283,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1014,18 +1368,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1046,6 +1439,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1053,7 +1447,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js new file mode 100644 index 0000000000000..64bd0e4b685ea --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js @@ -0,0 +1,1467 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map new file mode 100644 index 0000000000000..4de4d630c6205 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js index 55779f90c37ba..6780519082913 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)"); @@ -13,13 +14,14 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)"); } ]); @@ -31,7 +33,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -62,6 +64,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -86,8 +147,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -139,6 +203,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -413,6 +500,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -554,7 +642,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -565,18 +653,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -593,7 +683,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -625,35 +715,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -666,10 +765,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -703,24 +807,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -743,22 +850,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -848,17 +1111,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -961,10 +1242,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -989,18 +1280,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1010,18 +1365,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1042,6 +1436,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1049,7 +1444,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map index 7e424ee4e5d5f..4de4d630c6205 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map @@ -1,6 +1,6 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, - {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js index 1357b30c228ca..50ffebf121736 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js index 2de3cd40902ce..3d07e709d38cc 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js" diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js new file mode 100644 index 0000000000000..9854d9a96d405 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)": (() => {{ + +throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n 1 | {\n 2 | \"nested\": {\n | v\n 3 + \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n"); + +}}), +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__(JSON.parse("{\"name\":\"json-snapshot\"}")); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__["default"].name); +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__["default"]["this-is"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map new file mode 100644 index 0000000000000..483ffc655fc99 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js"],"sourcesContent":["import pkg from \"./package.json\";\nconsole.log(pkg.name);\nimport invalid from \"./invalid.json\";\nconsole.log(invalid[\"this-is\"]);\n"],"names":[],"mappings":";;;;AACA,QAAQ,GAAG,CAAC,2KAAI,IAAI;;AAEpB,QAAQ,GAAG,CAAC,0KAAO,CAAC,UAAU"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js index 94f923bcc7209..5a953d3117a30 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js @@ -5,11 +5,11 @@ throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n 1 | {\n 2 | \"nested\": {\n | v\n 3 + \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n"); }}), -"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__(JSON.parse("{\"name\":\"json-snapshot\"}")); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)"); @@ -20,8 +20,9 @@ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__["default"]["this-is"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js new file mode 100644 index 0000000000000..998ffda23838d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js @@ -0,0 +1,1458 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const dne = __turbopack_require__((()=>{ + const e = new Error("Cannot find module 'does-not-exist/path'"); + e.code = 'MODULE_NOT_FOUND'; + throw e; +})()); +console.log(dne); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map new file mode 100644 index 0000000000000..0d6ececf1247c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js"],"sourcesContent":["const dne = require(\"does-not-exist/path\");\n\nconsole.log(dne);\n"],"names":[],"mappings":"AAAA,MAAM,MAAM;;;;;AAEZ,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js index 3da4a929bc237..751eb513a337a 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const dne = __turbopack_require__((()=>{ const e = new Error("Cannot find module 'does-not-exist/path'"); @@ -10,8 +10,9 @@ const dne = __turbopack_require__((()=>{ console.log(dne); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)"); } ]); @@ -23,7 +24,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -54,6 +55,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -78,8 +138,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -131,6 +194,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -405,6 +491,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -546,7 +633,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -557,18 +644,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -585,7 +674,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -617,35 +706,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -658,10 +756,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -695,24 +798,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -735,22 +841,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -840,17 +1102,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -953,10 +1233,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -981,18 +1271,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1002,18 +1356,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1034,6 +1427,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1041,7 +1435,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js new file mode 100644 index 0000000000000..fbd972f8eaf33 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js @@ -0,0 +1,1461 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +(()=>{ + const e = new Error("Cannot find module 'does-not-exist/path'"); + e.code = 'MODULE_NOT_FOUND'; + throw e; +})(); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(dne); +console.log({}[dne]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map new file mode 100644 index 0000000000000..d13204ccb8a96 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js"],"sourcesContent":["import dne from \"does-not-exist/path\";\n\nconsole.log(dne);\nconsole.log({}[dne]);\n"],"names":[],"mappings":";;;;;;;AAEA,QAAQ,GAAG,CAAC;AACZ,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js index 7b73d92eccbb9..0bd9d2b984d05 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { (()=>{ const e = new Error("Cannot find module 'does-not-exist/path'"); @@ -13,8 +13,9 @@ console.log(dne); console.log({}[dne]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)"); } ]); @@ -26,7 +27,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -57,6 +58,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -81,8 +141,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -134,6 +197,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -408,6 +494,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -549,7 +636,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -560,18 +647,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -588,7 +677,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -620,35 +709,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -661,10 +759,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -698,24 +801,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -738,22 +844,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -843,17 +1105,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -956,10 +1236,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -984,18 +1274,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1005,18 +1359,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1037,6 +1430,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1044,7 +1438,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js new file mode 100644 index 0000000000000..50974ed6615cc --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js @@ -0,0 +1,1479 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "default": ()=>__TURBOPACK__default__export__ +}); +const __TURBOPACK__default__export__ = "turbopack"; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__["default"]); +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map new file mode 100644 index 0000000000000..db5d1dd46e145 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js index efbdcff5c977f..d535883079762 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); @@ -13,7 +14,7 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ @@ -21,7 +22,7 @@ __turbopack_esm__({ const __TURBOPACK__default__export__ = "turbopack"; })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -30,8 +31,9 @@ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$ __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)"); } ]); @@ -43,7 +45,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -74,6 +76,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -98,8 +159,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -151,6 +215,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -425,6 +512,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -566,7 +654,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -577,18 +665,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -605,7 +695,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -637,35 +727,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -678,10 +777,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -715,24 +819,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -755,22 +862,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -860,17 +1123,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -973,10 +1254,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1001,18 +1292,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1022,18 +1377,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1054,6 +1448,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1061,7 +1456,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map index 275483d690c9e..db5d1dd46e145 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map @@ -1,8 +1,8 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, - {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, - {"offset": {"line": 25, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, - {"offset": {"line": 30, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js index ce81794e7177f..7fff51cea0f28 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js index c9f4caba61a8c..b78eb7686bf45 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js" diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js index 31d2abcff9f4c..e04824a94c61e 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js @@ -1,10 +1,10 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/imports/static/static/957b9b162f8447f9.svg"); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -12,8 +12,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)"); } ]); @@ -25,7 +26,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -56,6 +57,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -80,8 +140,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -133,6 +196,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -407,6 +493,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -548,7 +635,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -559,18 +646,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -587,7 +676,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -619,35 +708,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -660,10 +758,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -697,24 +800,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -737,22 +843,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -842,17 +1104,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -955,10 +1235,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -983,18 +1273,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1004,18 +1358,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1036,6 +1429,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1043,7 +1437,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js new file mode 100644 index 0000000000000..302f4069e0977 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js @@ -0,0 +1,1460 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/imports/static/static/957b9b162f8447f9.svg"); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map new file mode 100644 index 0000000000000..9f2ca3f475e0d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js"],"sourcesContent":["import img from \"./vercel.svg\";\nconsole.log(img);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js index 34594288aa663..d45d4e066208d 100644 --- a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js @@ -1,14 +1,15 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js", { -"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__external__node$3a$fs__ = __turbopack_external_require__("node:fs", true); "__TURBOPACK__ecmascript__hoisting__location__"; ; })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_node_node_protocol_external_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)"); } ]); @@ -20,7 +21,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -51,6 +52,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -75,8 +135,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -128,6 +191,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -402,6 +488,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -543,7 +630,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -554,18 +641,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -582,7 +671,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -614,35 +703,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -655,10 +753,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -692,24 +795,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -732,22 +838,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -837,17 +1099,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -950,10 +1230,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -978,18 +1268,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -999,18 +1353,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1031,6 +1424,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1038,7 +1432,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js new file mode 100644 index 0000000000000..e9dc42858f45e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js @@ -0,0 +1,1455 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__external__node$3a$fs__ = __turbopack_external_require__("node:fs", true); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_node_node_protocol_external_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map new file mode 100644 index 0000000000000..e998c77728e14 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 7, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js index b21d071e4ccb4..8dccf61e5f92e 100644 --- a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_styled-components_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "styled-components/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js new file mode 100644 index 0000000000000..21dac7303a59e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js @@ -0,0 +1,1462 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +const MyButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__["default"].button.withConfig({ + displayName: "MyButton", + componentId: "sc-86737cfc-0" +})` + background: blue; +`; +console.log(MyButton); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_styled-components_index.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_styled_components_styled_components_input_index.js_f5704b._.json", ["output/63a02_styled-components_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map new file mode 100644 index 0000000000000..d8b322fd809ba --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js"],"sourcesContent":["import styled from \"styled-components\";\n\nconst MyButton = styled.button`\n background: blue;\n`;\n\nconsole.log(MyButton);\n"],"names":[],"mappings":";;;AAEA,MAAM,WAAW,6KAAO,MAAM;;;EAAA,CAAC;;AAE/B,CAAC;AAED,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js index f1a0d13516439..5885493148e28 100644 --- a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js", { -"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -14,8 +14,9 @@ const MyButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbo console.log(MyButton); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_styled-components_index.js") && loadedChunks.has("output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_styled-components_index.js") && loadedChunks.has("output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_styled_components_styled_components_input_index.js_f5704b._.json", ["output/63a02_styled-components_index.js","output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)"); } ]); @@ -27,7 +28,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -58,6 +59,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -82,8 +142,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -135,6 +198,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -409,6 +495,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -550,7 +637,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -561,18 +648,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -589,7 +678,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -621,35 +710,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -662,10 +760,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -699,24 +802,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -739,22 +845,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -844,17 +1106,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -957,10 +1237,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -985,18 +1275,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1006,18 +1360,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1038,6 +1431,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1045,7 +1439,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js index 0612b8265bb49..a213570a77fd4 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_react_jsx-dev-runtime.js", { -"[project]/crates/turbopack-tests/tests/node_modules/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "react/jsx-dev-runtime.js"; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js index b4e78f94dd03b..3d00664f6ee27 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/7b7bf_third_party_component_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>ThirdPartyComponent diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js index 8839a8077796c..7a6da67f32bab 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)"); @@ -10,8 +10,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; + registerChunkList("output/8562f_snapshot_swc_transforms_mono_transforms_input_packages_app_index.js_f5704b._.json", ["output/63a02_react_jsx-dev-runtime.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js","output/7b7bf_third_party_component_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)"); } ]); @@ -23,7 +24,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -54,6 +55,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -78,8 +138,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -131,6 +194,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -405,6 +491,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -546,7 +633,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -557,18 +644,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -585,7 +674,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -617,35 +706,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -658,10 +756,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -695,24 +798,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -735,22 +841,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -840,17 +1102,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -953,10 +1233,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -981,18 +1271,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1002,18 +1356,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1034,6 +1427,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1041,7 +1435,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js new file mode 100644 index 0000000000000..814ac88c0b810 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js @@ -0,0 +1,1458 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; + registerChunkList("output/8562f_snapshot_swc_transforms_mono_transforms_input_packages_app_index.js_f5704b._.json", ["output/63a02_react_jsx-dev-runtime.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js","output/7b7bf_third_party_component_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map new file mode 100644 index 0000000000000..b2a1e5637adfd --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js"],"sourcesContent":["import MyApp from \"component\";\nimport ThirdPartyComponent from \"third_party_component\";\n\nconsole.log(MyApp, ThirdPartyComponent);\n"],"names":[],"mappings":";;;;;AAGA,QAAQ,GAAG"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js index a574bc27f94bc..38d322db6db67 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>MyApp diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js index c90a8b0be8c88..fcaab5e5dae5a 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@swc_helpers_src__class_call_check.mjs._.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@swc/helpers/src/_class_call_check.mjs"; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js index 84b577e0fa121..f115ae0243411 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -13,8 +13,9 @@ var Foo = function Foo() { console.log(Foo, [].includes("foo")); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js") && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js") && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index.js_f5704b._.json", ["output/63a02_@swc_helpers_src__class_call_check.mjs._.js","output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)"); } ]); @@ -26,7 +27,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -57,6 +58,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -81,8 +141,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -134,6 +197,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -408,6 +494,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -549,7 +636,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -560,18 +647,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -588,7 +677,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -620,35 +709,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -661,10 +759,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -698,24 +801,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -738,22 +844,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -843,17 +1105,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -956,10 +1236,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -984,18 +1274,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1005,18 +1359,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1037,6 +1430,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1044,7 +1438,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js new file mode 100644 index 0000000000000..6a3d444ee3e44 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js @@ -0,0 +1,1461 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +var Foo = function Foo() { + "use strict"; + __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__["default"](this, Foo); +}; +console.log(Foo, [].includes("foo")); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index.js_f5704b._.json", ["output/63a02_@swc_helpers_src__class_call_check.mjs._.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map new file mode 100644 index 0000000000000..1e6e076dfc14f --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js"],"sourcesContent":["class Foo {}\n\nconsole.log(Foo, [].includes(\"foo\"));\n"],"names":[],"mappings":";;;;AAAA,IAAA,AAAM,MAAN,SAAM;;uMAAA;;AAEN,QAAQ,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js new file mode 100644 index 0000000000000..179cdd95badb6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "prop": ()=>prop +}); +const prop = 1; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_jsconfig-baseurl_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map new file mode 100644 index 0000000000000..76729a5945d3e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js"],"sourcesContent":["export const prop = 1;\n"],"names":[],"mappings":";;;AAAO,MAAM,OAAO"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js"],"sourcesContent":["import { prop as globalFoo } from \"foo\";\nimport { prop as localFoo } from \"./foo\";\nimport { prop as atFoo } from \"@/foo\";\n\nconsole.log(globalFoo, localFoo, atFoo);\n"],"names":[],"mappings":";;;;;;;AAIA,QAAQ,GAAG"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js index 40ea89936a71e..41f37160094ff 100644 --- a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js", { -"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "prop": ()=>prop @@ -8,7 +8,7 @@ __turbopack_esm__({ const prop = 1; })()), -"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); @@ -20,8 +20,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_jsconfig-baseurl_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js new file mode 100644 index 0000000000000..656451ea1381c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "prop": ()=>prop +}); +const prop = 1; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[id]; + if (typeof moduleFactory !== "function") { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + if (module && hotState.selfAccepted && !hotState.selfInvalidated) { + outdatedSelfAcceptedModules.push({ + moduleId, + errorHandler: hotState.selfAccepted, + }); + } + } + return outdatedSelfAcceptedModules; +} + +/** + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + + const hotState = moduleHotState.get(module); + const data = {}; + + // Run the `hot.dispose` handler, if any, passing in the persistent + // `hot.data` object. + for (const disposeHandler of hotState.disposeHandlers) { + disposeHandler(data); + } + + // This used to warn in `getOrInstantiateModuleFromParent` when a disposed + // module is still importing other modules. + module.hot.active = false; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[moduleId] = factory; + } + + // TODO(alexkirsz) Run new runtime entries here. + + // TODO(alexkirsz) Dependencies: call accept handlers for outdated deps. + + // Re-instantiate all outdated self-accepted modules. + for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) { + try { + instantiateModule(moduleId, SourceType.Update); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + outdatedModules.add(moduleId); + + // We've arrived at the runtime of the chunk, which means that nothing + // else above can accept this update. + if (moduleId === undefined) { + return { + type: "unaccepted", + dependencyChain, + }; + } + + const module = moduleCache[moduleId]; + const hotState = moduleHotState.get(module); + + if ( + // The module is not in the cache. Since this is a "modified" update, + // it means that the module was never instantiated before. + !module || // The module accepted itself without invalidating globalThis. + // TODO is that right? + (hotState.selfAccepted && !hotState.selfInvalidated) + ) { + continue; + } + + if (hotState.selfDeclined) { + return { + type: "self-declined", + dependencyChain, + moduleId, + }; + } + + if (runtimeModules.has(moduleId)) { + queue.push({ + moduleId: undefined, + dependencyChain: [...dependencyChain, moduleId], + }); + continue; + } + + for (const parentId of module.parents) { + const parent = moduleCache[parentId]; + + if (!parent) { + // TODO(alexkirsz) Is this even possible? + continue; + } + + // TODO(alexkirsz) Dependencies: check accepted and declined + // dependencies here. + + queue.push({ + moduleId: parentId, + dependencyChain: [...dependencyChain, moduleId], + }); + } + } + + return { + type: "accepted", + moduleId, + outdatedModules, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const hot = { + // TODO(alexkirsz) This is not defined in the HMR API. It was used to + // decide whether to warn whenever an HMR-disposed module required other + // modules. We might want to remove it. + active: true, + + data: hotData ?? {}, + + accept: accept, + + decline: (dep) => { + if (dep === undefined) { + hotState.selfDeclined = true; + } else { + throw new Error("unsupported `decline` signature"); + } + }, + + dispose: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + addDisposeHandler: (callback) => { + hotState.disposeHandlers.push(callback); + }, + + removeDisposeHandler: (callback) => { + const idx = hotState.disposeHandlers.indexOf(callback); + if (idx >= 0) { + hotState.disposeHandlers.splice(idx, 1); + } + }, + + invalidate: () => { + hotState.selfInvalidated = true; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // NOTE(alexkirsz) This is part of the management API, which we don't + // implement, but the Next.js React Refresh runtime uses this to decide + // whether to schedule an update. + status: () => "idle", + + // NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops. + addStatusHandler: (_handler) => {}, + removeStatusHandler: (_handler) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map new file mode 100644 index 0000000000000..e0343cefa513e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts"],"sourcesContent":["export const prop = 1;\n"],"names":[],"mappings":";;;AAAO,MAAM,OAAO"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts"],"sourcesContent":["import { prop as globalFoo } from \"foo\";\nimport { prop as localFoo } from \"./foo\";\nimport { prop as atFoo } from \"@/foo\";\n\nconsole.log(globalFoo, localFoo, atFoo);\n"],"names":[],"mappings":";;;;;;;AAIA,QAAQ,GAAG"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js index 00fc62e2e7bd4..fdd957f8758c5 100644 --- a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "prop": ()=>prop @@ -8,7 +8,7 @@ __turbopack_esm__({ const prop = 1; })()), -"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); @@ -20,8 +20,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_f5704b._.json", ["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * Adds, deletes, and moves modules between chunks. This must happen before the + * dispose phase as it needs to know which modules were removed from all chunks, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + switch (mergedChunkUpdate.type) { + case "added": { + const updateAdded = new Set(mergedChunkUpdate.modules); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + chunksAdded.set(chunkPath, updateAdded); + break; + } + case "deleted": { + // We could also use `mergedChunkUpdate.modules` here. + const updateDeleted = new Set(chunkModulesMap.get(chunkPath)); + for (const moduleId of updateDeleted) { + deleted.add(moduleId); + } + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + case "partial": { + const updateAdded = new Set(mergedChunkUpdate.added); + const updateDeleted = new Set(mergedChunkUpdate.deleted); + for (const moduleId of updateAdded) { + added.set(moduleId, entries[moduleId]); + } + for (const moduleId of updateDeleted) { + deleted.add([moduleId, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } + + // If a module was added from one chunk and deleted from another in the same update, + // consider it to be modified, as it means the module was moved from one chunk to another + // AND has new code in a single update. + for (const moduleId of added.keys()) { + if (deleted.has(moduleId)) { + added.delete(moduleId); + deleted.delete(moduleId); + } + } + + for (const [moduleId, entry] of Object.entries(entries)) { + // Modules that haven't been added to any chunk but have new code are considered + // to be modified. + // This needs to be under the previous loop, as we need it to get rid of modules + // that were added and deleted in the same update. + if (!added.has(moduleId)) { + modified.set(moduleId, entry); + } + } + + return { added, deleted, modified, chunksAdded, chunksDeleted }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // or the page itself was deleted. + // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. + // If it is a runtime chunk list, we restart the application. + if (runtimeChunkLists.has(chunkListPath)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory;