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); + } } }