diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b6edad359db..1ecb48c9ffd 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -42,6 +42,10 @@ jobs:
working-directory: ./update
run: deno publish
+ - name: Publish @fresh/plugin-vite
+ working-directory: ./plugin-vite
+ run: deno publish
+
- name: Publish @fresh/examples
working-directory: ./examples
run: deno publish
diff --git a/.gitignore b/.gitignore
index d76c6907b89..8ccb285c9fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
_fresh/
+.vite/
vendor/
node_modules/
.docs/
diff --git a/deno.json b/deno.json
index 06bcb755557..6ff43ab6a87 100644
--- a/deno.json
+++ b/deno.json
@@ -5,6 +5,7 @@
"./init",
"./plugin-tailwindcss",
"./plugin-tailwindcss-v3",
+ "./plugin-vite",
"./update",
"./www"
],
@@ -14,9 +15,12 @@
"exports": {
".": "./src/mod.ts",
"./runtime": "./src/runtime/shared.ts",
+ "./runtime-client": "./src/runtime/client/mod.tsx",
"./dev": "./src/dev/mod.ts",
"./compat": "./src/compat.ts",
- "./do-not-use": "./src/internals.ts"
+ "./internal": "./src/internals.ts",
+ "./internal-dev": "./src/internals_dev.ts",
+ "./build-id": "./src/runtime/build_id.ts"
},
"tasks": {
"test": "deno test -A --parallel",
@@ -30,7 +34,14 @@
"test:www": "deno test -A www/main_test.*",
"release": "deno run -A tools/release.ts"
},
- "exclude": ["**/_fresh/*", "**/tmp/*", "*/tests_OLD/**"],
+ "exclude": [
+ "**/_fresh/*",
+ "**/tmp/*",
+ "*/tests_OLD/**",
+ "**/vite.config.ts.*",
+ "*/vite.config.ts.*",
+ "vite.config.ts.*"
+ ],
"publish": {
"include": [
"src/**",
diff --git a/deno.lock b/deno.lock
index 0fb4f5b095c..c846fe40dc7 100644
--- a/deno.lock
+++ b/deno.lock
@@ -9,6 +9,7 @@
"jsr:@deno/esbuild-plugin@^1.1.5": "1.1.5",
"jsr:@deno/graph@0.86": "0.86.9",
"jsr:@deno/graph@~0.82.3": "0.82.3",
+ "jsr:@deno/loader@~0.3.2": "0.3.3",
"jsr:@deno/loader@~0.3.3": "0.3.3",
"jsr:@fresh/core@^2.0.0-alpha.37": "2.0.0-alpha.53",
"jsr:@marvinh-test/fresh-island@^0.0.1": "0.0.1",
@@ -63,8 +64,10 @@
"npm:@opentelemetry/api@^1.9.0": "1.9.0",
"npm:@preact/signals@^1.2.3": "1.3.2_preact@10.27.0",
"npm:@preact/signals@^2.2.1": "2.2.1_preact@10.27.0",
+ "npm:@prefresh/vite@^2.4.8": "2.4.8_preact@10.27.0_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15",
"npm:@tailwindcss/postcss@^4.1.10": "4.1.11",
"npm:@types/node@*": "22.15.15",
+ "npm:@types/node@^24.1.0": "24.1.0",
"npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6",
"npm:cssnano@^6.1.2": "6.1.2_postcss@8.5.6",
"npm:esbuild-wasm@0.25.7": "0.25.7",
@@ -80,11 +83,14 @@
"npm:postcss@^8.5.6": "8.5.6",
"npm:preact-render-to-string@^6.5.11": "6.5.13_preact@10.27.0",
"npm:preact@^10.22.0": "10.27.0",
+ "npm:preact@^10.26.9": "10.27.0",
"npm:preact@^10.27.0": "10.27.0",
"npm:prismjs@^1.29.0": "1.30.0",
"npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.5.6",
"npm:tailwindcss@^4.1.10": "4.1.11",
- "npm:ts-morph@26": "26.0.0"
+ "npm:ts-morph@26": "26.0.0",
+ "npm:vite-plugin-inspect@^11.3.2": "11.3.2_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15",
+ "npm:vite@^7.0.6": "7.0.6_@types+node@22.15.15_picomatch@4.0.3"
},
"jsr": {
"@astral/astral@0.5.3": {
@@ -124,7 +130,7 @@
"@deno/esbuild-plugin@1.1.5": {
"integrity": "c9cde95990b97802a0da6c73c26ab4a48f30d286818845e365dddcd8297abd7d",
"dependencies": [
- "jsr:@deno/loader",
+ "jsr:@deno/loader@~0.3.3",
"jsr:@std/path@^1.1.1",
"npm:esbuild@~0.25.5"
]
@@ -335,6 +341,126 @@
"@jridgewell/trace-mapping"
]
},
+ "@babel/code-frame@7.27.1": {
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dependencies": [
+ "@babel/helper-validator-identifier",
+ "js-tokens",
+ "picocolors"
+ ]
+ },
+ "@babel/compat-data@7.28.0": {
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="
+ },
+ "@babel/core@7.28.0": {
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
+ "dependencies": [
+ "@ampproject/remapping",
+ "@babel/code-frame",
+ "@babel/generator",
+ "@babel/helper-compilation-targets",
+ "@babel/helper-module-transforms",
+ "@babel/helpers",
+ "@babel/parser",
+ "@babel/template",
+ "@babel/traverse",
+ "@babel/types",
+ "convert-source-map",
+ "debug",
+ "gensync",
+ "json5",
+ "semver"
+ ]
+ },
+ "@babel/generator@7.28.0": {
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
+ "dependencies": [
+ "@babel/parser",
+ "@babel/types",
+ "@jridgewell/gen-mapping",
+ "@jridgewell/trace-mapping",
+ "jsesc"
+ ]
+ },
+ "@babel/helper-compilation-targets@7.27.2": {
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dependencies": [
+ "@babel/compat-data",
+ "@babel/helper-validator-option",
+ "browserslist",
+ "lru-cache@5.1.1",
+ "semver"
+ ]
+ },
+ "@babel/helper-globals@7.28.0": {
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="
+ },
+ "@babel/helper-module-imports@7.27.1": {
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dependencies": [
+ "@babel/traverse",
+ "@babel/types"
+ ]
+ },
+ "@babel/helper-module-transforms@7.27.3_@babel+core@7.28.0": {
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/helper-module-imports",
+ "@babel/helper-validator-identifier",
+ "@babel/traverse"
+ ]
+ },
+ "@babel/helper-string-parser@7.27.1": {
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
+ },
+ "@babel/helper-validator-identifier@7.27.1": {
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
+ },
+ "@babel/helper-validator-option@7.27.1": {
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="
+ },
+ "@babel/helpers@7.28.2": {
+ "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
+ "dependencies": [
+ "@babel/template",
+ "@babel/types"
+ ]
+ },
+ "@babel/parser@7.28.0": {
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dependencies": [
+ "@babel/types"
+ ],
+ "bin": true
+ },
+ "@babel/template@7.27.2": {
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dependencies": [
+ "@babel/code-frame",
+ "@babel/parser",
+ "@babel/types"
+ ]
+ },
+ "@babel/traverse@7.28.0": {
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
+ "dependencies": [
+ "@babel/code-frame",
+ "@babel/generator",
+ "@babel/helper-globals",
+ "@babel/parser",
+ "@babel/template",
+ "@babel/types",
+ "debug"
+ ]
+ },
+ "@babel/types@7.28.2": {
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "dependencies": [
+ "@babel/helper-string-parser",
+ "@babel/helper-validator-identifier"
+ ]
+ },
"@emnapi/core@1.4.5": {
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
"dependencies": [
@@ -941,6 +1067,9 @@
"@pkgjs/parseargs@0.11.0": {
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="
},
+ "@polka/url@1.0.0-next.29": {
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="
+ },
"@preact/signals-core@1.11.0": {
"integrity": "sha512-jglbibeWHuFRzEWVFY/TT7wB1PppJxmcSfUHcK+2J9vBRtiooMfw6tAPttojNYrrpdGViqAYCbPpmWYlMm+eMQ=="
},
@@ -958,6 +1087,137 @@
"preact"
]
},
+ "@prefresh/babel-plugin@0.5.2": {
+ "integrity": "sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA=="
+ },
+ "@prefresh/core@1.5.5_preact@10.27.0": {
+ "integrity": "sha512-H6GTXUl4V4fe3ijz7yhSa/mZ+pGSOh7XaJb6uP/sQsagBx9yl0D1HKDaeoMQA8Ad2Xm27LqvbitMGSdY9UFSKQ==",
+ "dependencies": [
+ "preact"
+ ]
+ },
+ "@prefresh/utils@1.2.1": {
+ "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw=="
+ },
+ "@prefresh/vite@2.4.8_preact@10.27.0_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15": {
+ "integrity": "sha512-H7vlo9UbJInuRbZhRQrdgVqLP7qKjDoX7TgYWWwIVhEHeHO0hZ4zyicvwBrV1wX5A3EPOmArgRkUaN7cPI2VXQ==",
+ "dependencies": [
+ "@babel/core",
+ "@prefresh/babel-plugin",
+ "@prefresh/core",
+ "@prefresh/utils",
+ "@rollup/pluginutils",
+ "preact",
+ "vite"
+ ]
+ },
+ "@rollup/pluginutils@4.2.1": {
+ "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
+ "dependencies": [
+ "estree-walker",
+ "picomatch@2.3.1"
+ ]
+ },
+ "@rollup/rollup-android-arm-eabi@4.46.2": {
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
+ "os": ["android"],
+ "cpu": ["arm"]
+ },
+ "@rollup/rollup-android-arm64@4.46.2": {
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
+ "os": ["android"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-darwin-arm64@4.46.2": {
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
+ "os": ["darwin"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-darwin-x64@4.46.2": {
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
+ "os": ["darwin"],
+ "cpu": ["x64"]
+ },
+ "@rollup/rollup-freebsd-arm64@4.46.2": {
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
+ "os": ["freebsd"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-freebsd-x64@4.46.2": {
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
+ "os": ["freebsd"],
+ "cpu": ["x64"]
+ },
+ "@rollup/rollup-linux-arm-gnueabihf@4.46.2": {
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
+ "os": ["linux"],
+ "cpu": ["arm"]
+ },
+ "@rollup/rollup-linux-arm-musleabihf@4.46.2": {
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
+ "os": ["linux"],
+ "cpu": ["arm"]
+ },
+ "@rollup/rollup-linux-arm64-gnu@4.46.2": {
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
+ "os": ["linux"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-linux-arm64-musl@4.46.2": {
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
+ "os": ["linux"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-linux-loongarch64-gnu@4.46.2": {
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
+ "os": ["linux"],
+ "cpu": ["loong64"]
+ },
+ "@rollup/rollup-linux-ppc64-gnu@4.46.2": {
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
+ "os": ["linux"],
+ "cpu": ["ppc64"]
+ },
+ "@rollup/rollup-linux-riscv64-gnu@4.46.2": {
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
+ "os": ["linux"],
+ "cpu": ["riscv64"]
+ },
+ "@rollup/rollup-linux-riscv64-musl@4.46.2": {
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
+ "os": ["linux"],
+ "cpu": ["riscv64"]
+ },
+ "@rollup/rollup-linux-s390x-gnu@4.46.2": {
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
+ "os": ["linux"],
+ "cpu": ["s390x"]
+ },
+ "@rollup/rollup-linux-x64-gnu@4.46.2": {
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
+ "os": ["linux"],
+ "cpu": ["x64"]
+ },
+ "@rollup/rollup-linux-x64-musl@4.46.2": {
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
+ "os": ["linux"],
+ "cpu": ["x64"]
+ },
+ "@rollup/rollup-win32-arm64-msvc@4.46.2": {
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
+ "os": ["win32"],
+ "cpu": ["arm64"]
+ },
+ "@rollup/rollup-win32-ia32-msvc@4.46.2": {
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
+ "os": ["win32"],
+ "cpu": ["ia32"]
+ },
+ "@rollup/rollup-win32-x64-msvc@4.46.2": {
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
+ "os": ["win32"],
+ "cpu": ["x64"]
+ },
"@tailwindcss/node@4.1.11": {
"integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
"dependencies": [
@@ -1092,10 +1352,19 @@
"tslib"
]
},
+ "@types/estree@1.0.8": {
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
+ },
"@types/node@22.15.15": {
"integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
"dependencies": [
- "undici-types"
+ "undici-types@6.21.0"
+ ]
+ },
+ "@types/node@24.1.0": {
+ "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "dependencies": [
+ "undici-types@7.8.0"
]
},
"ansi-regex@5.0.1": {
@@ -1113,6 +1382,9 @@
"ansi-styles@6.2.1": {
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
},
+ "ansis@4.1.0": {
+ "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="
+ },
"any-promise@1.3.0": {
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
},
@@ -1120,7 +1392,7 @@
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dependencies": [
"normalize-path",
- "picomatch"
+ "picomatch@2.3.1"
]
},
"arg@5.0.2": {
@@ -1145,6 +1417,9 @@
"binary-extensions@2.3.0": {
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="
},
+ "birpc@2.5.0": {
+ "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="
+ },
"boolbase@1.0.0": {
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
@@ -1170,6 +1445,12 @@
],
"bin": true
},
+ "bundle-name@4.1.0": {
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dependencies": [
+ "run-applescript"
+ ]
+ },
"camelcase-css@2.0.1": {
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
},
@@ -1224,6 +1505,9 @@
"commander@7.2.0": {
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
},
+ "convert-source-map@2.0.0": {
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+ },
"cross-spawn@7.0.6": {
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": [
@@ -1328,6 +1612,25 @@
"cssom@0.5.0": {
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="
},
+ "debug@4.4.1": {
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dependencies": [
+ "ms"
+ ]
+ },
+ "default-browser-id@5.0.0": {
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="
+ },
+ "default-browser@5.2.1": {
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "dependencies": [
+ "bundle-name",
+ "default-browser-id"
+ ]
+ },
+ "define-lazy-prop@3.0.0": {
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="
+ },
"detect-libc@2.0.4": {
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="
},
@@ -1387,6 +1690,9 @@
"entities@6.0.1": {
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="
},
+ "error-stack-parser-es@1.0.5": {
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="
+ },
"esbuild-wasm@0.25.7": {
"integrity": "sha512-x3t1BlU59YOMtpwzayHxF4LPVujOvNKqm7y6jPvFKC13J8FmJRCdHPJwHq86er7ik+f7uwGcMbe+6KVzLGmsGw==",
"bin": true
@@ -1427,6 +1733,9 @@
"escalade@3.2.0": {
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
},
+ "estree-walker@2.0.2": {
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
"fast-glob@3.3.3": {
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dependencies": [
@@ -1443,6 +1752,15 @@
"reusify"
]
},
+ "fdir@6.4.6_picomatch@4.0.3": {
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dependencies": [
+ "picomatch@4.0.3"
+ ],
+ "optionalPeers": [
+ "picomatch@4.0.3"
+ ]
+ },
"fflate@0.8.2": {
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
@@ -1470,6 +1788,9 @@
"function-bind@1.1.2": {
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
+ "gensync@1.0.0-beta.2": {
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
+ },
"github-slugger@2.0.0": {
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
},
@@ -1530,6 +1851,10 @@
"hasown"
]
},
+ "is-docker@3.0.0": {
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "bin": true
+ },
"is-extglob@2.1.1": {
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
@@ -1542,9 +1867,22 @@
"is-extglob"
]
},
+ "is-inside-container@1.0.0": {
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dependencies": [
+ "is-docker"
+ ],
+ "bin": true
+ },
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
+ "is-wsl@3.1.0": {
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "dependencies": [
+ "is-inside-container"
+ ]
+ },
"isexe@2.0.0": {
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
@@ -1565,6 +1903,17 @@
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"bin": true
},
+ "js-tokens@4.0.0": {
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "jsesc@3.1.0": {
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "bin": true
+ },
+ "json5@2.2.3": {
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "bin": true
+ },
"lightningcss-darwin-arm64@1.30.1": {
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"os": ["darwin"],
@@ -1658,6 +2007,12 @@
"lru-cache@10.4.3": {
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
+ "lru-cache@5.1.1": {
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dependencies": [
+ "yallist@3.1.1"
+ ]
+ },
"magic-string@0.30.17": {
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dependencies": [
@@ -1687,7 +2042,7 @@
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": [
"braces",
- "picomatch"
+ "picomatch@2.3.1"
]
},
"mime-db@1.54.0": {
@@ -1718,6 +2073,12 @@
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"bin": true
},
+ "mrmime@2.0.1": {
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="
+ },
+ "ms@2.1.3": {
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
"mz@2.7.0": {
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dependencies": [
@@ -1751,6 +2112,18 @@
"object-hash@3.0.0": {
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="
},
+ "ohash@2.0.11": {
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="
+ },
+ "open@10.2.0": {
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dependencies": [
+ "default-browser",
+ "define-lazy-prop",
+ "is-inside-container",
+ "wsl-utils"
+ ]
+ },
"package-json-from-dist@1.0.1": {
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
},
@@ -1766,16 +2139,25 @@
"path-scurry@1.11.1": {
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dependencies": [
- "lru-cache",
+ "lru-cache@10.4.3",
"minipass"
]
},
+ "pathe@2.0.3": {
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
+ },
+ "perfect-debounce@1.0.0": {
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
+ },
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"picomatch@2.3.1": {
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
+ "picomatch@4.0.3": {
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
+ },
"pify@2.3.0": {
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
},
@@ -2059,7 +2441,7 @@
"readdirp@3.6.0": {
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": [
- "picomatch"
+ "picomatch@2.3.1"
]
},
"resolve@1.22.10": {
@@ -2074,12 +2456,49 @@
"reusify@1.1.0": {
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="
},
+ "rollup@4.46.2": {
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
+ "dependencies": [
+ "@types/estree"
+ ],
+ "optionalDependencies": [
+ "@rollup/rollup-android-arm-eabi",
+ "@rollup/rollup-android-arm64",
+ "@rollup/rollup-darwin-arm64",
+ "@rollup/rollup-darwin-x64",
+ "@rollup/rollup-freebsd-arm64",
+ "@rollup/rollup-freebsd-x64",
+ "@rollup/rollup-linux-arm-gnueabihf",
+ "@rollup/rollup-linux-arm-musleabihf",
+ "@rollup/rollup-linux-arm64-gnu",
+ "@rollup/rollup-linux-arm64-musl",
+ "@rollup/rollup-linux-loongarch64-gnu",
+ "@rollup/rollup-linux-ppc64-gnu",
+ "@rollup/rollup-linux-riscv64-gnu",
+ "@rollup/rollup-linux-riscv64-musl",
+ "@rollup/rollup-linux-s390x-gnu",
+ "@rollup/rollup-linux-x64-gnu",
+ "@rollup/rollup-linux-x64-musl",
+ "@rollup/rollup-win32-arm64-msvc",
+ "@rollup/rollup-win32-ia32-msvc",
+ "@rollup/rollup-win32-x64-msvc",
+ "fsevents"
+ ],
+ "bin": true
+ },
+ "run-applescript@7.0.0": {
+ "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="
+ },
"run-parallel@1.2.0": {
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dependencies": [
"queue-microtask"
]
},
+ "semver@6.3.1": {
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "bin": true
+ },
"shebang-command@2.0.0": {
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": [
@@ -2092,6 +2511,14 @@
"signal-exit@4.1.0": {
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
},
+ "sirv@3.0.1": {
+ "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
+ "dependencies": [
+ "@polka/url",
+ "mrmime",
+ "totalist"
+ ]
+ },
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
@@ -2202,7 +2629,7 @@
"minipass",
"minizlib",
"mkdirp",
- "yallist"
+ "yallist@5.0.0"
]
},
"thenify-all@1.6.0": {
@@ -2217,12 +2644,22 @@
"any-promise"
]
},
+ "tinyglobby@0.2.14_picomatch@4.0.3": {
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dependencies": [
+ "fdir",
+ "picomatch@4.0.3"
+ ]
+ },
"to-regex-range@5.0.1": {
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": [
"is-number"
]
},
+ "totalist@3.0.1": {
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
+ },
"ts-interface-checker@0.1.13": {
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
},
@@ -2242,6 +2679,16 @@
"undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
+ "undici-types@7.8.0": {
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
+ },
+ "unplugin-utils@0.2.4": {
+ "integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==",
+ "dependencies": [
+ "pathe",
+ "picomatch@4.0.3"
+ ]
+ },
"update-browserslist-db@1.1.3_browserslist@4.25.1": {
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [
@@ -2254,6 +2701,54 @@
"util-deprecate@1.0.2": {
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
+ "vite-dev-rpc@1.1.0_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15": {
+ "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==",
+ "dependencies": [
+ "birpc",
+ "vite",
+ "vite-hot-client"
+ ]
+ },
+ "vite-hot-client@2.1.0_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15": {
+ "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==",
+ "dependencies": [
+ "vite"
+ ]
+ },
+ "vite-plugin-inspect@11.3.2_vite@7.0.6__@types+node@22.15.15__picomatch@4.0.3_@types+node@22.15.15": {
+ "integrity": "sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==",
+ "dependencies": [
+ "ansis",
+ "debug",
+ "error-stack-parser-es",
+ "ohash",
+ "open",
+ "perfect-debounce",
+ "sirv",
+ "unplugin-utils",
+ "vite",
+ "vite-dev-rpc"
+ ]
+ },
+ "vite@7.0.6_@types+node@22.15.15_picomatch@4.0.3": {
+ "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
+ "dependencies": [
+ "@types/node@22.15.15",
+ "esbuild",
+ "fdir",
+ "picomatch@4.0.3",
+ "postcss",
+ "rollup",
+ "tinyglobby"
+ ],
+ "optionalDependencies": [
+ "fsevents"
+ ],
+ "optionalPeers": [
+ "@types/node@22.15.15"
+ ],
+ "bin": true
+ },
"which@2.0.2": {
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": [
@@ -2277,6 +2772,15 @@
"strip-ansi@7.1.0"
]
},
+ "wsl-utils@0.1.0": {
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "dependencies": [
+ "is-wsl"
+ ]
+ },
+ "yallist@3.1.1": {
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+ },
"yallist@5.0.0": {
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="
},
@@ -2395,6 +2899,16 @@
"npm:postcss@^8.5.6",
"npm:tailwindcss@^3.4.17"
]
+ },
+ "plugin-vite": {
+ "dependencies": [
+ "jsr:@deno/loader@~0.3.2",
+ "npm:@prefresh/vite@^2.4.8",
+ "npm:@types/node@^24.1.0",
+ "npm:preact@^10.26.9",
+ "npm:vite-plugin-inspect@^11.3.2",
+ "npm:vite@^7.0.6"
+ ]
}
}
}
diff --git a/init/src/init_test.ts b/init/src/init_test.ts
index e595e09e2f0..075ea875954 100644
--- a/init/src/init_test.ts
+++ b/init/src/init_test.ts
@@ -214,7 +214,7 @@ Deno.test("init - can start built project", async () => {
}).output();
await withChildProcessServer(
- { cwd: dir, args: ["task", "start"] },
+ { cwd: dir, env: { PORT: "0" }, args: ["task", "start"] },
async (address) => {
await withBrowser(async (page) => {
await page.goto(address);
diff --git a/plugin-vite/README.md b/plugin-vite/README.md
new file mode 100644
index 00000000000..17fc64c4b33
--- /dev/null
+++ b/plugin-vite/README.md
@@ -0,0 +1,35 @@
+# EXPERIMENTAL: vite plugin for Fresh
+
+> **Warning** This package is experimental. Please report bugs to
+> https://github.com/denoland/fresh/issues
+
+## Usage
+
+1. Run `deno install jsr:@fresh/plugin-vite npm:vite`
+2. Ensure that your `deno.json` has the `fresh` import mapping:
+
+```json
+{
+ "imports": {
+ "fresh": "jsr:@fresh/core"
+ }
+}
+```
+
+3. Update your `vite.config.ts` file:
+
+```diff
+ import { defineConfig } from "vite";
++ import { fresh } from "@fresh/plugin-vite";
+
+ export default defineConfig({
+ plugins: [
++ fresh(),
+ ],
+ });
+```
+
+4. Update your deno tasks respectively:
+
+- Dev: `vite`
+- Build: `vite build`
diff --git a/plugin-vite/demo/islands/Bar.tsx b/plugin-vite/demo/islands/Bar.tsx
new file mode 100644
index 00000000000..52ce8306cf8
--- /dev/null
+++ b/plugin-vite/demo/islands/Bar.tsx
@@ -0,0 +1,13 @@
+import { useState } from "preact/hooks";
+
+export function Bar() {
+ const [count, set] = useState(0);
+ return (
+
+
island asdf
+
+
+ );
+}
diff --git a/plugin-vite/demo/islands/Foo.tsx b/plugin-vite/demo/islands/Foo.tsx
new file mode 100644
index 00000000000..156c047ed61
--- /dev/null
+++ b/plugin-vite/demo/islands/Foo.tsx
@@ -0,0 +1,13 @@
+import { useSignal } from "@preact/signals";
+
+export function Foo() {
+ const count = useSignal(0);
+ return (
+
+
island
+
+
+ );
+}
diff --git a/plugin-vite/demo/islands/tests/CounterHooks.tsx b/plugin-vite/demo/islands/tests/CounterHooks.tsx
new file mode 100644
index 00000000000..ea53fb53e18
--- /dev/null
+++ b/plugin-vite/demo/islands/tests/CounterHooks.tsx
@@ -0,0 +1,13 @@
+import { useState } from "preact/hooks";
+
+export function CounterHooks() {
+ const [count, set] = useState(0);
+
+ return (
+
+
+
+ );
+}
diff --git a/plugin-vite/demo/main.ts b/plugin-vite/demo/main.ts
new file mode 100644
index 00000000000..84cba68f323
--- /dev/null
+++ b/plugin-vite/demo/main.ts
@@ -0,0 +1,7 @@
+import { App, staticFiles, trailingSlashes } from "fresh";
+
+export const app = new App()
+ .use(staticFiles())
+ .use(trailingSlashes("never"))
+ .get("/", () => new Response("it works"))
+ .fsRoutes();
diff --git a/plugin-vite/demo/routes/about.tsx b/plugin-vite/demo/routes/about.tsx
new file mode 100644
index 00000000000..0489d2cbab7
--- /dev/null
+++ b/plugin-vite/demo/routes/about.tsx
@@ -0,0 +1,17 @@
+import { Bar } from "../islands/Bar.tsx";
+import { Foo } from "../islands/Foo.tsx";
+
+export const handler = () => {
+ return { data: {} };
+};
+
+export default function About() {
+ return (
+
+
Hello from Vite
+
This page is the prod Fresh build and served by vite
+
+
+
+ );
+}
diff --git a/plugin-vite/demo/routes/index.tsx b/plugin-vite/demo/routes/index.tsx
new file mode 100644
index 00000000000..00f3b2800b6
--- /dev/null
+++ b/plugin-vite/demo/routes/index.tsx
@@ -0,0 +1,3 @@
+export default function Index() {
+ return index page2
;
+}
diff --git a/plugin-vite/demo/routes/tests/island_hooks.tsx b/plugin-vite/demo/routes/tests/island_hooks.tsx
new file mode 100644
index 00000000000..1aab54d0f59
--- /dev/null
+++ b/plugin-vite/demo/routes/tests/island_hooks.tsx
@@ -0,0 +1,5 @@
+import { CounterHooks } from "../../islands/tests/CounterHooks.tsx";
+
+export default function Page() {
+ return ;
+}
diff --git a/plugin-vite/demo/routes/tests/it_works.tsx b/plugin-vite/demo/routes/tests/it_works.tsx
new file mode 100644
index 00000000000..5048322b358
--- /dev/null
+++ b/plugin-vite/demo/routes/tests/it_works.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return it works
;
+}
diff --git a/plugin-vite/demo/static/foo.txt b/plugin-vite/demo/static/foo.txt
new file mode 100644
index 00000000000..19102815663
--- /dev/null
+++ b/plugin-vite/demo/static/foo.txt
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/plugin-vite/demo/static/test_static/foo.txt b/plugin-vite/demo/static/test_static/foo.txt
new file mode 100644
index 00000000000..85c88293920
--- /dev/null
+++ b/plugin-vite/demo/static/test_static/foo.txt
@@ -0,0 +1 @@
+it works
\ No newline at end of file
diff --git a/plugin-vite/demo/vite.config.ts b/plugin-vite/demo/vite.config.ts
new file mode 100644
index 00000000000..653f850e0f3
--- /dev/null
+++ b/plugin-vite/demo/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import { fresh } from "@fresh/plugin-vite";
+import inspect from "vite-plugin-inspect";
+
+export default defineConfig({
+ plugins: [
+ inspect(),
+ fresh(),
+ ],
+});
diff --git a/plugin-vite/deno.json b/plugin-vite/deno.json
new file mode 100644
index 00000000000..00411770082
--- /dev/null
+++ b/plugin-vite/deno.json
@@ -0,0 +1,26 @@
+{
+ "name": "@fresh/plugin-vite",
+ "version": "0.0.1",
+ "license": "MIT",
+ "exports": {
+ ".": "./src/mod.ts"
+ },
+ "compilerOptions": {
+ "jsx": "precompile",
+ "jsxImportSource": "preact"
+ },
+ "tasks": {
+ "demo": "vite demo",
+ "debug": "deno run -A --inspect-brk npm:vite demo",
+ "demo:build": "vite build demo",
+ "demo:start": "cd demo && deno serve -A _fresh/server/server-entry.mjs"
+ },
+ "imports": {
+ "@deno/loader": "jsr:@deno/loader@^0.3.2",
+ "@prefresh/vite": "npm:@prefresh/vite@^2.4.8",
+ "@types/node": "npm:@types/node@^24.1.0",
+ "preact": "npm:preact@^10.26.9",
+ "vite": "npm:vite@^7.0.6",
+ "vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2"
+ }
+}
diff --git a/plugin-vite/src/mod.ts b/plugin-vite/src/mod.ts
new file mode 100644
index 00000000000..1d19b6a0f62
--- /dev/null
+++ b/plugin-vite/src/mod.ts
@@ -0,0 +1,128 @@
+import type { Plugin } from "vite";
+import {
+ type FreshViteConfig,
+ pathWithRoot,
+ type ResolvedFreshViteConfig,
+} from "./utils.ts";
+import { deno } from "./plugins/deno.ts";
+import prefresh from "@prefresh/vite";
+import { serverEntryPlugin } from "./plugins/server_entry.ts";
+import { clientEntryPlugin } from "./plugins/client_entry.ts";
+import { devServer } from "./plugins/dev_server.ts";
+import { buildIdPlugin } from "./plugins/build_id.ts";
+import { clientSnapshot } from "./plugins/client_snapshot.ts";
+import { serverSnapshot } from "./plugins/server_snapshot.ts";
+
+export function fresh(config?: FreshViteConfig): Plugin[] {
+ const fConfig: ResolvedFreshViteConfig = {
+ serverEntry: config?.serverEntry ?? "main.ts",
+ islandsDir: config?.islandsDir ?? "islands",
+ routeDir: config?.routeDir ?? "routes",
+ ignore: config?.ignore ?? [],
+ };
+
+ return [
+ {
+ name: "fresh",
+ config(config, env) {
+ return {
+ esbuild: {
+ jsx: "automatic",
+ jsxImportSource: "preact",
+ jsxDev: env.command === "serve",
+ },
+ resolve: {
+ alias: {
+ "react-dom/test-utils": "preact/test-utils",
+ "react-dom": "preact/compat",
+ react: "preact/compat",
+ },
+ noExternal: true,
+ },
+ optimizeDeps: {
+ include: [
+ "preact",
+ "preact/compat",
+ "preact/debug",
+ "preact/devtools",
+ "preact/hooks",
+ "preact/jsx-runtime",
+ "preact/jsx-dev-runtime",
+ "@preact/signals",
+ ],
+ },
+
+ publicDir: pathWithRoot("static", config.root),
+
+ builder: {
+ async buildApp(builder) {
+ // Build client env first
+ const clientEnv = builder.environments.client;
+ if (clientEnv !== undefined) {
+ await builder.build(clientEnv);
+ }
+
+ await Promise.all(
+ Object.values(builder.environments).filter((env) =>
+ env !== clientEnv
+ ).map((env) => builder.build(env)),
+ );
+ },
+ },
+ environments: {
+ client: {
+ build: {
+ copyPublicDir: false,
+ manifest: true,
+
+ outDir: config.environments?.client?.build?.outDir ??
+ "_fresh/client",
+ rollupOptions: {
+ preserveEntrySignatures: "strict",
+ input: {
+ "client-entry": "fresh:client-entry",
+ "client-snapshot": "fresh:client-snapshot",
+ },
+ },
+ },
+ },
+ ssr: {
+ build: {
+ manifest: false,
+ copyPublicDir: false,
+
+ outDir: config.environments?.ssr?.build?.outDir ??
+ "_fresh/server",
+ rollupOptions: {
+ input: {
+ "server-entry": "fresh:server_entry",
+ },
+ },
+ },
+ },
+ },
+ };
+ },
+ configResolved(config) {
+ fConfig.islandsDir = pathWithRoot(fConfig.islandsDir, config.root);
+ fConfig.routeDir = pathWithRoot(fConfig.routeDir, config.root);
+ },
+ },
+ serverEntryPlugin(fConfig),
+ ...serverSnapshot(fConfig),
+ clientEntryPlugin(),
+ clientSnapshot(fConfig),
+ buildIdPlugin(),
+ ...devServer(),
+ prefresh({
+ include: [/\.[cm]?[tj]sx?$/],
+ exclude: [/node_modules/],
+ parserPlugins: [
+ "importMeta",
+ "explicitResourceManagement",
+ "topLevelAwait",
+ ],
+ }),
+ deno(),
+ ];
+}
diff --git a/plugin-vite/src/plugins/build_id.ts b/plugin-vite/src/plugins/build_id.ts
new file mode 100644
index 00000000000..c5ae2effd47
--- /dev/null
+++ b/plugin-vite/src/plugins/build_id.ts
@@ -0,0 +1,51 @@
+import type { Plugin } from "vite";
+
+export function buildIdPlugin(): Plugin {
+ let buildId = "";
+
+ return {
+ name: "fresh:build-id",
+ async config(_, env) {
+ const isDev = env.command === "serve";
+
+ buildId = await getBuildId(isDev);
+
+ return {
+ define: {
+ BUILD_ID: JSON.stringify(buildId),
+ },
+ };
+ },
+ applyToEnvironment() {
+ return true;
+ },
+ resolveId(id) {
+ if (id === "fresh/build-id") {
+ return `\0fresh/build-id`;
+ }
+ },
+ load(id) {
+ if (id !== `\0fresh/build-id`) return;
+
+ return `export let BUILD_ID = ${JSON.stringify(buildId)};
+export const DENO_DEPLOYMENT_ID = undefined;
+export function setBuildId(id) {
+ BUILD_ID = id;
+}`;
+ },
+ };
+}
+
+export async function getBuildId(dev: boolean): Promise {
+ if (!dev) {
+ const bin = Deno.build.os === "windows" ? "git.exe" : "git";
+ const res = await new Deno.Command(bin, { args: ["rev-parse", "HEAD"] })
+ .output();
+
+ return new TextDecoder().decode(res.stdout);
+ }
+
+ const arr = new Uint32Array(1);
+ crypto.getRandomValues(arr);
+ return String(arr[0]);
+}
diff --git a/plugin-vite/src/plugins/client_entry.ts b/plugin-vite/src/plugins/client_entry.ts
new file mode 100644
index 00000000000..45c6a3d3936
--- /dev/null
+++ b/plugin-vite/src/plugins/client_entry.ts
@@ -0,0 +1,33 @@
+import type { Plugin } from "vite";
+
+export function clientEntryPlugin(): Plugin {
+ const modName = "fresh:client-entry";
+
+ return {
+ name: "fresh:client-entry",
+ applyToEnvironment(env) {
+ return env.name === "client";
+ },
+ resolveId(id) {
+ if (id === modName) {
+ return `\0${modName}`;
+ }
+ },
+ load(id) {
+ if (id !== `\0${modName}`) return;
+
+ return `export * from "fresh/runtime-client";
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ console.log("accepting")
+ });
+ import.meta.hot.on("fresh:reload", ev => {
+ console.log(ev)
+ window.location.reload();
+ });
+}
+`;
+ },
+ };
+}
diff --git a/plugin-vite/src/plugins/client_snapshot.ts b/plugin-vite/src/plugins/client_snapshot.ts
new file mode 100644
index 00000000000..d1ab7d16ca7
--- /dev/null
+++ b/plugin-vite/src/plugins/client_snapshot.ts
@@ -0,0 +1,79 @@
+import type { Plugin, ViteDevServer } from "vite";
+import type { ResolvedFreshViteConfig } from "../utils.ts";
+import { crawlFsItem } from "fresh/internal-dev";
+
+export function clientSnapshot(options: ResolvedFreshViteConfig): Plugin {
+ const modName = "fresh:client-snapshot";
+
+ const islands = new Set();
+ let server: ViteDevServer | undefined;
+ let isDev = false;
+
+ return {
+ name: "fresh:client-snapshot",
+ applyToEnvironment(env) {
+ return env.name === "client";
+ },
+ config(_, env) {
+ isDev = env.command === "serve";
+
+ return {
+ environments: {
+ client: {
+ build: {
+ rollupOptions: {
+ input: {
+ "client-snapshot": "fresh:client-snapshot",
+ },
+ },
+ },
+ },
+ },
+ };
+ },
+ async buildStart() {
+ const result = await crawlFsItem({
+ islandDir: options.islandsDir,
+ routeDir: options.routeDir,
+ ignore: options.ignore,
+ });
+
+ for (let i = 0; i < result.islands.length; i++) {
+ const filePath = result.islands[i];
+ islands.add(filePath);
+ }
+ },
+ resolveId(id) {
+ if (id === modName) {
+ return `\0${modName}`;
+ }
+ },
+ configureServer(devServer) {
+ server = devServer;
+ },
+ load(id) {
+ if (id !== `\0${modName}`) return;
+
+ const imports = Array.from(islands.keys()).map((file, i) => {
+ return `export const mod_${i} = await import(${JSON.stringify(file)});`;
+ }).join("\n");
+
+ if (isDev && server !== undefined) {
+ const mod = server.environments.ssr.moduleGraph.getModuleById(
+ "\0fresh:server-snapshot",
+ );
+ if (mod) {
+ server.environments.ssr.moduleGraph.invalidateModule(mod);
+ }
+ }
+
+ return `${imports}
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ console.log("accepting client-snapshot")
+ });
+}
+`;
+ },
+ };
+}
diff --git a/plugin-vite/src/plugins/deno.ts b/plugin-vite/src/plugins/deno.ts
new file mode 100644
index 00000000000..56f0812402a
--- /dev/null
+++ b/plugin-vite/src/plugins/deno.ts
@@ -0,0 +1,187 @@
+import type { Plugin } from "vite";
+import {
+ type Loader,
+ RequestedModuleType,
+ ResolutionMode,
+ Workspace,
+} from "@deno/loader";
+import * as path from "@std/path";
+
+interface DenoState {
+ type: RequestedModuleType;
+}
+
+export function deno(): Plugin {
+ let loader: Loader;
+
+ return {
+ name: "deno",
+ async configResolved() {
+ // TODO: Pass conditions
+ loader = await new Workspace({}).createLoader();
+ },
+ applyToEnvironment() {
+ return true;
+ },
+ async resolveId(id, importer, options) {
+ importer = isDenoSpecifier(importer)
+ ? parseDenoSpecifier(importer).specifier
+ : importer;
+
+ try {
+ // Ensure we're passing a valid importer that Deno understands
+ const denoImporter = importer && !importer.startsWith("\0")
+ ? importer
+ : undefined;
+
+ let resolved = await loader.resolve(
+ id,
+ denoImporter,
+ ResolutionMode.Import,
+ );
+
+ if (id === resolved) {
+ return id;
+ }
+
+ const type = getDenoType(options.attributes.type ?? "default");
+ if (
+ type !== RequestedModuleType.Default ||
+ /^(https?|jsr|npm):/.test(resolved) ||
+ // Vite does weird things to windows file paths
+ Deno.build.os === "windows"
+ ) {
+ return toDenoSpecifier(resolved, type);
+ }
+
+ if (resolved.startsWith("file://")) {
+ resolved = path.fromFileUrl(resolved);
+ }
+
+ return {
+ id: resolved,
+ meta: {
+ deno: {
+ type,
+ },
+ },
+ };
+ } catch {
+ // ignore
+ }
+ },
+ async load(id) {
+ if (isDenoSpecifier(id)) {
+ const { type, specifier } = parseDenoSpecifier(id);
+
+ const result = await loader.load(specifier, type);
+ if (result.kind === "external") {
+ return null;
+ }
+
+ return {
+ code: new TextDecoder().decode(result.code),
+ };
+ }
+
+ if (id.startsWith("\0")) {
+ id = id.slice(1);
+ }
+
+ const meta = this.getModuleInfo(id)?.meta.deno as
+ | DenoState
+ | undefined
+ | null;
+
+ if (meta === null || meta === undefined) return;
+
+ const url = path.toFileUrl(id);
+
+ const result = await loader.load(url.href, meta.type);
+ if (result.kind === "external") {
+ return null;
+ }
+
+ return {
+ code: new TextDecoder().decode(result.code),
+ };
+ },
+ async transform(_, id, options) {
+ // This transform is a hack to be able to re-use Deno's precompile
+ // jsx transform.
+ if (!options?.ssr || !id.endsWith(".tsx") || id.endsWith(".jsx")) {
+ return;
+ }
+
+ let actualId = id;
+ if (isDenoSpecifier(id)) {
+ const { specifier } = parseDenoSpecifier(id);
+ actualId = specifier;
+ }
+ if (path.isAbsolute(actualId)) {
+ actualId = path.toFileUrl(actualId).href;
+ }
+
+ const resolved = await loader.resolve(
+ actualId,
+ undefined,
+ ResolutionMode.Import,
+ );
+ const result = await loader.load(resolved, RequestedModuleType.Default);
+ if (result.kind === "external") {
+ return;
+ }
+
+ const code = new TextDecoder().decode(result.code);
+
+ return {
+ code,
+ };
+ },
+ };
+}
+
+export type DenoSpecifier = string & { __deno: string };
+
+function isDenoSpecifier(str: unknown): str is DenoSpecifier {
+ return typeof str === "string" && str.startsWith("\0deno::");
+}
+
+function toDenoSpecifier(spec: string, type: RequestedModuleType) {
+ return `\0deno::${type}::${spec}`;
+}
+
+function parseDenoSpecifier(
+ spec: DenoSpecifier,
+): { type: RequestedModuleType; specifier: string } {
+ const match = spec.match(/^\0deno::([^:]+)::(.*)$/)!;
+
+ let specifier = match[2];
+
+ const specMatch = specifier.match(/^(\w+):\/([^/].*)$/);
+ if (specMatch !== null) {
+ const protocol = specMatch[1];
+
+ let rest = specMatch[2];
+ if (protocol === "file") {
+ rest = "/" + rest;
+ }
+
+ specifier = `${protocol}://${rest}`;
+ }
+
+ return { type: +match[1], specifier };
+}
+
+function getDenoType(type: string): RequestedModuleType {
+ switch (type) {
+ case "json":
+ return RequestedModuleType.Json;
+ case "bytes":
+ return RequestedModuleType.Bytes;
+ case "text":
+ return RequestedModuleType.Text;
+ default:
+ return RequestedModuleType.Default;
+ }
+}
diff --git a/plugin-vite/src/plugins/dev_server.ts b/plugin-vite/src/plugins/dev_server.ts
new file mode 100644
index 00000000000..5e630209292
--- /dev/null
+++ b/plugin-vite/src/plugins/dev_server.ts
@@ -0,0 +1,79 @@
+import type { Plugin } from "vite";
+import { nodeToRequest, responseToNode } from "../request.ts";
+import * as path from "@std/path";
+import { pathWithRoot } from "../utils.ts";
+
+export function devServer(): Plugin[] {
+ let publicDir = "";
+
+ return [
+ {
+ name: "fresh:dev_server",
+ configResolved(cfg) {
+ publicDir = pathWithRoot(cfg.publicDir, cfg.root);
+ },
+ configureServer(server) {
+ const IGNORE_URLS = /^\/(@(vite|fs|id)|\.vite)\//;
+
+ server.middlewares.use(async (nodeReq, nodeRes, next) => {
+ const serverCfg = server.config.server;
+
+ const protocol = serverCfg.https ? "https" : "http";
+ const host = serverCfg.host ? serverCfg.host : "localhost";
+ const port = serverCfg.port;
+ const url = new URL(
+ `${protocol}://${host}:${port}${nodeReq.url ?? "/"}`,
+ );
+
+ // Check if it's a vite url
+ if (
+ IGNORE_URLS.test(url.pathname) ||
+ server.environments.client.moduleGraph.urlToModuleMap.has(
+ url.pathname,
+ )
+ ) {
+ return next();
+ }
+
+ // Check if it's a static file first
+ // FIXME: Should this still go through fresh?
+ if (url.pathname !== "/") {
+ try {
+ await Deno.stat(path.join(publicDir, url.pathname));
+ return next();
+ } catch (err) {
+ if (!(err instanceof Deno.errors.NotFound)) {
+ return next(err);
+ }
+ }
+ }
+
+ const mod = await server.ssrLoadModule("fresh:server_entry");
+
+ try {
+ const req = nodeToRequest(nodeReq, url);
+ const res = await mod.default.fetch(req);
+ await responseToNode(res, nodeRes);
+ return nodeRes;
+ } catch (err) {
+ return next(err);
+ }
+ });
+ },
+ },
+ {
+ name: "fresh:server_hmr",
+ applyToEnvironment(env) {
+ return env.name === "ssr";
+ },
+ hotUpdate(options) {
+ const clientMod = options.server.environments.client.moduleGraph
+ .getModulesByFile(options.file);
+
+ if (clientMod === undefined) {
+ options.server.hot.send("fresh:reload");
+ }
+ },
+ },
+ ];
+}
diff --git a/plugin-vite/src/plugins/server_entry.ts b/plugin-vite/src/plugins/server_entry.ts
new file mode 100644
index 00000000000..ad8b6c978c7
--- /dev/null
+++ b/plugin-vite/src/plugins/server_entry.ts
@@ -0,0 +1,65 @@
+import type { Plugin } from "vite";
+import { generateServerEntry } from "fresh/internal-dev";
+import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts";
+import * as path from "@std/path";
+
+export function serverEntryPlugin(
+ options: ResolvedFreshViteConfig,
+): Plugin {
+ const modName = "fresh:server_entry";
+
+ let serverEntry = "";
+ let serverOutDir = "";
+ let root = "";
+
+ let isDev = false;
+
+ return {
+ name: "fresh:server_entry",
+ applyToEnvironment(env) {
+ return env.name === "ssr";
+ },
+ config(_, env) {
+ isDev = env.command === "serve";
+ },
+ configResolved(config) {
+ root = config.root;
+ serverEntry = pathWithRoot(options.serverEntry, config.root);
+ serverOutDir = pathWithRoot(
+ config.environments.ssr.build.outDir,
+ config.root,
+ );
+ },
+ resolveId(id) {
+ if (id === modName) {
+ return `\0${modName}`;
+ }
+ },
+ load(id) {
+ if (id !== `\0${modName}`) return;
+
+ let code = generateServerEntry({
+ root: path.relative(serverOutDir, root),
+ serverEntry: path.toFileUrl(serverEntry).href,
+ snapshotSpecifier: "fresh:server-snapshot",
+ });
+
+ if (isDev) {
+ code += `if (import.meta.hot) import.meta.hot.accept();\n`;
+ }
+
+ return code;
+ },
+ async writeBundle() {
+ const outDir = path.dirname(serverOutDir);
+ await Deno.writeTextFile(
+ path.join(outDir, "server.js"),
+ `import server from "./server/server-entry.mjs";
+export default {
+ fetch: server.fetch
+};
+`,
+ );
+ },
+ };
+}
diff --git a/plugin-vite/src/plugins/server_snapshot.ts b/plugin-vite/src/plugins/server_snapshot.ts
new file mode 100644
index 00000000000..f51fbf9b666
--- /dev/null
+++ b/plugin-vite/src/plugins/server_snapshot.ts
@@ -0,0 +1,189 @@
+import type { Manifest, Plugin, ViteDevServer } from "vite";
+import {
+ crawlFsItem,
+ fsAdapter,
+ generateSnapshotServer,
+ type IslandModChunk,
+ pathToSpec,
+ type PendingStaticFile,
+ specToName,
+ UniqueNamer,
+} from "fresh/internal-dev";
+import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts";
+import * as path from "@std/path";
+import { getBuildId } from "./build_id.ts";
+
+export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
+ const modName = "fresh:server-snapshot";
+
+ let isDev = false;
+ let server: ViteDevServer | undefined;
+
+ const namer = new UniqueNamer();
+
+ let clientOutDir = "";
+ let root = "";
+ let publicDir = "";
+
+ const islands = new Map();
+ const islandSpecByName = new Map();
+
+ return [{
+ name: "fresh:server-snapshot",
+ applyToEnvironment(env) {
+ return env.name === "ssr";
+ },
+ config(_, env) {
+ isDev = env.command === "serve";
+ },
+ configResolved(config) {
+ root = config.root;
+
+ publicDir = pathWithRoot(config.publicDir, config.root);
+ clientOutDir = pathWithRoot(
+ config.environments.client.build.outDir,
+ config.root,
+ );
+ },
+ configureServer(viteServer) {
+ server = viteServer;
+ },
+ async buildStart() {
+ },
+ resolveId(id) {
+ if (id === modName) {
+ return `\0${modName}`;
+ }
+ },
+ async load(id) {
+ if (id !== `\0${modName}`) return;
+
+ const result = await crawlFsItem({
+ islandDir: options.islandsDir,
+ routeDir: options.routeDir,
+ ignore: options.ignore,
+ });
+
+ for (let i = 0; i < result.islands.length; i++) {
+ const spec = result.islands[i];
+ const specName = specToName(spec);
+ const name = namer.getUniqueName(specName);
+
+ islands.set(spec, { name, chunk: null });
+ islandSpecByName.set(name, spec);
+ }
+
+ const staticFiles: PendingStaticFile[] = [];
+ let islandMods: IslandModChunk[] = [];
+ let clientEntry = "/@id/fresh:client-entry";
+ let buildId = "";
+
+ if (isDev && server !== undefined) {
+ for (const id of islands.keys()) {
+ const mod = server.environments.client.moduleGraph.getModuleById(
+ id,
+ );
+ if (mod !== undefined) {
+ const def = islands.get(id);
+ if (def !== undefined) def.chunk = mod.url;
+ }
+ }
+
+ islandMods = Array.from(islands.entries()).map(([id, def]) => {
+ return {
+ name: def.name,
+ server: id,
+ browser: def.chunk ?? `/@id/fresh-island::${def.name}`,
+ };
+ });
+ } else {
+ buildId = await getBuildId(false);
+ const manifest = JSON.parse(
+ await Deno.readTextFile(
+ path.join(clientOutDir, ".vite", "manifest.json"),
+ ),
+ ) as Manifest;
+
+ for (const chunk of Object.values(manifest)) {
+ if (chunk.name === "client-entry") {
+ clientEntry = pathToSpec(clientOutDir, chunk.file);
+ }
+
+ staticFiles.push({
+ filePath: path.join(clientOutDir, chunk.file),
+ pathname: chunk.file,
+ hash: null,
+ });
+
+ const namer = new UniqueNamer();
+ if (chunk.name === "client-snapshot") {
+ for (const id of chunk.dynamicImports ?? []) {
+ const mod = manifest[id];
+ const serverPath = path.join(root, mod.src ?? id);
+
+ const name = namer.getUniqueName(specToName(id));
+ islandMods.push({
+ name,
+ browser: pathToSpec(clientOutDir, mod.file),
+ server: serverPath,
+ });
+ }
+ }
+ }
+
+ const entries = await fsAdapter.walk(
+ publicDir,
+ {
+ followSymlinks: false,
+ includeDirs: false,
+ includeFiles: true,
+ skip: options.ignore,
+ },
+ );
+
+ for await (const entry of entries) {
+ const relative = path.relative(publicDir, entry.path);
+ const filePath = path.join(clientOutDir, relative);
+
+ try {
+ await Deno.mkdir(path.dirname(filePath), { recursive: true });
+ } catch (err) {
+ if (!(err instanceof Deno.errors.AlreadyExists)) {
+ throw err;
+ }
+ }
+ await Deno.copyFile(entry.path, filePath);
+
+ staticFiles.push({
+ filePath,
+ hash: null,
+ pathname: relative,
+ });
+ }
+ }
+
+ const code = await generateSnapshotServer({
+ outDir: root,
+ staticFiles,
+ buildId,
+ clientEntry,
+ fsRoutesFiles: result.routes,
+ islands: islandMods,
+ writeSpecifier: (file) => {
+ return path.toFileUrl(file).href;
+ },
+ });
+
+ return code;
+ },
+ }, {
+ name: "fresh:island-resolver",
+ resolveId(id) {
+ if (id.startsWith("fresh-island::")) {
+ const name = id.slice("fresh-island::".length);
+ const spec = islandSpecByName.get(name);
+ if (spec !== undefined) return spec;
+ }
+ },
+ }];
+}
diff --git a/plugin-vite/src/request.ts b/plugin-vite/src/request.ts
new file mode 100644
index 00000000000..967aea9f646
--- /dev/null
+++ b/plugin-vite/src/request.ts
@@ -0,0 +1,84 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
+
+export function nodeToRequest(node: IncomingMessage, url: URL): Request {
+ const controller = new AbortController();
+ node.once("aborted", () => controller.abort());
+
+ const body = undefined;
+
+ if (
+ node.method !== "HEAD" && node.method !== "GET" && node.method !== "OPTIONS"
+ ) {
+ // FIXME
+ }
+
+ return new Request(url, {
+ headers: node.headers as Record,
+ method: node.method,
+ body,
+ signal: controller.signal,
+ });
+}
+
+export async function responseToNode(
+ res: Response,
+ node: ServerResponse,
+): Promise {
+ res.headers.forEach((value, key) => {
+ node.setHeader(key, value);
+ });
+
+ node.writeHead(res.status);
+
+ if (res.body === null) {
+ node.end();
+ return;
+ }
+
+ if (res.body.locked) {
+ node.end(`Error: Response body is locked.`);
+ return;
+ }
+
+ const reader = res.body.getReader();
+
+ if (node.destroyed) {
+ await reader.cancel();
+ return;
+ }
+
+ const cancel = (error: unknown) => {
+ node.off("close", cancel);
+ node.off("error", cancel);
+
+ // Can't respond anymore with error
+ reader.cancel(error).catch(() => {});
+
+ if (error) {
+ node.destroy(error instanceof Error ? error : new Error(String(error)));
+ }
+ };
+
+ node.on("close", cancel);
+ node.on("error", cancel);
+
+ async function next() {
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ if (!node.write(value)) {
+ node.once("drain", next);
+ return;
+ }
+ }
+
+ node.end();
+ } catch (error) {
+ cancel(error);
+ }
+ }
+
+ next();
+}
diff --git a/plugin-vite/src/utils.ts b/plugin-vite/src/utils.ts
new file mode 100644
index 00000000000..0b23176048b
--- /dev/null
+++ b/plugin-vite/src/utils.ts
@@ -0,0 +1,41 @@
+import * as path from "@std/path";
+import type { FsRouteFileNoMod, UniqueNamer } from "fresh/internal-dev";
+
+export function pathWithRoot(fileOrDir: string, root?: string): string {
+ if (path.isAbsolute(fileOrDir)) return fileOrDir;
+
+ if (root === undefined) {
+ return path.join(Deno.cwd(), fileOrDir);
+ }
+
+ if (path.isAbsolute(root)) return path.join(root, fileOrDir);
+
+ return path.join(Deno.cwd(), root, fileOrDir);
+}
+
+export interface FreshState {
+ namer: UniqueNamer;
+ root: string;
+ serverEntry: string;
+ islandDir: string;
+ routeDir: string;
+ dev: boolean;
+ islands: Map;
+ // deno-lint-ignore no-explicit-any
+ routes: FsRouteFileNoMod[];
+ clientOutDir: string;
+ serverOutDir: string;
+}
+
+export interface ClientSnapshot {
+ entry: string;
+}
+
+export interface FreshViteConfig {
+ serverEntry?: string;
+ islandsDir?: string;
+ routeDir?: string;
+ ignore?: RegExp[];
+}
+
+export type ResolvedFreshViteConfig = Required;
diff --git a/plugin-vite/tests/build_test.ts b/plugin-vite/tests/build_test.ts
new file mode 100644
index 00000000000..73e7d441ad5
--- /dev/null
+++ b/plugin-vite/tests/build_test.ts
@@ -0,0 +1,77 @@
+import { expect } from "@std/expect";
+import {
+ waitForText,
+ withBrowser,
+ withChildProcessServer,
+} from "../../tests/test_utils.tsx";
+import { buildVite } from "./test_utils.ts";
+
+Deno.test({
+ name: "vite build - launches",
+ fn: async () => {
+ await using res = await buildVite();
+
+ await withChildProcessServer(
+ {
+ cwd: res.tmp,
+ args: ["serve", "-A", "--port", "0", "_fresh/server.js"],
+ },
+ async (address) => {
+ const res = await fetch(address);
+ const text = await res.text();
+ expect(text).toEqual("it works");
+ },
+ );
+ },
+ sanitizeOps: false,
+ sanitizeResources: false,
+});
+
+Deno.test({
+ name: "vite build - serves static files",
+ fn: async () => {
+ await using viteResult = await buildVite();
+
+ await withChildProcessServer(
+ {
+ cwd: viteResult.tmp,
+ args: ["serve", "-A", "--port", "0", "_fresh/server.js"],
+ },
+ async (address) => {
+ const res = await fetch(`${address}/test_static/foo.txt`);
+ const text = await res.text();
+ expect(text).toEqual("it works");
+ },
+ );
+ },
+ sanitizeOps: false,
+ sanitizeResources: false,
+});
+
+Deno.test({
+ name: "vite build - loads islands",
+ fn: async () => {
+ await using res = await buildVite();
+
+ await withChildProcessServer(
+ {
+ cwd: res.tmp,
+ args: ["serve", "-A", "--port", "0", "_fresh/server.js"],
+ },
+ async (address) => {
+ await withBrowser(async (page) => {
+ await page.goto(`${address}/tests/island_hooks`, {
+ waitUntil: "networkidle2",
+ });
+
+ await waitForText(page, "button", "count: 0");
+
+ await page.locator("button").click();
+ await waitForText(page, "button", "count: 1");
+ });
+ },
+ );
+ },
+ sanitizeOps: false,
+ sanitizeResources: false,
+});
diff --git a/plugin-vite/tests/dev_server_test.ts b/plugin-vite/tests/dev_server_test.ts
new file mode 100644
index 00000000000..c5bf1b117d5
--- /dev/null
+++ b/plugin-vite/tests/dev_server_test.ts
@@ -0,0 +1,83 @@
+import * as path from "@std/path";
+import { expect } from "@std/expect";
+import { waitForText, withBrowser } from "../../tests/test_utils.tsx";
+import { updateFile, withDevServer } from "./test_utils.ts";
+
+Deno.test({
+ name: "vite dev - launches",
+ fn: async () => {
+ await withDevServer(async (address) => {
+ const res = await fetch(`${address}/tests/it_works`);
+ const text = await res.text();
+ expect(text).toContain("it works");
+ });
+ },
+ sanitizeResources: false,
+ sanitizeOps: false,
+});
+
+Deno.test({
+ name: "vite dev - serves static files",
+ fn: async () => {
+ await withDevServer(async (address) => {
+ const res = await fetch(`${address}/test_static/foo.txt`);
+ const text = await res.text();
+ expect(text).toContain("it works");
+ });
+ },
+ sanitizeResources: false,
+ sanitizeOps: false,
+});
+
+Deno.test({
+ name: "vite dev - loads islands",
+ fn: async () => {
+ await withDevServer(async (address) => {
+ await withBrowser(async (page) => {
+ await page.goto(`${address}/tests/island_hooks`, {
+ waitUntil: "networkidle2",
+ });
+ await waitForText(page, "button", "count: 0");
+
+ await page.locator("button").click();
+ await waitForText(page, "button", "count: 1");
+ });
+ });
+ },
+ sanitizeResources: false,
+ sanitizeOps: false,
+});
+
+Deno.test({
+ name: "vite dev - can apply HMR to islands (hooks)",
+ ignore: true, // Test is very flaky
+ fn: async () => {
+ await withDevServer(async (address, dir) => {
+ await withBrowser(async (page) => {
+ await page.goto(`${address}/tests/island_hooks`, {
+ waitUntil: "networkidle2",
+ });
+ await waitForText(page, "button", "count: 0");
+ await page.locator("button").click();
+ await waitForText(page, "button", "count: 1");
+
+ const island = path.join(
+ dir,
+ "islands",
+ "tests",
+ "CounterHooks.tsx",
+ );
+ await using _ = await updateFile(
+ island,
+ (text) => text.replace("count:", "hmr:"),
+ );
+
+ await waitForText(page, "button", "hmr: 1");
+ await page.locator("button").click();
+ await waitForText(page, "button", "hmr: 2");
+ });
+ });
+ },
+ sanitizeResources: false,
+ sanitizeOps: false,
+});
diff --git a/plugin-vite/tests/test_utils.ts b/plugin-vite/tests/test_utils.ts
new file mode 100644
index 00000000000..3fa6dc454a2
--- /dev/null
+++ b/plugin-vite/tests/test_utils.ts
@@ -0,0 +1,110 @@
+import { createBuilder } from "vite";
+import * as path from "@std/path";
+import { walk } from "@std/fs/walk";
+import { withTmpDir } from "../../src/test_utils.ts";
+import { withChildProcessServer } from "../../tests/test_utils.tsx";
+
+export const DEMO_DIR = path.join(import.meta.dirname!, "..", "demo");
+
+export async function updateFile(
+ filePath: string,
+ fn: (text: string) => string | Promise,
+) {
+ const original = await Deno.readTextFile(filePath);
+ const result = await fn(original);
+ await Deno.writeTextFile(filePath, result);
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await Deno.writeTextFile(filePath, original);
+ },
+ };
+}
+
+async function copyDir(from: string, to: string) {
+ const entries = walk(from, {
+ includeFiles: true,
+ includeDirs: false,
+ skip: [/([\\/]+(_fresh|node_modules|vendor)[\\/]+|[\\/]+vite\.config\.ts)/],
+ });
+
+ for await (const entry of entries) {
+ if (entry.isFile) {
+ const relative = path.relative(from, entry.path);
+ const target = path.join(to, relative);
+
+ try {
+ await Deno.mkdir(path.dirname(target), { recursive: true });
+ } catch (err) {
+ if (!(err instanceof Deno.errors.NotFound)) {
+ throw err;
+ }
+ }
+
+ await Deno.copyFile(entry.path, target);
+ }
+ }
+}
+
+export async function withDevServer(
+ fn: (address: string, dir: string) => void | Promise,
+) {
+ await using tmp = await withTmpDir({
+ dir: path.join(import.meta.dirname!, ".."),
+ prefix: "tmp_vite_",
+ });
+
+ await copyDir(DEMO_DIR, tmp.dir);
+
+ await Deno.writeTextFile(
+ path.join(tmp.dir, "vite.config.ts"),
+ `import { defineConfig } from "vite";
+import { fresh } from "@fresh/plugin-vite";
+
+export default defineConfig({
+ plugins: [
+ fresh(),
+ ],
+});
+`,
+ );
+
+ await withChildProcessServer(
+ { cwd: tmp.dir, args: ["run", "-A", "npm:vite", "--port", "0"] },
+ async (address) => await fn(address, tmp.dir),
+ );
+}
+
+export async function buildVite() {
+ const tmp = await withTmpDir({
+ dir: path.join(import.meta.dirname!, ".."),
+ prefix: "tmp_vite_",
+ });
+
+ const builder = await createBuilder({
+ root: DEMO_DIR,
+ build: {
+ emptyOutDir: true,
+ },
+ environments: {
+ ssr: {
+ build: {
+ outDir: path.join(tmp.dir, "_fresh", "server"),
+ },
+ },
+ client: {
+ build: {
+ outDir: path.join(tmp.dir, "_fresh", "client"),
+ },
+ },
+ },
+ });
+ await builder.buildApp();
+
+ return {
+ tmp: tmp.dir,
+ async [Symbol.asyncDispose]() {
+ return await tmp[Symbol.asyncDispose]();
+ },
+ };
+}
diff --git a/src/app.ts b/src/app.ts
index 880c3fc6639..03653ca8fc9 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,6 +1,6 @@
import { trace } from "@opentelemetry/api";
-import { DENO_DEPLOYMENT_ID } from "./runtime/build_id.ts";
+import { DENO_DEPLOYMENT_ID } from "fresh/build-id";
import * as colors from "@std/fmt/colors";
import {
type MaybeLazyMiddleware,
@@ -152,6 +152,7 @@ export let getBuildCache: (app: App) => BuildCache | null;
export let setBuildCache: (
app: App,
cache: BuildCache,
+ mode: "development" | "production",
) => void;
/**
@@ -164,8 +165,9 @@ export class App {
static {
getBuildCache = (app) => app.#getBuildCache();
- setBuildCache = (app, cache) => {
+ setBuildCache = (app, cache, mode: "development" | "production") => {
app.config.root = cache.root;
+ app.config.mode = mode;
app.#getBuildCache = () => cache;
};
}
@@ -179,7 +181,7 @@ export class App {
this.config = {
root: Deno.cwd(),
basePath: config.basePath ?? "",
- mode: "production",
+ mode: config.mode ?? "production",
};
}
@@ -363,7 +365,7 @@ export class App {
`Could not find _fresh directory. Maybe you forgot to run "deno task build"?`,
);
} else {
- buildCache = new MockBuildCache([]);
+ buildCache = new MockBuildCache([], this.config.mode);
}
}
diff --git a/src/build_cache.ts b/src/build_cache.ts
index de7075088f4..957d1c1ad13 100644
--- a/src/build_cache.ts
+++ b/src/build_cache.ts
@@ -1,10 +1,10 @@
import * as path from "@std/path";
-import { setBuildId } from "./runtime/build_id.ts";
import type { Command } from "./commands.ts";
import { fsItemsToCommands, type FsRouteFile } from "./fs_routes.ts";
import type { ServerIslandRegistry } from "./context.ts";
import type { AnyComponent } from "preact";
import { UniqueNamer } from "./utils.ts";
+import { setBuildId } from "fresh/build-id";
export interface FileSnapshot {
name: string;
@@ -15,6 +15,7 @@ export interface FileSnapshot {
export interface BuildSnapshot {
version: string;
+ clientEntry: string;
fsRoutes: FsRouteFile[];
staticFiles: Map;
islands: ServerIslandRegistry;
@@ -32,6 +33,10 @@ export interface StaticFile {
export interface BuildCache {
root: string;
islandRegistry: ServerIslandRegistry;
+ clientEntry: string;
+ features: {
+ errorOverlay: boolean;
+ };
getFsRoutes(): Command[];
readFile(pathname: string): Promise;
}
@@ -39,11 +44,14 @@ export interface BuildCache {
export class ProdBuildCache implements BuildCache {
#snapshot: BuildSnapshot;
islandRegistry: ServerIslandRegistry;
+ clientEntry: string;
+ features = { errorOverlay: false };
constructor(public root: string, snapshot: BuildSnapshot) {
setBuildId(snapshot.version);
this.#snapshot = snapshot;
this.islandRegistry = snapshot.islands;
+ this.clientEntry = snapshot.clientEntry;
}
getFsRoutes(): Command[] {
@@ -56,7 +64,9 @@ export class ProdBuildCache implements BuildCache {
const info = staticFiles.get(pathname);
if (info === undefined) return null;
- const filePath = path.join(this.root, info.filePath);
+ const filePath = path.isAbsolute(info.filePath)
+ ? info.filePath
+ : path.join(this.root, info.filePath);
const [stat, file] = await Promise.all([
Deno.stat(filePath),
diff --git a/src/config.ts b/src/config.ts
index 8a312cd57da..7fae4f086e9 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -7,6 +7,10 @@ export interface FreshConfig {
* @default undefined
*/
basePath?: string;
+ /**
+ * The mode Fresh can run in.
+ */
+ mode?: "development" | "production";
}
/**
diff --git a/src/context.ts b/src/context.ts
index a829ef4a1be..fdaabe0dd91 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -296,15 +296,3 @@ export class Context {
return new Response(html, responseInit);
}
}
-
-Object.defineProperties(Context.prototype, {
- config: { enumerable: true },
- url: { enumerable: true },
- req: { enumerable: true },
- params: { enumerable: true },
- state: { enumerable: true },
- data: { enumerable: true },
- error: { enumerable: true },
- next: { enumerable: true },
- info: { enumerable: true },
-});
diff --git a/src/context_test.tsx b/src/context_test.tsx
index 6bc6ff052b3..6e4c717d700 100644
--- a/src/context_test.tsx
+++ b/src/context_test.tsx
@@ -3,7 +3,7 @@ import { Context } from "./context.ts";
import { App } from "fresh";
import { asset } from "fresh/runtime";
import { FakeServer } from "./test_utils.ts";
-import { BUILD_ID } from "./runtime/build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import { parseHtml } from "../tests/test_utils.tsx";
Deno.test("FreshReqContext.prototype.redirect", () => {
diff --git a/src/dev/builder.ts b/src/dev/builder.ts
index 08bc3634107..ac20e850a1f 100644
--- a/src/dev/builder.ts
+++ b/src/dev/builder.ts
@@ -24,7 +24,7 @@ import { automaticWorkspaceFolders } from "./middlewares/automatic_workspace_fol
import { parseDirPath } from "../config.ts";
import { pathToExportName, UniqueNamer } from "../utils.ts";
import { checkDenoCompilerOptions } from "./check.ts";
-import { crawlRouteDir, walkDir } from "./fs_crawl.ts";
+import { crawlFsItem } from "./fs_crawl.ts";
import { DAY } from "../constants.ts";
export interface BuildOptions {
@@ -169,7 +169,7 @@ export class Builder {
app.config.root = this.config.root;
app.config.mode = "development";
- setBuildCache(app, buildCache);
+ setBuildCache(app, buildCache, "development");
const appHandler = app.handler();
@@ -187,7 +187,7 @@ export class Builder {
devApp.config.root = this.config.root;
devApp.config.mode = "development";
- setBuildCache(devApp, buildCache);
+ setBuildCache(devApp, buildCache, "development");
// Boot in parallel to spin up the server quicker. We'll hold
// requests until the required assets are processed.
@@ -248,26 +248,24 @@ export class Builder {
await buildCache.prepare();
return (app) => {
- setBuildCache(app, buildCache);
+ setBuildCache(app, buildCache, app.config.mode);
};
}
async #crawlFsItems() {
- await Promise.all([
- walkDir(
- fsAdapter,
- this.config.islandDir,
- (entry) => this.registerIsland(entry.path),
- this.config.ignore,
- ),
- crawlRouteDir(
- fsAdapter,
- this.config.routeDir,
- this.config.ignore,
- (entry) => this.registerIsland(entry),
- this.#fsRoutes.files,
- ),
- ]);
+ const { islands, routes } = await crawlFsItem(
+ {
+ islandDir: this.config.islandDir,
+ routeDir: this.config.routeDir,
+ ignore: this.config.ignore,
+ },
+ );
+
+ for (let i = 0; i < islands.length; i++) {
+ this.registerIsland(islands[i]);
+ }
+
+ this.#fsRoutes.files = routes;
}
async #build(buildCache: DevBuildCache, dev: boolean): Promise {
diff --git a/src/dev/builder_test.ts b/src/dev/builder_test.ts
index 84d15844ec8..e0edd0fb1c0 100644
--- a/src/dev/builder_test.ts
+++ b/src/dev/builder_test.ts
@@ -3,7 +3,7 @@ import * as path from "@std/path";
import { Builder, specToName } from "./builder.ts";
import { App } from "../app.ts";
import { DEV_ERROR_OVERLAY_URL } from "../constants.ts";
-import { BUILD_ID } from "../runtime/build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import { withTmpDir, writeFiles } from "../test_utils.ts";
import {
getStdOutput,
@@ -68,7 +68,7 @@ Deno.test({
await builder.build();
const snapshotJson = await Deno.readTextFile(
- path.join(tmp, "dist", "static-files.json"),
+ path.join(tmp, "dist", "snapshot.js"),
);
expect(snapshotJson).toContain("/images/batman.svg");
},
diff --git a/src/dev/dev_build_cache.ts b/src/dev/dev_build_cache.ts
index b6ed607d6a3..f74bdc80711 100644
--- a/src/dev/dev_build_cache.ts
+++ b/src/dev/dev_build_cache.ts
@@ -1,11 +1,10 @@
import {
type BuildCache,
- type FileSnapshot,
IslandPreparer,
type StaticFile,
} from "../build_cache.ts";
import * as path from "@std/path";
-import { SEPARATOR as WINDOWS_SEPARATOR } from "@std/path/windows/constants";
+import * as pathWin32 from "@std/path/windows";
import { encodeHex } from "@std/encoding/hex";
import { fsAdapter } from "../fs.ts";
import type { FileTransformer } from "./file_transformer.ts";
@@ -16,6 +15,8 @@ import type { Command } from "../commands.ts";
import type { ServerIslandRegistry } from "../context.ts";
import { contentType as getStdContentType } from "@std/media-types/content-type";
+const WINDOWS_SEPARATOR = pathWin32.SEPARATOR;
+
export interface MemoryFile {
hash: string | null;
contentType: string;
@@ -60,16 +61,24 @@ export class MemoryBuildCache implements DevBuildCache {
#commands: Command[] = [];
root: string;
islandRegistry: ServerIslandRegistry = new Map();
+ clientEntry: string;
+ features = { errorOverlay: false };
constructor(
config: ResolvedBuildConfig,
fsRoutes: FsRoute,
transformer: FileTransformer,
) {
+ if (config.mode === "development") {
+ this.features.errorOverlay = true;
+ }
+
this.#config = config;
this.#fsRoutes = fsRoutes;
this.#transformer = transformer;
this.root = config.root;
+
+ this.clientEntry = getClientEntry(config.buildId);
}
getFsRoutes(): Command[] {
@@ -221,12 +230,17 @@ export class DiskBuildCache implements DevBuildCache {
#fsRoutes: FsRoute;
root: string;
islandRegistry: ServerIslandRegistry = new Map();
+ clientEntry: string = "";
+ features = { errorOverlay: false };
constructor(
config: ResolvedBuildConfig,
fsRoutes: FsRoute,
transformer: FileTransformer,
) {
+ if (config.mode === "development") {
+ this.features.errorOverlay = true;
+ }
this.#fsRoutes = fsRoutes;
this.#transformer = transformer;
this.#config = config;
@@ -309,150 +323,54 @@ export class DiskBuildCache implements DevBuildCache {
}
}
- const staticFiles = new Map();
+ const staticFiles: PendingStaticFile[] = [];
for (const [name, filePath] of this.#unprocessedFiles.entries()) {
- const file = await Deno.open(filePath);
- const hash = await hashContent(file.readable);
-
- staticFiles.set(name, {
- name,
- hash,
- filePath: path.relative(root, filePath),
- contentType: getContentType(filePath),
- });
+ staticFiles.push({ filePath, pathname: name, hash: null });
}
for (const [name, maybeHash] of this.#processedFiles.entries()) {
- let hash = maybeHash;
-
// Ignore esbuild meta file. It's not intended for serving
if (name === "/metafile.json") {
continue;
}
const filePath = path.join(outDir, "static", name);
- if (maybeHash === null) {
- const file = await Deno.open(filePath);
- hash = await hashContent(file.readable);
- }
-
- staticFiles.set(name, {
- name,
- hash,
- filePath: path.relative(root, filePath),
- contentType: getContentType(filePath),
- });
+ staticFiles.push({ filePath, pathname: name, hash: maybeHash });
}
- await Deno.writeTextFile(
- path.join(outDir, "static-files.json"),
- JSON.stringify(Array.from(staticFiles.values()), null, 2),
- );
-
const islandSpecifiers: string[] = [];
for (const spec of this.islandModNameToChunk.keys()) {
islandSpecifiers.push(spec);
}
- const editWarning =
- `// WARNING: DO NOT EDIT THIS FILE. It is autogenerated by Fresh.`;
-
const islands = Array.from(this.islandModNameToChunk.values());
await Deno.writeTextFile(
path.join(outDir, "snapshot.js"),
- `${editWarning}
-
-import { IslandPreparer } from "fresh/do-not-use";
-import staticFileData from "./static-files.json" with { type: "json" };
-
-// Import islands
-${
- islands
- .map((item) => {
- const spec = pathToSpec(outDir, item.server);
- return `import * as ${item.name} from "${spec}";`;
- })
- .join("\n")
- }
-
-// Import routes
-${
- this.#fsRoutes.files
- .map((item, i) => {
- if (item.lazy) return null;
- const spec = pathToSpec(outDir, item.filePath);
- return `import * as fsRoute_${i} from "${spec}"`;
- })
- .filter(Boolean)
- .join("\n")
- }
-
-export const version = ${JSON.stringify(this.#config.buildId)};
-
-const prefix = \`/_fresh/js/\${version}\`;
-
-export const islands = new Map();
-const islandPreparer = new IslandPreparer();
-${
- islands.map((item) => {
- // Strip prefix
- const prefix = `/_fresh/js/${this.#config.buildId}`;
- const chunkName = item.browser
- ? item.browser.slice(prefix.length)
- : item.browser;
- return `islandPreparer.prepare(islands, ${item.name}, \`\${prefix}${chunkName}\`, ${
- JSON.stringify(item.name)
- });`;
- }).join("\n")
- }
-
-export const staticFiles = new Map();
-for (let i = 0; i < staticFileData.length; i++) {
- const data = staticFileData[i];
- staticFiles.set(data.name, data);
-}
-
-export const fsRoutes = [
-${
- this.#fsRoutes.files
- .map((item, i) => {
- const id = JSON.stringify(item.id);
- const pattern = JSON.stringify(item.pattern);
- const type = JSON.stringify(item.type);
- const routePattern = JSON.stringify(item.routePattern);
-
- let mod = "";
- if (item.lazy) {
- const spec = pathToSpec(outDir, item.filePath);
- mod = `() => import(${JSON.stringify(spec)})`;
- } else {
- mod = `fsRoute_${i}`;
- }
-
- return ` { id: ${id}, mod: ${mod}, type: ${type}, pattern: ${pattern}, routePattern: ${routePattern} },`;
- })
- .join("\n")
- }
-];
-`,
+ await generateSnapshotServer({
+ buildId: this.#config.buildId,
+ clientEntry: getClientEntry(this.#config.buildId),
+ staticFiles,
+ islands,
+ writeSpecifier: (filePath) => {
+ return pathToSpec(outDir, filePath);
+ },
+ fsRoutesFiles: this.#fsRoutes.files,
+ outDir: root,
+ }),
);
// TODO: Make main file configurable
const appPath = path.relative(outDir, root);
await Deno.writeTextFile(
path.join(outDir, "server.js"),
- `${editWarning}
-import { setBuildCache, ProdBuildCache, path } from "fresh/do-not-use";
-import * as snapshot from "./snapshot.js";
-import { app } from "${appPath}/main.ts";
-
-const root = path.join(import.meta.dirname, ${JSON.stringify(appPath)});
-setBuildCache(app, new ProdBuildCache(root, snapshot));
-
-export default {
- fetch: app.handler()
-}`,
+ generateServerEntry({
+ root: appPath,
+ serverEntry: path
+ .join(appPath, "main.ts")
+ .replaceAll(/[\\/]+/g, "/"),
+ snapshotSpecifier: "./snapshot.js",
+ }),
);
await Deno.writeTextFile(
@@ -467,7 +385,10 @@ Deno.serve(
}
}
-async function hashContent(
+const EDIT_WARNING =
+ `// WARNING: DO NOT EDIT THIS FILE. It is autogenerated by Fresh.`;
+
+export async function hashContent(
content: Uint8Array | ReadableStream,
): Promise {
const buffer = await new Response(content).arrayBuffer();
@@ -487,3 +408,149 @@ export function getContentType(filePath: string): string {
function maybeToFileUrl(file: string) {
return file.startsWith("file://") ? file : path.toFileUrl(file).href;
}
+
+export interface PendingStaticFile {
+ pathname: string;
+ filePath: string;
+ hash: string | null;
+}
+
+export async function generateSnapshotServer(
+ options: {
+ outDir: string;
+ buildId: string;
+ clientEntry: string;
+ islands: IslandModChunk[];
+ // deno-lint-ignore no-explicit-any
+ fsRoutesFiles: FsRouteFileNoMod[];
+ staticFiles: PendingStaticFile[];
+ writeSpecifier: (filePath: string) => string;
+ },
+): Promise {
+ const {
+ islands,
+ writeSpecifier,
+ fsRoutesFiles,
+ outDir,
+ } = options;
+
+ const islandImports = islands
+ .map((item) => {
+ const spec = writeSpecifier(item.server);
+ return `import * as ${item.name} from "${spec}";`;
+ })
+ .join("\n");
+
+ const fsRouteImports = fsRoutesFiles
+ .map((item, i) => {
+ if (item.lazy) return null;
+ const spec = writeSpecifier(item.filePath);
+ return `import * as fsRoute_${i} from "${spec}"`;
+ })
+ .filter(Boolean)
+ .join("\n");
+
+ const islandMarkers = islands.map((item) => {
+ const browser = JSON.stringify(item.browser);
+ const name = JSON.stringify(item.name);
+ return `islandPreparer.prepare(islands, ${item.name}, ${browser}, ${name});`;
+ }).join("\n");
+
+ const serializedFsRoutes = fsRoutesFiles
+ .map((item, i) => {
+ const id = JSON.stringify(item.id);
+ const pattern = JSON.stringify(item.pattern);
+ const type = JSON.stringify(item.type);
+ const routePattern = JSON.stringify(item.routePattern);
+
+ let mod = "";
+ if (item.lazy) {
+ const spec = writeSpecifier(item.filePath);
+ mod = `() => import(${JSON.stringify(spec)})`;
+ } else {
+ mod = `fsRoute_${i}`;
+ }
+
+ return ` { id: ${id}, mod: ${mod}, type: ${type}, pattern: ${pattern}, routePattern: ${routePattern} },`;
+ })
+ .join("\n");
+
+ const staticFiles = await Promise.all(
+ options.staticFiles.map(async (item) => {
+ const file = await Deno.open(item.filePath);
+ const hash = item.hash ? item.hash : await hashContent(file.readable);
+ const url = new URL(item.pathname, "http://localhost");
+
+ return {
+ name: url.pathname,
+ hash,
+ filePath: path.isAbsolute(item.filePath)
+ ? path.relative(outDir, item.filePath)
+ : item.filePath,
+ contentType: getContentType(item.filePath),
+ };
+ }),
+ );
+
+ return `${EDIT_WARNING}
+import { IslandPreparer } from "fresh/internal";
+${islandImports}
+${fsRouteImports}
+
+export const clientEntry = ${JSON.stringify(options.clientEntry)}
+export const version = ${JSON.stringify(options.buildId)}
+
+export const islands = new Map();
+const islandPreparer = new IslandPreparer();
+${islandMarkers}
+
+export const staticFiles = new Map([
+${
+ staticFiles.map((def) =>
+ ` [${JSON.stringify(def.name)}, ${JSON.stringify(def)}]`
+ ).join(",\n")
+ }
+]);
+
+export const fsRoutes = [
+${serializedFsRoutes}
+];
+`.replaceAll(/\n[\n]+/g, "\n\n");
+}
+
+export function generateServerEntry(
+ options: {
+ root: string;
+ serverEntry: string;
+ snapshotSpecifier: string;
+ },
+): string {
+ let rootPath = `path.join(import.meta.dirname, ${
+ JSON.stringify(options.root)
+ })`;
+ if (path.isAbsolute(options.root)) {
+ // deno-lint-ignore no-console
+ console.warn(
+ `WARN: using absolute root path in snapshot: "${options.root}"`,
+ );
+
+ rootPath = JSON.stringify(options.root);
+ }
+
+ return `${EDIT_WARNING}
+import { setBuildCache, ProdBuildCache, path } from "fresh/internal";
+import * as snapshot from "${options.snapshotSpecifier}";
+import { app } from "${options.serverEntry}";
+
+const root = ${rootPath};
+setBuildCache(app, new ProdBuildCache(root, snapshot), "production");
+
+export default {
+ fetch: app.handler()
+};
+`;
+}
+
+function getClientEntry(buildId: string) {
+ return `/_fresh/js/${buildId}/fresh-runtime.js`;
+}
diff --git a/src/dev/esbuild.ts b/src/dev/esbuild.ts
index 496972aa4c5..eec6289d821 100644
--- a/src/dev/esbuild.ts
+++ b/src/dev/esbuild.ts
@@ -166,14 +166,14 @@ function buildIdPlugin(buildId: string): EsbuildPlugin {
return {
name: "fresh-build-id",
setup(build) {
- build.onResolve({ filter: /[/\\]+build_id\.ts$/ }, (args) => {
+ build.onResolve({ filter: /^fresh\/build-id$/ }, (args) => {
return {
path: args.path,
namespace: "fresh-internal",
};
});
build.onLoad({
- filter: /[/\\]build_id\.ts$/,
+ filter: /^fresh\/build-id$/,
namespace: "fresh-internal",
}, () => {
return {
diff --git a/src/dev/fs_crawl.ts b/src/dev/fs_crawl.ts
index 9cc9832ff6a..126358af1b0 100644
--- a/src/dev/fs_crawl.ts
+++ b/src/dev/fs_crawl.ts
@@ -1,4 +1,4 @@
-import type { FsAdapter } from "../fs.ts";
+import { type FsAdapter, fsAdapter } from "../fs.ts";
import type { WalkEntry } from "@std/fs/walk";
import type { FsRouteFileNoMod } from "./dev_build_cache.ts";
import * as path from "@std/path";
@@ -14,92 +14,90 @@ export async function crawlRouteDir(
routeDir: string,
ignore: RegExp[],
onIslandSpecifier: (spec: string) => void,
- files: FsRouteFileNoMod[],
-) {
- await walkDir(
- fs,
- routeDir,
- async (entry) => {
- // A `(_islands)` path segment is a local island folder.
- // Any route path segment wrapped in `(_...)` is ignored
- // during route collection.
- const match = entry.path.match(GROUP_REG);
- if (match !== null) {
- if (match[1] === "_islands") {
- onIslandSpecifier(entry.path);
- }
- return;
+): Promise[]> {
+ const files: FsRouteFileNoMod[] = [];
+
+ await walkDir(fs, routeDir, async (entry) => {
+ // A `(_islands)` path segment is a local island folder.
+ // Any route path segment wrapped in `(_...)` is ignored
+ // during route collection.
+ const match = entry.path.match(GROUP_REG);
+ if (match !== null) {
+ if (match[1] === "_islands") {
+ onIslandSpecifier(entry.path);
}
+ return;
+ }
- let lazy = false;
- const relative = path.relative(routeDir, entry.path);
- const url = new URL(relative, "http://localhost/");
- const id = url.pathname.slice(0, url.pathname.lastIndexOf("."));
-
- let overrideConfig: RouteConfig | undefined;
- let pattern = "*";
- let routePattern = pattern;
- let type = CommandType.Route;
- if (id.endsWith("/_middleware")) {
- type = CommandType.Middleware;
- pattern = pathToPattern(
- id.slice(1, -"/_middleware".length),
- { keepGroups: true },
- );
- routePattern = pattern;
- } else if (id.endsWith("/_layout")) {
- type = CommandType.Layout;
- pattern = pathToPattern(
- id.slice(1, -"/_layout".length),
- { keepGroups: true },
- );
- routePattern = pattern;
- } else if (id.endsWith("/_app")) {
- type = CommandType.App;
- } else if (id.endsWith("/_404")) {
- type = CommandType.NotFound;
- } else if (id.endsWith("/_error") || id.endsWith("/_500")) {
- type = CommandType.Error;
- pattern = pathToPattern(
- id.slice(1, -"/_error".length),
- { keepGroups: true },
- );
- routePattern = pattern;
- } else {
- pattern = pathToPattern(id.slice(1), { keepGroups: true });
- if (id.endsWith("/index")) {
- if (!pattern.endsWith("/")) {
- pattern += "/";
- }
+ let lazy = false;
+ const relative = path.relative(routeDir, entry.path);
+ const url = new URL(relative, "http://localhost/");
+ const id = url.pathname.slice(0, url.pathname.lastIndexOf("."));
+
+ let overrideConfig: RouteConfig | undefined;
+ let pattern = "*";
+ let routePattern = pattern;
+ let type = CommandType.Route;
+ if (id.endsWith("/_middleware")) {
+ type = CommandType.Middleware;
+ pattern = pathToPattern(
+ id.slice(1, -"/_middleware".length),
+ { keepGroups: true },
+ );
+ routePattern = pattern;
+ } else if (id.endsWith("/_layout")) {
+ type = CommandType.Layout;
+ pattern = pathToPattern(
+ id.slice(1, -"/_layout".length),
+ { keepGroups: true },
+ );
+ routePattern = pattern;
+ } else if (id.endsWith("/_app")) {
+ type = CommandType.App;
+ } else if (id.endsWith("/_404")) {
+ type = CommandType.NotFound;
+ } else if (id.endsWith("/_error") || id.endsWith("/_500")) {
+ type = CommandType.Error;
+ pattern = pathToPattern(
+ id.slice(1, -"/_error".length),
+ { keepGroups: true },
+ );
+ routePattern = pattern;
+ } else {
+ pattern = pathToPattern(id.slice(1), { keepGroups: true });
+ if (id.endsWith("/index")) {
+ if (!pattern.endsWith("/")) {
+ pattern += "/";
}
+ }
- routePattern = pathToPattern(id.slice(1));
+ routePattern = pathToPattern(id.slice(1));
- const code = await fs.readTextFile(entry.path);
- lazy = !code.includes("routeOverride");
+ const code = await fs.readTextFile(entry.path);
+ lazy = !code.includes("routeOverride");
- // TODO: We could do an AST parse here to detect the
- // kind of handler that's used to get a more accurate
- // list of methods this route supports.
- overrideConfig = {
- methods: "ALL",
- };
- }
+ // TODO: We could do an AST parse here to detect the
+ // kind of handler that's used to get a more accurate
+ // list of methods this route supports.
+ overrideConfig = {
+ methods: "ALL",
+ };
+ }
- files.push({
- id,
- filePath: entry.path,
- type,
- pattern,
- routePattern,
- lazy,
- overrideConfig,
- });
- },
- ignore,
- );
+ files.push({
+ id,
+ filePath: entry.path,
+ type,
+ pattern,
+ routePattern,
+ lazy,
+ overrideConfig,
+ });
+ }, ignore);
files.sort((a, b) => sortRoutePaths(a.id, b.id));
+
+ return files;
}
export async function walkDir(
@@ -121,3 +119,28 @@ export async function walkDir(
await callback(entry);
}
}
+
+export async function crawlFsItem(
+ options: { islandDir: string; routeDir: string; ignore: RegExp[] },
+): Promise<{ islands: string[]; routes: FsRouteFileNoMod[] }> {
+ const islands: string[] = [];
+
+ const [, routes] = await Promise.all([
+ walkDir(
+ fsAdapter,
+ options.islandDir,
+ (entry) => {
+ islands.push(entry.path);
+ },
+ options.ignore,
+ ),
+ crawlRouteDir(
+ fsAdapter,
+ options.routeDir,
+ options.ignore,
+ (entry) => islands.push(entry),
+ ),
+ ]);
+
+ return { islands, routes };
+}
diff --git a/src/dev/middlewares/error_overlay/code_frame.tsx b/src/dev/middlewares/error_overlay/code_frame.tsx
index 203a512aa7c..047031d5424 100644
--- a/src/dev/middlewares/error_overlay/code_frame.tsx
+++ b/src/dev/middlewares/error_overlay/code_frame.tsx
@@ -89,7 +89,7 @@ function getFirstUserFile(
const lines = stack.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(STACK_FRAME);
- if (match && match) {
+ if (match) {
const fnName = match[1] ?? "";
const file = match[2];
const line = +match[3];
diff --git a/src/dev/middlewares/error_overlay/middleware_test.tsx b/src/dev/middlewares/error_overlay/middleware_test.tsx
index ba72b8c3036..8e7a2d011be 100644
--- a/src/dev/middlewares/error_overlay/middleware_test.tsx
+++ b/src/dev/middlewares/error_overlay/middleware_test.tsx
@@ -86,9 +86,8 @@ Deno.test(
);
Deno.test("error overlay - should not be visible in prod", async () => {
- const app = new App();
+ const app = new App({ mode: "production" });
app.use(devErrorOverlay());
- app.config.mode = "production";
app.get("/", () => {
throw new HttpError(500);
diff --git a/src/fs_routes_test.tsx b/src/fs_routes_test.tsx
index b76c14c85bd..1332d0da760 100644
--- a/src/fs_routes_test.tsx
+++ b/src/fs_routes_test.tsx
@@ -9,7 +9,6 @@ import type { Method } from "./router.ts";
import { parseHtml } from "../tests/test_utils.tsx";
import type { Context } from "./context.ts";
import { HttpError } from "./error.ts";
-import type { FsRouteFileNoMod } from "./dev/dev_build_cache.ts";
import { crawlRouteDir } from "./dev/fs_crawl.ts";
import * as path from "@std/path";
@@ -19,8 +18,7 @@ async function createServer(
const fs = createFakeFs(files);
const routeDir = path.join(fs.cwd(), "routes");
- const rawFiles: FsRouteFileNoMod[] = [];
- await crawlRouteDir(fs, routeDir, [], () => {}, rawFiles);
+ const rawFiles = await crawlRouteDir(fs, routeDir, [], () => {});
const fsFiles = rawFiles.map((file) => {
// deno-lint-ignore no-explicit-any
@@ -30,8 +28,8 @@ async function createServer(
const app = new App()
.fsRoutes();
- const buildCache = new MockBuildCache(fsFiles);
- setBuildCache(app, buildCache);
+ const buildCache = new MockBuildCache(fsFiles, "development");
+ setBuildCache(app, buildCache, "development");
return new FakeServer(app.handler());
}
@@ -229,7 +227,9 @@ Deno.test("fsRoutes - middleware array", async () => {
return ctx.next();
},
},
- "routes/foo/index.ts": { default: (ctx) => {ctx.state.text}
},
+ "routes/foo/index.ts": {
+ default: (ctx) => {ctx.state.text}
,
+ },
});
const res = await server.get("/foo");
diff --git a/src/internals_dev.ts b/src/internals_dev.ts
new file mode 100644
index 00000000000..27ba8825bb2
--- /dev/null
+++ b/src/internals_dev.ts
@@ -0,0 +1,11 @@
+export { crawlFsItem } from "./dev/fs_crawl.ts";
+export { type FsAdapter, fsAdapter } from "./fs.ts";
+export {
+ type FsRouteFileNoMod,
+ generateServerEntry,
+ generateSnapshotServer,
+ type IslandModChunk,
+ type PendingStaticFile,
+} from "./dev/dev_build_cache.ts";
+export { specToName } from "./dev/builder.ts";
+export { pathToSpec, UniqueNamer } from "./utils.ts";
diff --git a/src/middlewares/static_files.ts b/src/middlewares/static_files.ts
index 57899ff799b..370b9c29b20 100644
--- a/src/middlewares/static_files.ts
+++ b/src/middlewares/static_files.ts
@@ -1,6 +1,6 @@
import type { Middleware } from "./mod.ts";
import { ASSET_CACHE_BUST_KEY } from "../runtime/shared_internal.tsx";
-import { BUILD_ID } from "../runtime/build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import { tracer } from "../otel.ts";
import { getBuildCache } from "../context.ts";
diff --git a/src/middlewares/static_files_test.ts b/src/middlewares/static_files_test.ts
index a85ba3b2cff..eec3d60c82e 100644
--- a/src/middlewares/static_files_test.ts
+++ b/src/middlewares/static_files_test.ts
@@ -3,7 +3,7 @@ import { serveMiddleware } from "../test_utils.ts";
import type { BuildCache, StaticFile } from "../build_cache.ts";
import { expect } from "@std/expect";
import { ASSET_CACHE_BUST_KEY } from "../runtime/shared_internal.tsx";
-import { BUILD_ID } from "../runtime/build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import type { Command } from "../commands.ts";
import type { ServerIslandRegistry } from "../context.ts";
import { getContentType } from "../dev/dev_build_cache.ts";
@@ -13,6 +13,8 @@ class MockBuildCache implements BuildCache {
buildId = "MockId";
files = new Map();
islandRegistry: ServerIslandRegistry = new Map();
+ clientEntry = "";
+ features = { errorOverlay: false };
constructor(files: Record) {
const encoder = new TextEncoder();
diff --git a/src/render.ts b/src/render.ts
index d5000898c72..5588492d8c4 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -14,7 +14,6 @@ import type { Context } from "./context.ts";
import { recordSpanError, tracer } from "./otel.ts";
import { DEV_ERROR_OVERLAY_URL } from "./constants.ts";
import { renderToString } from "preact-render-to-string";
-import { BUILD_ID } from "./runtime/build_id.ts";
export type AsyncAnyComponent = {
(
@@ -85,12 +84,17 @@ export function preactRender(
} finally {
// Add preload headers
const basePath = ctx.config.basePath;
- const runtimeUrl = `${basePath}/_fresh/js/${BUILD_ID}/fresh-runtime.js`;
- let link = `<${encodeURI(runtimeUrl)}>; rel="modulepreload"; as="script"`;
+ const runtimeUrl = state.buildCache.clientEntry.startsWith(".")
+ ? state.buildCache.clientEntry.slice(1)
+ : state.buildCache.clientEntry;
+ let link = `<${
+ encodeURI(`${basePath}${runtimeUrl}`)
+ }>; rel="modulepreload"; as="script"`;
state.islands.forEach((island) => {
- link += `, <${
- encodeURI(`${basePath}${island.file}`)
- }>; rel="modulepreload"; as="script"`;
+ const specifier = `${basePath}${
+ island.file.startsWith(".") ? island.file.slice(1) : island.file
+ }`;
+ link += `, <${encodeURI(specifier)}>; rel="modulepreload"; as="script"`;
});
if (link !== "") {
diff --git a/src/runtime/build_id.ts b/src/runtime/build_id.ts
index 1e9ddcd847d..a7f3375b4b0 100644
--- a/src/runtime/build_id.ts
+++ b/src/runtime/build_id.ts
@@ -1,6 +1,8 @@
import { encodeHex } from "@std/encoding/hex";
-export const DENO_DEPLOYMENT_ID = Deno.env.get("DENO_DEPLOYMENT_ID");
+export const DENO_DEPLOYMENT_ID: string | undefined = Deno.env.get(
+ "DENO_DEPLOYMENT_ID",
+);
const deploymentId = DENO_DEPLOYMENT_ID ||
// For CI
Deno.env.get("GITHUB_SHA") ||
@@ -10,8 +12,8 @@ const buildIdHash = await crypto.subtle.digest(
new TextEncoder().encode(deploymentId),
);
-export let BUILD_ID = encodeHex(buildIdHash);
+export let BUILD_ID: string = encodeHex(buildIdHash);
-export function setBuildId(buildId: string) {
+export function setBuildId(buildId: string): void {
BUILD_ID = buildId;
}
diff --git a/src/runtime/client/partials.ts b/src/runtime/client/partials.ts
index 5db02a99294..26af1878b7d 100644
--- a/src/runtime/client/partials.ts
+++ b/src/runtime/client/partials.ts
@@ -329,7 +329,7 @@ export async function applyPartials(res: Response): Promise {
for (let i = 0; i < json.islands.length; i++) {
const island = json.islands[i];
promises.push(
- import(island.chunk).then((mod) => {
+ import(/* @vite-ignore */ island.chunk).then((mod) => {
ISLAND_REGISTRY.set(island.name, mod[island.exportName]);
}),
);
diff --git a/src/runtime/client/preact_hooks_client.ts b/src/runtime/client/preact_hooks_client.ts
index 345648823d8..9684385ad00 100644
--- a/src/runtime/client/preact_hooks_client.ts
+++ b/src/runtime/client/preact_hooks_client.ts
@@ -1,6 +1,6 @@
import { options } from "preact";
import { assetHashingHook, CLIENT_NAV_ATTR } from "../shared_internal.tsx";
-import { BUILD_ID } from "../build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
const oldVNodeHook = options.vnode;
options.vnode = (vnode) => {
diff --git a/src/runtime/server/preact_hooks.tsx b/src/runtime/server/preact_hooks.tsx
index 09d209f57a8..ab01760ca1c 100644
--- a/src/runtime/server/preact_hooks.tsx
+++ b/src/runtime/server/preact_hooks.tsx
@@ -23,7 +23,7 @@ import {
setActiveUrl,
} from "../shared_internal.tsx";
import type { BuildCache } from "../../build_cache.ts";
-import { BUILD_ID } from "../build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import {
DEV_ERROR_OVERLAY_URL,
PARTIAL_SEARCH_PARAM,
@@ -433,7 +433,8 @@ export interface PartialStateJson {
}
function FreshRuntimeScript() {
- const { islands, nonce, ctx, islandProps, partialId } = RENDER_STATE!;
+ const { islands, nonce, ctx, islandProps, partialId, buildCache } =
+ RENDER_STATE!;
const basePath = ctx.config.basePath;
const islandArr = Array.from(islands);
@@ -470,7 +471,11 @@ function FreshRuntimeScript() {
: island.exportName === island.name
? `{ ${island.exportName} }`
: `{ ${island.exportName} as ${island.name} }`;
- return `import ${named} from "${`${basePath}${island.file}`}";`;
+
+ const islandSpec = island.file.startsWith(".")
+ ? island.file.slice(1)
+ : island.file;
+ return `import ${named} from "${basePath}${islandSpec}";`;
}).join("");
const islandObj = "{" + islandArr.map((island) => island.name)
@@ -482,9 +487,12 @@ function FreshRuntimeScript() {
{ json: true },
);
- const runtimeUrl = `${basePath}/_fresh/js/${BUILD_ID}/fresh-runtime.js`;
+ // const runtimeUrl = `${basePath}/_fresh/js/${BUILD_ID}/fresh-runtime.js`;
+ const runtimeUrl = buildCache.clientEntry.startsWith(".")
+ ? buildCache.clientEntry.slice(1)
+ : buildCache.clientEntry;
const scriptContent =
- `import { boot } from "${runtimeUrl}";${islandImports}boot(${islandObj},${serializedProps});`;
+ `import { boot } from "${basePath}${runtimeUrl}";${islandImports}boot(${islandObj},${serializedProps});`;
return (
<>
@@ -496,7 +504,7 @@ function FreshRuntimeScript() {
__html: scriptContent,
}}
/>
- {ctx.config.mode === "development" ? : null}
+ {buildCache.features.errorOverlay ? : null}
>
);
}
diff --git a/src/runtime/shared.ts b/src/runtime/shared.ts
index c2293ac6600..f316a834268 100644
--- a/src/runtime/shared.ts
+++ b/src/runtime/shared.ts
@@ -1,5 +1,5 @@
import type { ComponentChildren, VNode } from "preact";
-import { BUILD_ID } from "./build_id.ts";
+import { BUILD_ID } from "fresh/build-id";
import { assetInternal, assetSrcSetInternal } from "./shared_internal.tsx";
/**
diff --git a/src/test_utils.ts b/src/test_utils.ts
index fa408dc12e8..449f9d54ba8 100644
--- a/src/test_utils.ts
+++ b/src/test_utils.ts
@@ -83,7 +83,7 @@ export function serveMiddleware(
(() => new Response("not found", { status: 404 }));
const config = options.config ?? DEFAULT_CONFIG;
const buildCache = options.buildCache ??
- new MockBuildCache([]);
+ new MockBuildCache([], options.config?.mode ?? "production");
const ctx = new Context(
req,
@@ -149,9 +149,12 @@ export async function withTmpDir(
export class MockBuildCache implements BuildCache {
#files: FsRouteFile[];
root = "";
+ clientEntry = "";
islandRegistry: ServerIslandRegistry = new Map();
+ features = { errorOverlay: false };
- constructor(files: FsRouteFile[]) {
+ constructor(files: FsRouteFile[], mode: "development" | "production") {
+ this.features.errorOverlay = mode === "development";
this.#files = files;
}
diff --git a/tests/test_utils.tsx b/tests/test_utils.tsx
index 53072f865a1..6555ec3fcf0 100644
--- a/tests/test_utils.tsx
+++ b/tests/test_utils.tsx
@@ -129,7 +129,8 @@ export async function withChildProcessServer(
let address = "";
let found = false;
// @ts-ignore yes it does
- for await (const line of lines.values({ preventCancel: true })) {
+ for await (const raw of lines.values({ preventCancel: true })) {
+ const line = colors.stripAnsiCode(raw);
output.push(line);
const match = line.match(
/https?:\/\/[^:]+:\d+(\/\w+[-\w]*)*/g,
@@ -147,12 +148,25 @@ export async function withChildProcessServer(
throw new Error(`Could not find server address`);
}
+ let failed = false;
try {
await fn(address);
+ } catch (err) {
+ // deno-lint-ignore no-console
+ console.log(output);
+ failed = true;
+ throw err;
} finally {
aborter.abort();
await cp.status;
- for await (const _ of lines) { /* noop */ }
+ for await (const line of lines) {
+ output.push(line);
+ }
+
+ if (failed) {
+ // deno-lint-ignore no-console
+ console.log(output);
+ }
}
}