diff --git a/.swcrc b/.swcrc index 889d091c2..60e144e72 100644 --- a/.swcrc +++ b/.swcrc @@ -4,6 +4,5 @@ "syntax": "typescript" }, "target": "esnext" - }, - "minify": true + } } diff --git a/cli.js b/cli.js index 727db1502..61b7aacdc 100755 --- a/cli.js +++ b/cli.js @@ -1,14 +1,4 @@ #!/usr/bin/env node -import { Worker } from "node:worker_threads"; - const cmd = process.argv[2]; -process.env.WAKUWORK_CMD = cmd; -const execArgv = [ - ...(cmd === "dev" ? ["--experimental-loader", "tsx"] : []), - "--experimental-loader", - "wakuwork/node-loader", - "--experimental-loader", - "react-server-dom-webpack/node-loader", -]; -new Worker(new URL(`dist/cli-${cmd}.js`, import.meta.url), { execArgv }); +import(`./dist/cli-${cmd}.js`); diff --git a/examples/01_counter/entries.ts b/examples/01_counter/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/01_counter/entries.ts +++ b/examples/01_counter/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/01_counter/package.json b/examples/01_counter/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/01_counter/package.json +++ b/examples/01_counter/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/02_async/entries.ts b/examples/02_async/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/02_async/entries.ts +++ b/examples/02_async/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/02_async/package.json b/examples/02_async/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/02_async/package.json +++ b/examples/02_async/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/03_promise/entries.ts b/examples/03_promise/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/03_promise/entries.ts +++ b/examples/03_promise/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/03_promise/package.json b/examples/03_promise/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/03_promise/package.json +++ b/examples/03_promise/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/04_callserver/entries.ts b/examples/04_callserver/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/04_callserver/entries.ts +++ b/examples/04_callserver/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/04_callserver/package.json b/examples/04_callserver/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/04_callserver/package.json +++ b/examples/04_callserver/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/05_mutation/entries.ts b/examples/05_mutation/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/05_mutation/entries.ts +++ b/examples/05_mutation/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/05_mutation/package.json b/examples/05_mutation/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/05_mutation/package.json +++ b/examples/05_mutation/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/06_nesting/entries.ts b/examples/06_nesting/entries.ts index b40e856e8..b6328aa02 100644 --- a/examples/06_nesting/entries.ts +++ b/examples/06_nesting/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -11,32 +11,18 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [ - ["App", { name: "Wakuwork" }], - ["InnerApp", { count: 0 }], - ], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [ - ["App", { name: "Wakuwork" }], - ["InnerApp", { count: 0 }], - ["InnerApp", { count: 1 }], - ["InnerApp", { count: 2 }], - ["InnerApp", { count: 3 }], - ["InnerApp", { count: 4 }], - ["InnerApp", { count: 5 }], - ], - paths: ["/"], + "/": { + elements: [ + ["App", { name: "Wakuwork" }], + ["InnerApp", { count: 0 }], + ["InnerApp", { count: 1 }, true], + ["InnerApp", { count: 2 }, true], + ["InnerApp", { count: 3 }, true], + ["InnerApp", { count: 4 }, true], + ["InnerApp", { count: 5 }, true], + ], + }, }; }; diff --git a/examples/06_nesting/package.json b/examples/06_nesting/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/06_nesting/package.json +++ b/examples/06_nesting/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/07_router/entries.ts b/examples/07_router/entries.ts index b749bdb90..7a6ddc3a7 100644 --- a/examples/07_router/entries.ts +++ b/examples/07_router/entries.ts @@ -3,6 +3,7 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; -export const { getEntry, prefetcher, prerenderer } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") +export const { getEntry, getBuilder, getCustomModules } = fileRouter( + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); diff --git a/examples/07_router/package.json b/examples/07_router/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/07_router/package.json +++ b/examples/07_router/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/package.json b/package.json index 57217cfdb..788baac65 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ }, "exports": { "./package.json": "./package.json", - "./node-loader": "./dist/node-loader.js", ".": { "types": "./dist/main.d.ts", "default": "./dist/main.js" @@ -48,7 +47,7 @@ "compile:types": "tsc --project tsconfig.build.json", "test": "tsc --project . --noEmit", "e2e": "cd e2e/01 && playwright test && cd ../02 && playwright test", - "examples:dev": "WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" nodemon --ext ts,tsx --ignore ./examples/${NAME}/src/Counter.tsx --exec 'npm run compile:code && ./cli.js dev'", + "examples:dev": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js dev", "examples:dev:01_counter": "NAME=01_counter npm run examples:dev", "examples:dev:02_async": "NAME=02_async npm run examples:dev", "examples:dev:03_promise": "NAME=03_promise npm run examples:dev", @@ -56,7 +55,8 @@ "examples:dev:05_mutation": "NAME=05_mutation npm run examples:dev", "examples:dev:06_nesting": "NAME=06_nesting npm run examples:dev", "examples:dev:07_router": "NAME=07_router npm run examples:dev", - "examples:prd": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}/dist\\\"}}\" ./cli.js start", + "examples:build": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js build", + "examples:prd": "npm run examples:build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}/dist\\\"}}\" ./cli.js start", "examples:prd:01_counter": "NAME=01_counter npm run examples:prd", "examples:prd:02_async": "NAME=02_async npm run examples:prd", "examples:prd:03_promise": "NAME=03_promise npm run examples:prd", @@ -64,13 +64,13 @@ "examples:prd:05_mutation": "NAME=05_mutation npm run examples:prd", "examples:prd:06_nesting": "NAME=06_nesting npm run examples:prd", "examples:prd:07_router": "NAME=07_router npm run examples:prd", - "website:dev": "WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./website\\\"}}\" nodemon --ext ts,tsx --exec 'npm run compile:code && ./cli.js dev'", + "website:dev": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./website\\\"}}\" ./cli.js dev", "website:build": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./website\\\"}}\" ./cli.js build", "website:prd": "npm run website:build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./website/dist\\\"}}\" ./cli.js start" }, "license": "MIT", "engines": { - "node": ">=16.17.0" + "node": ">=18.0.0" }, "dependencies": { "@swc/core": "1.3.56", @@ -89,19 +89,16 @@ "@types/react": "^18.2.5", "@types/react-dom": "^18.2.3", "autoprefixer": "^10.4.14", - "nodemon": "^2.0.22", "postcss": "^8.4.23", "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", "tailwindcss": "^3.3.2", - "tsx": "^3.12.7", "typescript": "^5.0.4", "wakuwork": "link:." }, "peerDependencies": { "react": "18.3.0-canary-aef7ce554-20230503", - "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.6" + "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0f43e7ce..ea056f4dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ devDependencies: autoprefixer: specifier: ^10.4.14 version: 10.4.14(postcss@8.4.23) - nodemon: - specifier: ^2.0.22 - version: 2.0.22 postcss: specifier: ^8.4.23 version: 8.4.23 @@ -59,13 +56,10 @@ devDependencies: version: 18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503) react-server-dom-webpack: specifier: 18.3.0-canary-aef7ce554-20230503 - version: 18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503) + version: 18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503)(webpack@5.83.1) tailwindcss: specifier: ^3.3.2 version: 3.3.2 - tsx: - specifier: ^3.12.7 - version: 3.12.7 typescript: specifier: ^5.0.4 version: 5.0.4 @@ -304,33 +298,13 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - /@esbuild-kit/cjs-loader@2.4.2: - resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.5.0 - dev: true - - /@esbuild-kit/core-utils@3.1.0: - resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} - dependencies: - esbuild: 0.17.18 - source-map-support: 0.5.21 - dev: true - - /@esbuild-kit/esm-loader@2.5.5: - resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.5.0 - dev: true - /@esbuild/android-arm64@0.17.18: resolution: {integrity: sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==} engines: {node: '>=12'} cpu: [arm64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm@0.17.18: @@ -339,6 +313,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-x64@0.17.18: @@ -347,6 +322,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/darwin-arm64@0.17.18: @@ -355,6 +331,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-x64@0.17.18: @@ -363,6 +340,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-arm64@0.17.18: @@ -371,6 +349,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-x64@0.17.18: @@ -379,6 +358,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm64@0.17.18: @@ -387,6 +367,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm@0.17.18: @@ -395,6 +376,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ia32@0.17.18: @@ -403,6 +385,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-loong64@0.17.18: @@ -411,6 +394,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-mips64el@0.17.18: @@ -419,6 +403,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ppc64@0.17.18: @@ -427,6 +412,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-riscv64@0.17.18: @@ -435,6 +421,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-s390x@0.17.18: @@ -443,6 +430,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-x64@0.17.18: @@ -451,6 +439,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/netbsd-x64@0.17.18: @@ -459,6 +448,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-x64@0.17.18: @@ -467,6 +457,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/sunos-x64@0.17.18: @@ -475,6 +466,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: false optional: true /@esbuild/win32-arm64@0.17.18: @@ -483,6 +475,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-ia32@0.17.18: @@ -491,6 +484,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-x64@0.17.18: @@ -499,6 +493,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@jridgewell/gen-mapping@0.3.3: @@ -517,6 +512,13 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/source-map@0.3.3: + resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} @@ -756,10 +758,32 @@ packages: '@types/responselike': 1.0.0 dev: true + /@types/eslint-scope@3.7.4: + resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} + dependencies: + '@types/eslint': 8.37.0 + '@types/estree': 1.0.1 + dev: true + + /@types/eslint@8.37.0: + resolution: {integrity: sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==} + dependencies: + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.11 + dev: true + + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + dev: true + /@types/http-cache-semantics@4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -816,8 +840,126 @@ packages: - supports-color dev: false - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: true + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + dev: true + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + dev: true + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: true + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + dev: true + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + dev: true + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /acorn-import-assertions@1.9.0(acorn@8.8.2): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.8.2 dev: true /acorn-loose@8.3.0: @@ -833,6 +975,23 @@ packages: hasBin: true dev: true + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -995,6 +1154,11 @@ packages: fsevents: 2.3.2 dev: true + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + dev: true + /clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} dependencies: @@ -1011,6 +1175,10 @@ packages: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} dev: false + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1063,18 +1231,6 @@ packages: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: true - /debug@3.2.7(supports-color@5.5.0): - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - supports-color: 5.5.0 - dev: true - /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1116,6 +1272,18 @@ packages: once: 1.4.0 dev: true + /enhanced-resolve@5.14.0: + resolution: {integrity: sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /es-module-lexer@1.2.1: + resolution: {integrity: sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==} + dev: true + /esbuild@0.17.18: resolution: {integrity: sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==} engines: {node: '>=12'} @@ -1144,6 +1312,7 @@ packages: '@esbuild/win32-arm64': 0.17.18 '@esbuild/win32-ia32': 0.17.18 '@esbuild/win32-x64': 0.17.18 + dev: false /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -1159,6 +1328,36 @@ packages: engines: {node: '>=12'} dev: true + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + /execa@0.7.0: resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} engines: {node: '>=4'} @@ -1209,6 +1408,10 @@ packages: sort-keys-length: 1.0.1 dev: true + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1220,6 +1423,10 @@ packages: micromatch: 4.0.5 dev: true + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -1304,10 +1511,6 @@ packages: engines: {node: '>=10'} dev: true - /get-tsconfig@4.5.0: - resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==} - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1322,6 +1525,10 @@ packages: is-glob: 4.0.3 dev: true + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} dependencies: @@ -1355,9 +1562,19 @@ packages: responselike: 2.0.1 dev: true + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + dev: false + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} @@ -1387,10 +1604,6 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true - /ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - dev: true - /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -1451,6 +1664,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.0.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + /jiti@1.18.2: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true @@ -1469,6 +1691,14 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1490,6 +1720,11 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1544,6 +1779,13 @@ packages: engines: {node: '>= 0.6'} dev: true + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + /mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1575,10 +1817,6 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -1599,30 +1837,6 @@ packages: /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - /nodemon@2.0.22: - resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} - engines: {node: '>=8.10.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - debug: 3.2.7(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 3.1.2 - pstree.remy: 1.1.8 - semver: 5.7.1 - simple-update-notifier: 1.1.0 - supports-color: 5.5.0 - touch: 3.1.0 - undefsafe: 2.0.5 - dev: true - - /nopt@1.0.10: - resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1813,10 +2027,6 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true - /pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - dev: true - /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -1824,6 +2034,11 @@ packages: once: 1.4.0 dev: true + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -1833,6 +2048,12 @@ packages: engines: {node: '>=10'} dev: true + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /react-dom@18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503): resolution: {integrity: sha512-XyunYyvQ74LQ70sQefYRte21x3zhBfSvMu3drjd8jONP8LoJmFH0wL5pNLqfE1nH1NeCuatHNhx4MrhOzFKmZA==} peerDependencies: @@ -1848,7 +2069,7 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react-server-dom-webpack@18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503): + /react-server-dom-webpack@18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503)(webpack@5.83.1): resolution: {integrity: sha512-pBsyZTIYmzSTsEl9h1ejG6MMSv9fG8bhPjOTMJR0oxLAQEAOu8gmrG0Z/V297Vrj7Q69hr0bpvQBrE/Za1869g==} engines: {node: '>=0.10.0'} peerDependencies: @@ -1861,6 +2082,7 @@ packages: neo-async: 2.6.2 react: 18.3.0-canary-aef7ce554-20230503 react-dom: 18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503) + webpack: 5.83.1(@swc/core@1.3.56) dev: true /react@18.3.0-canary-aef7ce554-20230503: @@ -1947,6 +2169,15 @@ packages: loose-envify: 1.4.0 dev: true + /schema-utils@3.1.2: + resolution: {integrity: sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + /semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -1959,20 +2190,10 @@ packages: semver: 6.3.0 dev: true - /semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - dev: true - /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true - /semver@7.0.0: - resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} - hasBin: true - dev: true - /semver@7.5.0: resolution: {integrity: sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==} engines: {node: '>=10'} @@ -1981,6 +2202,12 @@ packages: lru-cache: 6.0.0 dev: true + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + dev: true + /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -2009,13 +2236,6 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true - /simple-update-notifier@1.1.0: - resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} - engines: {node: '>=8.10.0'} - dependencies: - semver: 7.0.0 - dev: true - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2109,6 +2329,14 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 + dev: false + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -2147,6 +2375,47 @@ packages: - ts-node dev: true + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /terser-webpack-plugin@5.3.9(@swc/core@1.3.56)(webpack@5.83.1): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@swc/core': 1.3.56 + jest-worker: 27.5.1 + schema-utils: 3.1.2 + serialize-javascript: 6.0.1 + terser: 5.17.4 + webpack: 5.83.1(@swc/core@1.3.56) + dev: true + + /terser@5.17.4: + resolution: {integrity: sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.3 + acorn: 8.8.2 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2179,13 +2448,6 @@ packages: ieee754: 1.2.1 dev: true - /touch@3.1.0: - resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} - hasBin: true - dependencies: - nopt: 1.0.10 - dev: true - /trim-repeated@2.0.0: resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==} engines: {node: '>=12'} @@ -2197,27 +2459,12 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /tsx@3.12.7: - resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} - hasBin: true - dependencies: - '@esbuild-kit/cjs-loader': 2.4.2 - '@esbuild-kit/core-utils': 3.1.0 - '@esbuild-kit/esm-loader': 2.5.5 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /typescript@5.0.4: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} hasBin: true dev: true - /undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - dev: true - /update-browserslist-db@1.0.11(browserslist@4.21.5): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true @@ -2228,6 +2475,12 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -2265,6 +2518,59 @@ packages: fsevents: 2.3.2 dev: false + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.83.1(@swc/core@1.3.56): + resolution: {integrity: sha512-TNsG9jDScbNuB+Lb/3+vYolPplCS3bbEaJf+Bj0Gw4DhP3ioAflBb1flcRt9zsWITyvOhM96wMQNRWlSX52DgA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.4 + '@types/estree': 1.0.1 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.8.2 + acorn-import-assertions: 1.9.0(acorn@8.8.2) + browserslist: 4.21.5 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.14.0 + es-module-lexer: 1.2.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.1.2 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(@swc/core@1.3.56)(webpack@5.83.1) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true diff --git a/src/builder.ts b/src/builder.ts index 0f854eb88..7f1cf32ba 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -8,27 +8,23 @@ import react from "@vitejs/plugin-react"; import * as swc from "@swc/core"; import type { Config } from "./config.js"; -import type { GetEntry, Prefetcher, Prerenderer } from "./server.js"; -import { generatePrefetchCode } from "./middleware/lib/rsc-utils.js"; -import { renderRSC } from "./middleware/lib/rsc-renderer.js"; - -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -// TODO we have duplicate code here and rscPrd.ts and rsc-renderers*.ts - -const rscPlugin = (): Plugin => { - const code = ` -globalThis.__wakuwork_module_cache__ = new Map(); -globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); -globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id); -`; +import { codeToInject } from "./middleware/lib/rsc-utils.js"; +import { + shutdown, + setClientEntries, + getCustomModulesRSC, + buildRSC, +} from "./middleware/lib/rsc-handler.js"; + +// FIXME we could do this without plugin anyway +const rscIndexPlugin = (): Plugin => { return { - name: "rscPlugin", + name: "rsc-index-plugin", async transformIndexHtml() { return [ { tag: "script", - children: code, + children: codeToInject, injectTo: "body", }, ]; @@ -36,200 +32,35 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge }; }; -const walkDirSync = (dir: string, callback: (filePath: string) => void) => { - fs.readdirSync(dir, { withFileTypes: true }).forEach((dirent) => { - const filePath = path.join(dir, dirent.name); - if (dirent.isDirectory()) { - if (dirent.name !== "node_modules") { - walkDirSync(filePath, callback); - } - } else { - callback(filePath); - } - }); -}; - -const getClientEntryFiles = (dir: string) => { - const files: string[] = []; - walkDirSync(dir, (fname) => { - if (fname.endsWith(".ts") || fname.endsWith(".tsx")) { - const mod = swc.parseFileSync(fname, { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), - }); - for (const item of mod.body) { - if ( - item.type === "ExpressionStatement" && - item.expression.type === "StringLiteral" && - item.expression.value === "use client" - ) { - files.push(fname); - } - } - } - // TODO transpile ".jsx" - }); - return files; -}; - -const compileFiles = (dir: string, distPath: string) => { - walkDirSync(dir, (fname) => { - const relativePath = path.relative(dir, fname); - if (relativePath.startsWith(distPath)) { - return; - } - if (fname.endsWith(".ts") || fname.endsWith(".tsx")) { - const { code } = swc.transformFileSync(fname, { - jsc: { - parser: { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), - }, - transform: { - react: { - runtime: "automatic", - }, - }, - }, - }); - const destFile = path.join( - dir, - distPath, - relativePath.replace(/\.tsx?$/, ".js") - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - fs.writeFileSync(destFile, code); - } - // TODO transpile ".jsx" - }); -}; - -const prerender = async ( - dir: string, - distPath: string, - publicPath: string, - entriesFile: string, - basePath: string, - publicIndexHtmlFile: string -): Promise> => { - const serverEntries: Record = {}; - - const { prefetcher, prerenderer, clientEntries } = await (import( - entriesFile - ) as Promise<{ - getEntry: GetEntry; - prefetcher?: Prefetcher; - prerenderer?: Prerenderer; - clientEntries?: Record; - }>); - - const getClientEntry = (id: string) => { - if (!clientEntries) { - throw new Error("Missing client entries"); - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(path.join(dir, distPath)), id); - id = basePath + getClientEntry(decodeURI(id)); - } - return [id, name]; - }; - - if (prerenderer) { - const { - entryItems = [], - paths = [], - unstable_customCode = () => "", - } = await prerenderer(); - await Promise.all( - Array.from(entryItems).map(async ([rscId, props]) => { - // FIXME we blindly expect JSON.stringify usage is deterministic - const serializedProps = JSON.stringify(props); - const searchParams = new URLSearchParams(); - searchParams.set("props", serializedProps); - const destFile = path.join( - dir, - publicPath, - "RSC", - decodeURIComponent(rscId), - decodeURIComponent(`${searchParams}`) - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - await new Promise((resolve, reject) => { - const stream = fs.createWriteStream(destFile); - stream.on("finish", resolve); - stream.on("error", reject); - renderRSC( - { - rscId, - props: props as any, - }, - { - loadClientEntries: true, - serverEntryCallback: (rsfId, fileId) => { - serverEntries[rsfId] = fileId; - }, - } - ).pipe(stream); +const rscAnalyzePlugin = ( + clientEntryCallback: (id: string) => void, + serverEntryCallback: (id: string) => void +): Plugin => { + return { + name: "rsc-bundle-plugin", + transform(code, id) { + const ext = path.extname(id); + if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) { + const mod = swc.parseSync(code, { + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", }); - }) - ); - - const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { - encoding: "utf8", - }); - for (const pathItem of paths) { - let code = ""; - if (prefetcher) { - const { entryItems = [], clientModules = [] } = await prefetcher( - pathItem - ); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); + for (const item of mod.body) { + if ( + item.type === "ExpressionStatement" && + item.expression.type === "StringLiteral" + ) { + if (item.expression.value === "use client") { + clientEntryCallback(id); + } else if (item.expression.value === "use server") { + serverEntryCallback(id); + } } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; - } - const destFile = path.join( - dir, - publicPath, - pathItem, - pathItem.endsWith("/") ? "index.html" : "" - ); - let data = ""; - if (fs.existsSync(destFile)) { - data = fs.readFileSync(destFile, { encoding: "utf8" }); - } else { - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - data = publicIndexHtml; } - if (code) { - // HACK is this too naive to inject script code? - data = data.replace(/<\/body>/, ``); - } - const code2 = unstable_customCode(pathItem, decodeId); - if (code2) { - data = data.replace(/<\/body>/, ``); - } - fs.writeFileSync(destFile, data, { encoding: "utf8" }); - } - } - - return serverEntries; + return code; + }, + }; }; export async function runBuild(config: Config = {}) { @@ -238,28 +69,112 @@ export async function runBuild(config: Config = {}) { const distPath = config.files?.dist || "dist"; const publicPath = path.join(distPath, config.files?.public || "public"); const indexHtmlFile = path.join(dir, config.files?.indexHtml || "index.html"); - const publicIndexHtmlFile = path.join( - dir, - publicPath, - config.files?.indexHtml || "index.html" - ); - const entriesFile = path.join( + const distEntriesFile = path.join( dir, distPath, config.files?.entriesJs || "entries.js" ); + let entriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); + if (entriesFile.endsWith(".js")) { + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const tmp = entriesFile.slice(0, -3) + ext; + if (fs.existsSync(tmp)) { + entriesFile = tmp; + break; + } + } + } const require = createRequire(import.meta.url); + const customModules = await getCustomModulesRSC(); + const clientEntryFileSet = new Set(); + const serverEntryFileSet = new Set(); + await build({ + root: dir, + base: basePath, + plugins: [ + rscAnalyzePlugin( + (id) => clientEntryFileSet.add(id), + (id) => serverEntryFileSet.add(id) + ), + ], + ssr: { + // FIXME Without this, wakuwork/router isn't considered to have client + // entries, and "No client entry" error occurs. + // Unless we fix this, RSC-capable packages aren't supported. + noExternal: ["wakuwork"], + }, + build: { + outDir: distPath, + write: false, + ssr: true, + rollupOptions: { + input: { + entries: entriesFile, + ...customModules, + }, + }, + }, + }); const clientEntryFiles = Object.fromEntries( - getClientEntryFiles(dir).map((fname, i) => [`rsc${i}`, fname]) + Array.from(clientEntryFileSet).map((fname, i) => [`rsc${i}`, fname]) + ); + const serverEntryFiles = Object.fromEntries( + Array.from(serverEntryFileSet).map((fname, i) => [`rsf${i}`, fname]) ); - const output = await build({ + + const serverBuildOutput = await build({ + root: dir, + base: basePath, + ssr: { + noExternal: Array.from(clientEntryFileSet).map( + (fname) => + path.relative(path.join(dir, "node_modules"), fname).split("/")[0]! + ), + }, + build: { + outDir: distPath, + ssr: true, + rollupOptions: { + input: { + entries: entriesFile, + ...clientEntryFiles, + ...serverEntryFiles, + ...customModules, + }, + output: { + banner: (chunk) => { + // HACK to bring directives to the front + let code = ""; + if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) { + code += '"use client";'; + } + if (chunk.moduleIds.some((id) => serverEntryFileSet.has(id))) { + code += '"use server";'; + } + return code; + }, + entryFileNames: (chunkInfo) => { + if (chunkInfo.name === "entries" || customModules[chunkInfo.name]) { + return "[name].js"; + } + return "assets/[name].js"; + }, + }, + }, + }, + }); + if (!("output" in serverBuildOutput)) { + throw new Error("Unexpected vite server build output"); + } + + const clientBuildOutput = await build({ root: dir, base: basePath, plugins: [ // @ts-ignore react(), - rscPlugin(), + rscIndexPlugin(), ], build: { outDir: publicPath, @@ -272,37 +187,39 @@ export async function runBuild(config: Config = {}) { }, }, }); - const clientEntries: Record = {}; - if (!("output" in output)) { - throw new Error("Unexpected vite build output"); + if (!("output" in clientBuildOutput)) { + throw new Error("Unexpected vite client build output"); } - for (const item of output.output) { + + const clientEntries: Record = {}; + for (const item of clientBuildOutput.output) { const { name, fileName } = item; - const entryFile = name && clientEntryFiles[name]; + const entryFile = + name && + serverBuildOutput.output.find( + (item) => + "moduleIds" in item && + item.moduleIds.includes(clientEntryFiles[name] as string) + )?.fileName; if (entryFile) { - clientEntries[path.relative(dir, entryFile)] = fileName; + clientEntries[entryFile] = fileName; } } console.log("clientEntries", clientEntries); - - compileFiles(dir, distPath); fs.appendFileSync( - entriesFile, + distEntriesFile, `export const clientEntries=${JSON.stringify(clientEntries)};` ); - const serverEntries = await prerender( - dir, - distPath, - publicPath, - entriesFile, - basePath, - publicIndexHtmlFile - ); - console.log("serverEntries", serverEntries); - fs.appendFileSync( - entriesFile, - `export const serverEntries=${JSON.stringify(serverEntries)};` + + const absoluteClientEntries = Object.fromEntries( + Object.entries(clientEntries).map(([key, val]) => [ + path.join(path.dirname(entriesFile), distPath, key), + basePath + val, + ]) ); + await setClientEntries(absoluteClientEntries); + + await buildRSC(); const origPackageJson = require(path.join(dir, "package.json")); const packageJson = { @@ -319,4 +236,6 @@ export async function runBuild(config: Config = {}) { path.join(dir, distPath, "package.json"), JSON.stringify(packageJson, null, 2) ); + + await shutdown(); } diff --git a/src/cli-build.ts b/src/cli-build.ts index d2e02727d..a7f72e045 100644 --- a/src/cli-build.ts +++ b/src/cli-build.ts @@ -4,4 +4,3 @@ const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); await runBuild(config); -process.exit(0); diff --git a/src/cli-dev.ts b/src/cli-dev.ts index d4d4f813b..adb789b89 100644 --- a/src/cli-dev.ts +++ b/src/cli-dev.ts @@ -1,5 +1,7 @@ import { startDevServer } from "./devServer.js"; +process.env.NODE_ENV ||= "development"; + const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); diff --git a/src/cli-start.ts b/src/cli-start.ts index 83d4d0176..c56aac1d4 100644 --- a/src/cli-start.ts +++ b/src/cli-start.ts @@ -1,5 +1,7 @@ import { startPrdServer } from "./prdServer.js"; +process.env.NODE_ENV ||= "production"; + const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); diff --git a/src/client.ts b/src/client.ts index 346a65e83..9b4d5eeac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,9 +2,7 @@ import { cache, use, useEffect, useState } from "react"; import type { ReactElement } from "react"; -import RSDWClient from "react-server-dom-webpack/client"; - -const { createFromFetch, encodeReply } = RSDWClient; +import { createFromFetch, encodeReply } from "react-server-dom-webpack/client"; // FIXME only works with basePath="/" const basePath = "/"; diff --git a/src/config.ts b/src/config.ts index 2cbe6fbfe..383e38625 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,7 +36,3 @@ export type Config = { build?: Build; files?: Files; }; - -export function defineConfig(config: Config): Config { - return config; -} diff --git a/src/devServer.ts b/src/devServer.ts index 181c8d685..a8e1c2651 100644 --- a/src/devServer.ts +++ b/src/devServer.ts @@ -2,10 +2,8 @@ import http from "node:http"; import type { Config, Middleware } from "./config.js"; import { pipe } from "./middleware/lib/common.js"; -import type { Shared } from "./middleware/lib/common.js"; export function startDevServer(config: Config = {}) { - const shared: Shared = {}; const middlewares = config.devServer?.middlewares || [ "rewriteRsc", "rscDev", @@ -16,7 +14,7 @@ export function startDevServer(config: Config = {}) { middlewares.map(async (middleware) => { if (typeof middleware === "string") { const mod = await import(`./middleware/${middleware}.js`); - return (mod.default || mod)(config, shared); + return (mod.default || mod)(config); } return middleware; }) diff --git a/src/main.ts b/src/main.ts index 540acbb43..4c8c34e04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -export { defineConfig } from "./config.js"; export type { Config } from "./config.js"; export { startDevServer } from "./devServer.js"; diff --git a/src/middleware/indexFallback.ts b/src/middleware/indexFallback.ts index dbb79717f..a602a1b9d 100644 --- a/src/middleware/indexFallback.ts +++ b/src/middleware/indexFallback.ts @@ -1,10 +1,9 @@ import path from "node:path"; import fs from "node:fs"; -import fsPromises from "node:fs/promises"; import type { MiddlewareCreator } from "./lib/common.js"; -const staticFile: MiddlewareCreator = (config, shared) => { +const staticFile: MiddlewareCreator = (config) => { const dir = path.resolve(config.prdServer?.dir || "."); const publicPath = config.files?.public || "public"; const indexHtml = config.files?.indexHtml || "index.html"; @@ -17,18 +16,6 @@ const staticFile: MiddlewareCreator = (config, shared) => { const stat = fs.statSync(indexHtmlFile, { throwIfNoEntry: false }); if (stat) { res.setHeader("Content-Type", "text/html; charset=utf-8"); - const code = await shared.prdScriptToInject?.(req.url || ""); - if (code) { - let data = await fsPromises.readFile(indexHtmlFile, { - encoding: "utf-8", - }); - const scriptToInject = ``; - if (!data.includes(scriptToInject)) { - data = data.replace(/<\/body>/, `${scriptToInject}`); - } - res.end(data); - return; - } res.setHeader("Content-Length", stat.size); fs.createReadStream(indexHtmlFile).pipe(res); return; diff --git a/src/middleware/lib/common.ts b/src/middleware/lib/common.ts index 82c3119fe..a175a0b1b 100644 --- a/src/middleware/lib/common.ts +++ b/src/middleware/lib/common.ts @@ -1,11 +1,6 @@ import type { Config, Middleware } from "../../config.js"; -export type Shared = { - devScriptToInject?: (path: string) => Promise; - prdScriptToInject?: (path: string) => Promise; -}; - -export type MiddlewareCreator = (config: Config, shared: Shared) => Middleware; +export type MiddlewareCreator = (config: Config) => Middleware; export const pipe = (middlewares: Middleware[]): Middleware => diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts new file mode 100644 index 000000000..7e4fa0d44 --- /dev/null +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -0,0 +1,385 @@ +import path from "node:path"; +import fs from "node:fs"; +import { parentPort } from "node:worker_threads"; +import { Writable } from "node:stream"; + +import { createServer } from "vite"; +import { createElement } from "react"; +import RSDWServer from "react-server-dom-webpack/server"; + +import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; +import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; +import type { Config } from "../../config.js"; +import type { GetEntry, GetBuilder, GetCustomModules } from "../../server.js"; +import { rscTransformPlugin, rscReloadPlugin } from "./vite-plugin-rsc.js"; + +const { renderToPipeableStream } = RSDWServer; + +const handleSetClientEntries = async ( + mesg: MessageReq & { type: "setClientEntries" } +) => { + const { id, value } = mesg; + try { + await setClientEntries(value); + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + +const handleRender = async (mesg: MessageReq & { type: "render" }) => { + const { id, input } = mesg; + try { + const pipeable = await renderRSC(input); + const writable = new Writable({ + write(chunk, encoding, callback) { + if (encoding !== ("buffer" as any)) { + throw new Error("Unknown encoding"); + } + const buffer: Buffer = chunk; + const mesg: MessageRes = { + id, + type: "buf", + buf: buffer.buffer, + offset: buffer.byteOffset, + len: buffer.length, + }; + parentPort!.postMessage(mesg, [mesg.buf]); + callback(); + }, + final(callback) { + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + callback(); + }, + }); + pipeable.pipe(writable); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + +const handleGetCustomModules = async ( + mesg: MessageReq & { type: "getCustomModules" } +) => { + const { id } = mesg; + try { + const modules = await getCustomModulesRSC(); + const mesg: MessageRes = { + id, + type: "customModules", + modules, + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + +const handleBuild = async (mesg: MessageReq & { type: "build" }) => { + const { id } = mesg; + try { + await buildRSC(); + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + +parentPort!.on("message", (mesg: MessageReq) => { + if (mesg.type === "shutdown") { + vitePromise.then(async (vite) => { + await vite.close(); + parentPort!.close(); + }); + } else if (mesg.type === "setClientEntries") { + handleSetClientEntries(mesg); + } else if (mesg.type === "render") { + handleRender(mesg); + } else if (mesg.type === "getCustomModules") { + handleGetCustomModules(mesg); + } else if (mesg.type === "build") { + handleBuild(mesg); + } +}); + +type PipeableStream = { + pipe(destination: T): T; +}; + +// TODO use of process.env is all temporary +// TODO these are temporary +const config: Config = + (process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG)) || + {}; +const dirFromConfig = + config.prdServer?.dir ?? config.build?.dir ?? config.devServer?.dir; // HACK +const dir = path.resolve(dirFromConfig || "."); +const basePath = config.build?.basePath || "/"; // FIXME it's not build only +const distPath = config.files?.dist || "dist"; +const publicPath = path.join(distPath, config.files?.public || "public"); +const publicIndexHtmlFile = path.join( + dir, + publicPath, + config.files?.indexHtml || "index.html" +); +const entriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); +const distEntriesFile = path.join( + dir, + distPath, + config.files?.entriesJs || "entries.js" +); + +const vitePromise = createServer({ + root: dir, + ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), + plugins: [ + rscTransformPlugin(), + ...(process.env.NODE_ENV === "development" + ? [ + rscReloadPlugin((type) => { + const mesg: MessageRes = { type }; + parentPort!.postMessage(mesg); + }), + ] + : []), + ], + ssr: { + noExternal: ["wakuwork"], // FIXME this doesn't seem ideal? + }, + appType: "custom", +}); + +const loadServerFile = async (fname: string) => { + const vite = await vitePromise; + return vite.ssrLoadModule(fname); +}; + +const getFunctionComponent = async (rscId: string, isBuild: boolean) => { + const { getEntry } = await (loadServerFile( + isBuild ? distEntriesFile : entriesFile + ) as Promise<{ + getEntry: GetEntry; + }>); + const mod = await getEntry(rscId); + if (typeof mod === "function") { + return mod; + } + if (typeof mod.default === "function") { + return mod.default; + } + throw new Error("No function component found"); +}; + +let absoluteClientEntries: Record = {}; + +const resolveClientEntry = (filePath: string) => { + const clientEntry = absoluteClientEntries[filePath]; + if (!clientEntry) { + if (absoluteClientEntries["*"] === "*") { + return basePath + path.relative(dir, filePath); + } + throw new Error("No client entry found for " + filePath); + } + return clientEntry; +}; + +async function setClientEntries( + value: "load" | Record +): Promise { + if (value !== "load") { + absoluteClientEntries = value; + return; + } + const { clientEntries } = await loadServerFile(entriesFile); + if (!clientEntries) { + throw new Error("Failed to load clientEntries"); + } + const baseDir = path.dirname(entriesFile); + absoluteClientEntries = Object.fromEntries( + Object.entries(clientEntries).map(([key, val]) => [ + path.join(baseDir, key), + basePath + val, + ]) + ); +} + +async function renderRSC(input: RenderInput): Promise { + const bundlerConfig = new Proxy( + {}, + { + get(_target, encodedId: string) { + const [filePath, name] = encodedId.split("#") as [string, string]; + const id = resolveClientEntry(filePath); + return { id, chunks: [id], name, async: true }; + }, + } + ); + + if (input.rsfId && input.args) { + const [fileId, name] = input.rsfId.split("#"); + const fname = path.join(dir, fileId!); + const mod = await loadServerFile(fname); + const data = await (mod[name!] || mod)(...input.args); + if (!input.rscId) { + return renderToPipeableStream(data, bundlerConfig); + } + // continue for mutation mode + } + if (input.rscId && input.props) { + const component = await getFunctionComponent(input.rscId, false); + return renderToPipeableStream( + createElement(component, input.props), + bundlerConfig + ).pipe(transformRsfId(dir)); + } + throw new Error("Unexpected input"); +} + +async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { + const { getCustomModules } = await (loadServerFile(entriesFile) as Promise<{ + getCustomModules?: GetCustomModules; + }>); + if (!getCustomModules) { + return {}; + } + const modules = await getCustomModules(); + return modules; +} + +// FIXME this may take too much responsibility +async function buildRSC(): Promise { + const { getBuilder } = await (loadServerFile(distEntriesFile) as Promise<{ + getBuilder?: GetBuilder; + }>); + if (!getBuilder) { + console.warn( + "getBuilder is undefined. It's recommended for optimization and sometimes required." + ); + return; + } + + // FIXME this doesn't seem an ideal solution + const decodeId = (encodedId: string): [id: string, name: string] => { + const [filePath, name] = encodedId.split("#") as [string, string]; + const id = resolveClientEntry(filePath); + return [id, name]; + }; + + const pathMap = await getBuilder(decodeId); + const clientModuleMap = new Map>(); + const addClientModule = (pathStr: string, id: string) => { + let idSet = clientModuleMap.get(pathStr); + if (!idSet) { + idSet = new Set(); + clientModuleMap.set(pathStr, idSet); + } + idSet.add(id); + }; + await Promise.all( + Object.entries(pathMap).map(async ([pathStr, { elements }]) => { + for (const [rscId, props] of elements || []) { + // FIXME we blindly expect JSON.stringify usage is deterministic + const serializedProps = JSON.stringify(props); + const searchParams = new URLSearchParams(); + searchParams.set("props", serializedProps); + const destFile = path.join( + dir, + publicPath, + "RSC", + decodeURIComponent(rscId), + decodeURIComponent(`${searchParams}`) + ); + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + const bundlerConfig = new Proxy( + {}, + { + get(_target, encodedId: string) { + const [id, name] = decodeId(encodedId); + addClientModule(pathStr, id); + return { id, chunks: [id], name, async: true }; + }, + } + ); + const component = await getFunctionComponent(rscId, true); + const pipeable = renderToPipeableStream( + createElement(component, props as any), + bundlerConfig + ).pipe(transformRsfId(path.join(dir, distPath))); + await new Promise((resolve, reject) => { + const stream = fs.createWriteStream(destFile); + stream.on("finish", resolve); + stream.on("error", reject); + pipeable.pipe(stream); + }); + } + }) + ); + + const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { + encoding: "utf8", + }); + await Promise.all( + Object.entries(pathMap).map(async ([pathStr, { elements, customCode }]) => { + const destFile = path.join( + dir, + publicPath, + pathStr, + pathStr.endsWith("/") ? "index.html" : "" + ); + let data = ""; + if (fs.existsSync(destFile)) { + data = fs.readFileSync(destFile, { encoding: "utf8" }); + } else { + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + data = publicIndexHtml; + } + const code = + generatePrefetchCode( + Array.from(elements || []).flatMap(([rscId, props, skipPrefetch]) => { + if (skipPrefetch) { + return []; + } + return [[rscId, props]]; + }), + clientModuleMap.get(pathStr) || [] + ) + (customCode || ""); + if (code) { + // HACK is this too naive to inject script code? + data = data.replace(/<\/body>/, ``); + } + fs.writeFileSync(destFile, data, { encoding: "utf8" }); + }) + ); +} diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts new file mode 100644 index 000000000..33bc5d13a --- /dev/null +++ b/src/middleware/lib/rsc-handler.ts @@ -0,0 +1,161 @@ +import { PassThrough } from "node:stream"; +import type { Readable } from "node:stream"; +import { Worker } from "node:worker_threads"; + +const worker = new Worker(new URL("rsc-handler-worker.js", import.meta.url), { + execArgv: ["--conditions", "react-server"], +}); + +export type RenderInput = { + rscId?: string | undefined; + props?: Props | undefined; + rsfId?: string | undefined; + args?: unknown[] | undefined; +}; + +type CustomModules = { + [name: string]: string; +}; + +export type MessageReq = + | { type: "shutdown" } + | { + id: number; + type: "setClientEntries"; + value: "load" | Record; + } + | { + id: number; + type: "render"; + input: RenderInput; + } + | { + id: number; + type: "getCustomModules"; + } + | { + id: number; + type: "build"; + }; + +export type MessageRes = + | { type: "full-reload" } + | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } + | { id: number; type: "end" } + | { id: number; type: "err"; err: unknown } + | { id: number; type: "customModules"; modules: CustomModules }; + +const messageCallbacks = new Map void>(); + +worker.on("message", (mesg: MessageRes) => { + if ("id" in mesg) { + messageCallbacks.get(mesg.id)?.(mesg); + } +}); + +export function registerReloadCallback(fn: (type: "full-reload") => void) { + const listener = (mesg: MessageRes) => { + if (mesg.type === "full-reload") { + fn(mesg.type); + } + }; + worker.on("message", listener); + return () => worker.off("message", listener); +} + +export function shutdown() { + return new Promise((resolve) => { + worker.on("close", resolve); + worker.postMessage({ type: "shutdown" }); + }); +} + +let nextId = 1; + +export function setClientEntries( + value: "load" | Record +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "end") { + resolve(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "setClientEntries", + value, + }; + worker.postMessage(mesg); + }); +} + +export function renderRSC(input: RenderInput): Readable { + const id = nextId++; + const passthrough = new PassThrough(); + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "buf") { + passthrough.write(Buffer.from(mesg.buf, mesg.offset, mesg.len)); + } else if (mesg.type === "end") { + passthrough.end(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + passthrough.destroy( + mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err)) + ); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "render", + input, + }; + worker.postMessage(mesg); + return passthrough; +} + +export function getCustomModulesRSC(): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "customModules") { + resolve(mesg.modules); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "getCustomModules", + }; + worker.postMessage(mesg); + }); +} + +export function buildRSC(): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "end") { + resolve(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "build", + }; + worker.postMessage(mesg); + }); +} diff --git a/src/middleware/lib/rsc-renderer-worker.ts b/src/middleware/lib/rsc-renderer-worker.ts deleted file mode 100644 index d3d4129e9..000000000 --- a/src/middleware/lib/rsc-renderer-worker.ts +++ /dev/null @@ -1,226 +0,0 @@ -import path from "node:path"; -import url from "node:url"; -import { parentPort } from "node:worker_threads"; -import { Writable } from "node:stream"; - -import { createElement } from "react"; -import RSDWServer from "react-server-dom-webpack/server"; - -import { transformRsfId } from "./rsc-utils.js"; -import type { Input, MessageReq, MessageRes } from "./rsc-renderer.js"; -import type { Config } from "../../config.js"; - -const { renderToPipeableStream } = RSDWServer; - -type PipeableStream = { - pipe(destination: T): T; -}; - -// TODO use of process.env is all temporary -// TODO these are temporary -const config: Config = - (process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG)) || - {}; -const dirFromConfig = { - dev: config.devServer?.dir, - build: config.build?.dir, - start: config.prdServer?.dir, -}[String(process.env.WAKUWORK_CMD)]; -const dir = path.resolve(dirFromConfig || "."); -const basePath = config.build?.basePath || "/"; // FIXME it's not build only -const distPath = config.files?.dist || "dist"; -const entriesFile = url - .pathToFileURL( - path.join( - dir, - process.env.WAKUWORK_CMD === "build" ? distPath : "", - config.files?.entriesJs || "entries.js" - ) - ) - .toString(); - -const getFunctionComponent = async (rscId: string) => { - const { getEntry } = await import(entriesFile); - const mod = await getEntry(rscId); - if (typeof mod === "function") { - return mod; - } - if (typeof mod.default === "function") { - return mod.default; - } - throw new Error("No function component found"); -}; - -parentPort!.on("message", async (mesg: MessageReq) => { - const { id, input, loadClientEntries, loadServerEntries, notifyServerEntry } = - mesg; - try { - const pipeable = await renderRSC(input, { - loadClientEntries, - loadServerEntries, - serverEntryCallback: notifyServerEntry - ? (rsfId, fileId) => { - const mesg: MessageRes = { - id, - type: "serverEntry", - rsfId, - fileId, - }; - parentPort!.postMessage(mesg); - } - : undefined, - }); - const writable = new Writable({ - write(chunk, encoding, callback) { - if (encoding !== ("buffer" as any)) { - throw new Error("Unknown encoding"); - } - const buffer: Buffer = chunk; - const mesg: MessageRes = { - id, - type: "buf", - buf: buffer.buffer, - offset: buffer.byteOffset, - len: buffer.length, - }; - parentPort!.postMessage(mesg, [mesg.buf]); - callback(); - }, - final(callback) { - const mesg: MessageRes = { - id, - type: "end", - }; - parentPort!.postMessage(mesg); - callback(); - }, - }); - pipeable.pipe(writable); - } catch (err) { - const mesg: MessageRes = { - id, - type: "err", - err, - }; - parentPort!.postMessage(mesg); - } -}); - -async function renderRSC( - input: Input, - options: { - loadClientEntries: boolean | undefined; - loadServerEntries: boolean | undefined; - serverEntryCallback: ((rsfId: string, fileId: string) => void) | undefined; - } -): Promise { - let clientEntries: Record | undefined; - let serverEntries: Record | undefined; - if (options.loadClientEntries) { - ({ clientEntries } = await import(entriesFile)); - if (!clientEntries) { - throw new Error("Failed to load clientEntries"); - } - } - if (options.loadServerEntries) { - ({ serverEntries } = await import(entriesFile)); - if (!serverEntries) { - throw new Error("Failed to load serverEntries"); - } - } else if (process.env.WAKUWORK_CMD !== "dev") { - serverEntries = {}; - } - - const getClientEntry = (id: string) => { - if (!clientEntries) { - return id; - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative( - "file://" + - encodeURI( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ), - id - ); - id = basePath + getClientEntry(decodeURI(id)); - } - return [id, name]; - }; - - const bundlerConfig = new Proxy( - {}, - { - get(_target, encodedId: string) { - const [id, name] = decodeId(encodedId); - return { id, chunks: [id], name, async: true }; - }, - } - ); - - const registerServerEntry = (fileId: string): string => { - if (!serverEntries) { - return fileId; - } - for (const entry of Object.entries(serverEntries)) { - if (entry[1] === fileId) { - return entry[0]; - } - } - const rsfId = `rsf${Object.keys(serverEntries).length}`; - serverEntries[rsfId] = fileId; - options.serverEntryCallback?.(rsfId, fileId); - return rsfId; - }; - - const getServerEntry = (rsfId: string): string => { - if (!serverEntries) { - return rsfId; - } - const fileId = serverEntries[rsfId]; - if (!fileId) { - throw new Error("No server entry found"); - } - return fileId; - }; - - if (input.rsfId && input.args) { - const [fileId, name] = getServerEntry(input.rsfId).split("#"); - const fname = path.join(dir, fileId!); - const mod = await import(fname); - const data = await (mod[name!] || mod)(...input.args); - if (!input.rscId) { - return renderToPipeableStream(data, bundlerConfig); - } - // continue for mutation mode - } - if (input.rscId && input.props) { - const component = await getFunctionComponent(input.rscId); - return renderToPipeableStream( - createElement(component, input.props), - bundlerConfig - ).pipe( - transformRsfId( - "file://" + - encodeURI( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ), - registerServerEntry - ) - ); - } - throw new Error("Unexpected input"); -} diff --git a/src/middleware/lib/rsc-renderer.ts b/src/middleware/lib/rsc-renderer.ts deleted file mode 100644 index 7063bbfdd..000000000 --- a/src/middleware/lib/rsc-renderer.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PassThrough } from "node:stream"; -import type { Readable } from "node:stream"; -import { Worker } from "node:worker_threads"; - -export type Input = { - rscId?: string | undefined; - props?: Props | undefined; - rsfId?: string | undefined; - args?: unknown[] | undefined; -}; - -type Options = { - loadClientEntries?: boolean; - loadServerEntries?: boolean; - serverEntryCallback?: (rsfId: string, fileId: string) => void; -}; - -const execArgv = [ - "--conditions", - "react-server", - // TODO the use of process.env is temporary - ...(process.env.WAKUWORK_CMD === "dev" - ? ["--experimental-loader", "tsx"] - : []), - "--experimental-loader", - "wakuwork/node-loader", - "--experimental-loader", - "react-server-dom-webpack/node-loader", -]; - -const worker = new Worker(new URL("rsc-renderer-worker.js", import.meta.url), { - execArgv, -}); - -export type MessageReq = { - id: number; - type: "start"; - input: Input; - loadClientEntries: boolean | undefined; - loadServerEntries: boolean | undefined; - notifyServerEntry: boolean; -}; - -export type MessageRes = - | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } - | { id: number; type: "end" } - | { id: number; type: "err"; err: unknown } - | { id: number; type: "serverEntry"; rsfId: string; fileId: string }; - -const handlers = new Map void>(); - -worker.on("message", (mesg: MessageRes) => { - handlers.get(mesg.id)?.(mesg); -}); - -let nextId = 1; - -export function renderRSC(input: Input, options?: Options): Readable { - const id = nextId++; - const passthrough = new PassThrough(); - handlers.set(id, (mesg) => { - if (mesg.type === "buf") { - passthrough.write(Buffer.from(mesg.buf, mesg.offset, mesg.len)); - } else if (mesg.type === "end") { - passthrough.end(); - handlers.delete(id); - } else if (mesg.type === "err") { - passthrough.destroy( - mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err)) - ); - handlers.delete(id); - } else if (mesg.type === "serverEntry" && options?.serverEntryCallback) { - options.serverEntryCallback(mesg.rsfId, mesg.fileId); - } - }); - const mesg: MessageReq = { - id, - type: "start", - input, - loadClientEntries: options?.loadClientEntries, - loadServerEntries: options?.loadServerEntries, - notifyServerEntry: !!options?.serverEntryCallback - }; - worker.postMessage(mesg); - return passthrough; -} diff --git a/src/middleware/lib/rsc-utils.ts b/src/middleware/lib/rsc-utils.ts index ecbd46729..1afa3edb5 100644 --- a/src/middleware/lib/rsc-utils.ts +++ b/src/middleware/lib/rsc-utils.ts @@ -3,6 +3,11 @@ import { Buffer } from "node:buffer"; import { Transform } from "node:stream"; +export const codeToInject = ` +globalThis.__wakuwork_module_cache__ = new Map(); +globalThis.__webpack_chunk_load__ = (id) => import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); +globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);`; + export const generatePrefetchCode = ( entryItemsIterable: Iterable, moduleIds: Iterable @@ -10,7 +15,7 @@ export const generatePrefetchCode = ( const entryItems = Array.from(entryItemsIterable); let code = ""; if (entryItems.length) { - const rscIds = [...new Set(entryItems.map(([rscId]) => rscId))]; + const rscIds = Array.from(new Set(entryItems.map(([rscId]) => rscId))); code += ` globalThis.__WAKUWORK_PREFETCHED__ = { ${rscIds @@ -43,10 +48,7 @@ import('${moduleId}');`; }; // HACK Patching stream is very fragile. -export const transformRsfId = ( - prefixToRemove: string, - convert: (id: string) => string -) => +export const transformRsfId = (prefixToRemove: string) => new Transform({ transform(chunk, encoding, callback) { if (encoding !== ("buffer" as any)) { @@ -60,7 +62,7 @@ export const transformRsfId = ( new RegExp(`^([0-9]+):{"id":"${prefixToRemove}(.*?)"(.*)$`) ); if (match) { - lines[i] = `${match[1]}:{"id":"${convert(match[2])}"${match[3]}`; + lines[i] = `${match[1]}:{"id":"${match[2]}"${match[3]}`; changed = true; } } diff --git a/src/middleware/lib/vite-plugin-rsc.ts b/src/middleware/lib/vite-plugin-rsc.ts new file mode 100644 index 000000000..e809fedd4 --- /dev/null +++ b/src/middleware/lib/vite-plugin-rsc.ts @@ -0,0 +1,84 @@ +import path from "node:path"; +import type { Plugin } from "vite"; +import * as swc from "@swc/core"; +import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; + +export const rscTransformPlugin = (): Plugin => { + return { + name: "rsc-transform-plugin", + async resolveId(id, importer, options) { + if (!id.endsWith(".js")) { + return id; + } + // FIXME This isn't necessary in production mode + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { + ...options, + skipSelf: true, + }); + if (resolved) { + return resolved; + } + } + }, + async transform(code, id) { + const resolve = async ( + specifier: string, + { parentURL }: { parentURL: string } + ) => { + if (!specifier) { + return { url: "" }; + } + const url = (await this.resolve(specifier, parentURL, { + skipSelf: true, + }))!.id; + return { url }; + }; + const load = async (url: string) => { + let source = url === id ? code : (await this.load({ id: url })).code; + // HACK move directives before import statements. + source = source!.replace( + /^(import {.*?} from ".*?";)\s*"use (client|server)";/, + '"use $2";$1' + ); + return { format: "module", source }; + }; + RSDWNodeLoader.resolve( + "", + { conditions: ["react-server"], parentURL: "" }, + resolve + ); + return (await RSDWNodeLoader.load(id, null, load)).source; + }, + }; +}; + +export const rscReloadPlugin = (fn: (type: "full-reload") => void): Plugin => { + const isClientEntry = (id: string, code: string) => { + const ext = path.extname(id); + if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) { + const mod = swc.parseSync(code, { + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", + }); + for (const item of mod.body) { + if ( + item.type === "ExpressionStatement" && + item.expression.type === "StringLiteral" && + item.expression.value === "use client" + ) { + return true; + } + } + } + return false; + }; + return { + name: "reload-plugin", + async handleHotUpdate(ctx) { + if (ctx.modules.length && !isClientEntry(ctx.file, await ctx.read())) { + fn("full-reload"); + } + }, + }; +}; diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 25a84b195..328e91127 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -1,62 +1,17 @@ -import path from "node:path"; - import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import type { Prefetcher } from "../server.js"; -import { generatePrefetchCode } from "./lib/rsc-utils.js"; - -import { renderRSC } from "./lib/rsc-renderer.js"; +import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -const rscDev: MiddlewareCreator = (config, shared) => { - const dir = path.resolve(config.devServer?.dir || "."); - - const entriesFile = - (process.platform === "win32" ? "file://" : "") + - path.join(dir, config.files?.entriesJs || "entries.js"); - const prefetcher: Prefetcher = async (pathItem) => { - const mod = await import(entriesFile); - return mod?.prefetcher(pathItem) ?? {}; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(dir), id); - id = "/" + decodeURI(id); - } - return [id, name]; - }; - - shared.devScriptToInject = async (path: string) => { - let code = ` -globalThis.__wakuwork_module_cache__ = new Map(); -globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); -globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id); -`; - const { entryItems = [], clientModules = [] } = await prefetcher(path); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; - return code; - }; - +const rscDev: MiddlewareCreator = () => { return async (req, res, next) => { const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; if (Array.isArray(rscId) || Array.isArray(rsfId)) { - throw new Error('rscId and rsfId should not be array') + throw new Error("rscId and rsfId should not be array"); } let props = {}; if (rscId) { @@ -84,7 +39,13 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge } } if (rscId || rsfId) { - renderRSC({ rscId, props, rsfId, args }).pipe(res); + const pipeable = renderRSC({ rscId, props, rsfId, args }); + pipeable.on("error", (err) => { + console.info("Cannot render RSC", err); + res.statusCode = 500; + res.end(String(err)); + }); + pipeable.pipe(res); return; } await next(); diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 095da3da1..7d086bfc2 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -1,77 +1,17 @@ -import path from "node:path"; - import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import type { Prefetcher } from "../server.js"; -import { generatePrefetchCode } from "./lib/rsc-utils.js"; -import { renderRSC } from "./lib/rsc-renderer.js"; +import { renderRSC, setClientEntries } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -// TODO we have duplicate code here and rsc-renderer-worker.ts - -const rscPrd: MiddlewareCreator = (config, shared) => { - const dir = path.resolve(config.prdServer?.dir || "."); - const basePath = config.build?.basePath || "/"; // FIXME it's not build only - - const entriesFile = - (process.platform === "win32" ? "file://" : "") + - path.join(dir, config.files?.entriesJs || "entries.js"); - const prefetcher: Prefetcher = async (pathItem) => { - const mod = await import(entriesFile); - return mod?.prefetcher(pathItem) ?? {}; - }; - let clientEntries: Record | undefined; - import(entriesFile).then((mod) => { - clientEntries = mod.clientEntries; - }); - - const getClientEntry = (id: string) => { - if (!clientEntries) { - throw new Error("Missing client entries"); - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(dir), id); - id = basePath + getClientEntry(decodeURI(id)); - } - return [id, name]; - }; - - shared.prdScriptToInject = async (path: string) => { - let code = ""; - if (prefetcher) { - const { entryItems = [], clientModules = [] } = await prefetcher(path); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; - } - return code; - }; +// FIXME we have duplicate code here and rscDev.ts +const rscPrd: MiddlewareCreator = () => { + const promise = setClientEntries("load"); return async (req, res, next) => { + await promise; const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; if (Array.isArray(rscId) || Array.isArray(rsfId)) { @@ -103,10 +43,13 @@ const rscPrd: MiddlewareCreator = (config, shared) => { } } if (rscId || rsfId) { - renderRSC( - { rscId, props, rsfId, args }, - { loadClientEntries: true, loadServerEntries: true } - ).pipe(res); + const pipeable = renderRSC({ rscId, props, rsfId, args }); + pipeable.on("error", (err) => { + console.info("Cannot render RSC", err); + res.statusCode = 500; + res.end(); + }); + pipeable.pipe(res); return; } await next(); diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index 0bda56511..3c6ed0cf9 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -6,43 +6,57 @@ import type { Plugin } from "vite"; import react from "@vitejs/plugin-react"; import type { MiddlewareCreator } from "./lib/common.js"; +import { codeToInject } from "./lib/rsc-utils.js"; +import { registerReloadCallback, setClientEntries } from "./lib/rsc-handler.js"; -const rscPlugin = ( - scriptToInject: (path: string) => Promise -): Plugin => { +const rscIndexPlugin = (): Plugin => { return { - name: "rscPlugin", - async transformIndexHtml(_html, ctx) { - const code = await scriptToInject(ctx.path); - if (code) { - return [ - { - tag: "script", - children: code, - injectTo: "body", - }, - ]; - } + name: "rsc-index-plugin", + async transformIndexHtml() { + return [ + { + tag: "script", + children: codeToInject, + injectTo: "body", + }, + ]; }, }; }; -const viteServer: MiddlewareCreator = (config, shared) => { +const viteServer: MiddlewareCreator = (config) => { const dir = path.resolve(config.devServer?.dir || "."); const indexHtml = config.files?.indexHtml || "index.html"; const indexHtmlFile = path.join(dir, indexHtml); const vitePromise = createServer({ root: dir, + optimizeDeps: { + include: ["react-server-dom-webpack/client"], + // FIXME without this, wakuwork router has dual module hazard, + // and "Uncaught Error: Missing Router" happens. + exclude: ["wakuwork"], + }, plugins: [ // @ts-ignore react(), - rscPlugin(async (path: string) => shared.devScriptToInject?.(path) || ""), + rscIndexPlugin(), ], server: { middlewareMode: true }, appType: "custom", }); + vitePromise.then((vite) => { + registerReloadCallback((type) => vite.ws.send({ type })); + }); return async (req, res, next) => { const vite = await vitePromise; + const absoluteClientEntries = Object.fromEntries( + Array.from(vite.moduleGraph.idToModuleMap.values()).map( + ({ file, url }) => [file, url] + ) + ); + absoluteClientEntries["*"] = "*"; // HACK to use fallback resolver + // FIXME this is bad in performance, let's revisit it + await setClientEntries(absoluteClientEntries); const indexFallback = async () => { const url = new URL(req.url || "", "http://" + req.headers.host); // TODO make it configurable? diff --git a/src/node-loader.ts b/src/node-loader.ts deleted file mode 100644 index d93e8227f..000000000 --- a/src/node-loader.ts +++ /dev/null @@ -1,39 +0,0 @@ -export async function resolve( - specifier: string, - context: any, - nextResolve: any -) { - if (specifier.endsWith(".js")) { - // Hoped tsx handles it, but doesn't seem so. - for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { - try { - return await nextResolve( - specifier.slice(0, -3) + ext, - context, - nextResolve - ); - } catch (e) { - // ignored - } - } - } - return await nextResolve(specifier, context, nextResolve); -} - -export async function load(url: string, context: any, nextLoad: any) { - const result = await nextLoad(url, context, nextLoad); - if (result.format === "module") { - let { source } = result; - if (typeof source !== "string") { - source = source.toString(); - } - // HACK pull directive to the root - // Hope we can configure tsx to avoid this - const p = source.match(/(?:^|\n|;)("use (client|server)";)/); - if (p) { - source = p[1] + source; - } - return { ...result, source }; - } - return result; -} diff --git a/src/prdServer.ts b/src/prdServer.ts index b2eb5c821..1e54a8814 100644 --- a/src/prdServer.ts +++ b/src/prdServer.ts @@ -2,10 +2,8 @@ import http from "node:http"; import type { Config, Middleware } from "./config.js"; import { pipe } from "./middleware/lib/common.js"; -import type { Shared } from "./middleware/lib/common.js"; export function startPrdServer(config: Config = {}) { - const shared: Shared = {}; const middlewares = config.prdServer?.middlewares || [ "staticFile", "rewriteRsc", @@ -17,7 +15,7 @@ export function startPrdServer(config: Config = {}) { middlewares.map(async (middleware) => { if (typeof middleware === "string") { const mod = await import(`./middleware/${middleware}.js`); - return (mod.default || mod)(config, shared); + return (mod.default || mod)(config); } return middleware; }) diff --git a/src/router/client.ts b/src/router/client.ts index 7a1f037a0..aa4da7c24 100644 --- a/src/router/client.ts +++ b/src/router/client.ts @@ -1,5 +1,7 @@ /// +"use client"; + import { cache, createContext, @@ -12,7 +14,6 @@ import { } from "react"; import { serve } from "../client.js"; -import { WAKUWORK_ROUTER } from "./common.js"; import type { RouteProps, ChildProps, LinkProps } from "./common.js"; type ChangeLocation = ( @@ -42,11 +43,8 @@ export function useLocation() { return value.location; } -// FIXME normalizing `search` before prefetch would be necessary. -// FIXME ommitting `search` items would be important for caching. - -// TODO prefetching dependent client modules in not supported yet. -// Is it only possible with build step? No runtime solution? +// FIXME normalizing `search` before prefetch might be good. +// FIXME selective `search` would be better. (for intermediate routes too) const prefetchRoutes = (pathname: string, search: string) => { const prefetched = ((globalThis as any).__WAKUWORK_PREFETCHED__ ||= {}); @@ -54,7 +52,7 @@ const prefetchRoutes = (pathname: string, search: string) => { for (let index = 0; index <= pathItems.length; ++index) { const rscId = pathItems.slice(0, index).join("/") || "index"; const props: RouteProps = - index < pathItems.length ? { childIndex: index + 1, search } : { search }; + index < pathItems.length ? { childIndex: index + 1 } : { search }; // FIXME we blindly expect JSON.stringify usage is deterministic const serializedProps = JSON.stringify(props); if (!prefetched[rscId]) { @@ -73,7 +71,7 @@ const prefetchRoutes = (pathname: string, search: string) => { const getRoute = cache((rscId: string) => serve(rscId)); -const Child = ({ index }: ChildProps) => { +export function Child({ index }: ChildProps) { const { pathname, search } = useLocation(); const pathItems = pathname.split("/").filter(Boolean); if (index > pathItems.length) { @@ -82,9 +80,15 @@ const Child = ({ index }: ChildProps) => { const rscId = pathItems.slice(0, index).join("/") || "index"; return createElement( getRoute(rscId), - index < pathItems.length ? { childIndex: index + 1, search } : { search } + index < pathItems.length + ? { + childIndex: index + 1, // we still have a child route + } + : { + search, // attach `search` only for a leaf route for now + } ); -}; +} export function Link({ href, @@ -122,12 +126,6 @@ export function Link({ return ele; } -// FIXME Eventually, if we have server module graph, we could omit this hack. -(globalThis as any).__wakuwork_module_cache__.set(WAKUWORK_ROUTER, { - Child, - Link, -}); - const parseLocation = () => { const { pathname, search } = window.location; return { pathname, search }; diff --git a/src/router/common.ts b/src/router/common.ts index 19ab64f92..1d57ba101 100644 --- a/src/router/common.ts +++ b/src/router/common.ts @@ -1,13 +1,8 @@ import type { ReactNode } from "react"; -export type RouteProps = { - childIndex?: number; - search: string; -}; +export type RouteProps = { childIndex: number } | { search: string }; -export type ChildProps = { - index: number; -}; +export type ChildProps = { index: number }; export type LinkProps = { href: string; @@ -16,19 +11,3 @@ export type LinkProps = { notPending?: ReactNode; unstable_prefetchOnEnter?: boolean; }; - -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -export const WAKUWORK_ROUTER = "wakuwork/router"; - -export const childReference = Object.defineProperties({} as any, { - $$typeof: { value: CLIENT_REFERENCE }, - $$id: { value: WAKUWORK_ROUTER + "#Child" }, - $$async: { value: false }, -}); - -export const linkReference = Object.defineProperties({} as any, { - $$typeof: { value: CLIENT_REFERENCE }, - $$id: { value: WAKUWORK_ROUTER + "#Link" }, - $$async: { value: false }, -}); diff --git a/src/router/server.ts b/src/router/server.ts index a2389003d..25f4feb48 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -3,10 +3,38 @@ import fs from "node:fs"; import { createElement } from "react"; import * as swc from "@swc/core"; -import type { GetEntry, Prefetcher, Prerenderer } from "../server.js"; +import type { GetEntry, GetBuilder, GetCustomModules } from "../server.js"; -import { childReference, linkReference } from "./common.js"; import type { RouteProps, LinkProps } from "./common.js"; +import { Child as ClientChild, Link as ClientLink } from "./client.js"; + +const getAllFiles = (base: string, parent = ""): string[] => + fs + .readdirSync(path.join(base, parent), { withFileTypes: true }) + .flatMap((dirent) => { + if (dirent.isDirectory()) { + return getAllFiles(base, path.join(parent, dirent.name)); + } + const fname = path.join(parent, dirent.name); + return [fname]; + }); + +const getAllPaths = (base: string, parent = ""): string[] => + fs + .readdirSync(path.join(base, parent), { withFileTypes: true }) + .flatMap((dirent) => { + if (dirent.isDirectory()) { + return getAllPaths(base, path.join(parent, dirent.name)); + } + const fname = path.join(parent, path.parse(dirent.name).name); + const stat = fs.statSync(path.join(base, fname), { + throwIfNoEntry: false, + }); + if (stat?.isDirectory()) { + return [fname + "/"]; + } + return [fname]; + }); const CLIENT_REFERENCE = Symbol.for("react.client.reference"); @@ -20,12 +48,13 @@ const resolveFileName = (fname: string) => { throw new Error(`Cannot resolve file ${fname}`); }; +// XXX Can we avoid doing this here? const findDependentModules = (fname: string) => { fname = resolveFileName(fname); - // TODO support ".js" and ".jsx" + const ext = path.extname(fname); const mod = swc.parseFileSync(fname, { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", }); const modules: (readonly [fname: string, exportNames: string[]])[] = []; for (const item of mod.body) { @@ -53,103 +82,84 @@ const findDependentModules = (fname: string) => { return modules; }; -export function fileRouter(base: string) { - const findClientModules = async (id: string) => { - const fname = `${base}/${id}.js`; - const modules = findDependentModules(fname); - return ( - await Promise.all( - modules.map(async ([fname, exportNames]) => { - const m = await import(fname); - return exportNames.flatMap((name) => { - if (m[name]?.["$$typeof"] === CLIENT_REFERENCE) { - return [m[name]]; - } - return []; - }); - }) - ) - ).flat(); - }; - - const getAllPaths = (dir = ""): string[] => - fs - .readdirSync(path.join(base, dir), { withFileTypes: true }) - .flatMap((dirent) => { - if (dirent.isDirectory()) { - return getAllPaths(path.join(dir, dirent.name)); - } - const fname = path.join(dir, path.parse(dirent.name).name); - const stat = fs.statSync(path.join(base, fname), { - throwIfNoEntry: false, +const findClientModules = async (base: string, id: string) => { + const fname = `${base}/${id}.js`; + const modules = findDependentModules(fname); + return ( + await Promise.all( + modules.map(async ([fname, exportNames]) => { + const m = await import(/* @vite-ignore */ fname); + return exportNames.flatMap((name) => { + if (m[name]?.["$$typeof"] === CLIENT_REFERENCE) { + return [m[name]]; + } + return []; }); - if (stat?.isDirectory()) { - return [fname + "/"]; - } - return [fname]; - }); + }) + ) + ).flat(); +}; +export function fileRouter(baseDir: string, routesPath: string) { + const base = path.join(baseDir, routesPath); const getEntry: GetEntry = async (id) => { // This can be too unsecure? FIXME - const component = (await import(`${base}/${id}.js`)).default; + const component = (await import(/* @vite-ignore */ `${base}/${id}.js`)) + .default; const RouteComponent: any = (props: RouteProps) => { const componentProps: Record = {}; - for (const [key, value] of new URLSearchParams(props.search)) { - componentProps[key] = value; + if ("search" in props) { + for (const [key, value] of new URLSearchParams(props.search)) { + componentProps[key] = value; + } } return createElement( component, componentProps, - props.childIndex - ? createElement(childReference, { index: props.childIndex }) + "childIndex" in props + ? createElement(ClientChild, { index: props.childIndex }) : null ); }; return RouteComponent; }; - const prefetcher: Prefetcher = async (path) => { - const url = new URL(path || "", "http://localhost"); - const result: (readonly [id: string, props: RouteProps])[] = []; + // We have to make prefetcher consistent with client behavior + const prefetcher = async (pathStr: string) => { + const url = new URL(pathStr, "http://localhost"); + const elements: (readonly [id: string, props: RouteProps])[] = []; const pathItems = url.pathname.split("/").filter(Boolean); const search = url.search; for (let index = 0; index <= pathItems.length; ++index) { const rscId = pathItems.slice(0, index).join("/") || "index"; - result.push([ + elements.push([ rscId, - index < pathItems.length - ? { childIndex: index + 1, search } - : { search }, + index < pathItems.length ? { childIndex: index + 1 } : { search }, ]); } const clientModules = new Set( ( - await Promise.all(result.map(([rscId]) => findClientModules(rscId))) + await Promise.all( + elements.map(([rscId]) => findClientModules(base, rscId)) + ) ).flat() ); - return { - entryItems: result, - clientModules, - }; + return { elements, clientModules }; }; - const prerenderer: Prerenderer = async () => { - const paths = getAllPaths().map((item) => + const getBuilder: GetBuilder = async ( + decodeId: (encodedId: string) => [id: string, name: string] + ) => { + const paths = getAllPaths(base).map((item) => item === "index" ? "/" : `/${item}` ); const prefetcherForPaths = await Promise.all(paths.map(prefetcher)); - const unstable_customCode = ( - _path: string, - decodeId: (encodedId: string) => [id: string, name: string] - ) => ` + const customCode = ` globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { const path = search ? pathname + "?" + search : pathname; const path2ids = {${paths.map((pathItem, index) => { const moduleIds: string[] = []; - for (const m of prefetcherForPaths[index]?.clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } + for (const m of prefetcherForPaths[index]?.clientModules || []) { const [id] = decodeId(m["$$id"]); moduleIds.push(id); } @@ -161,18 +171,31 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { import(id); } };`; - return { - entryItems: Array.from(prefetcherForPaths.values()).flatMap((item) => - Array.from(item.entryItems || []) - ), - paths, - unstable_customCode, - }; + return Object.fromEntries( + paths.map((pathStr, index) => { + return [ + pathStr, + { + elements: prefetcherForPaths[index]?.elements || [], + customCode, + }, + ]; + }) + ); + }; + + const getCustomModules: GetCustomModules = async () => { + return Object.fromEntries( + getAllFiles(base).map((file) => [ + `${routesPath}/${file.replace(/\.\w+$/, "")}`, + `${base}/${file}`, + ]) + ); }; - return { getEntry, prefetcher, prerenderer }; + return { getEntry, getBuilder, getCustomModules }; } export function Link(props: LinkProps) { - return createElement(linkReference, props); + return createElement(ClientLink, props); } diff --git a/src/server.ts b/src/server.ts index e2ec10215..899585613 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,21 +1,27 @@ import type { FunctionComponent } from "react"; +// TODO revisit entries API +// - prefer export default? +// - return null from getEntry for 404, instead of throwing + export type GetEntry = ( rscId: string ) => Promise; -// For run-time optimization (plus, for build-time optimization with `paths`) -export type Prefetcher = (path: string) => Promise<{ - entryItems?: Iterable; - clientModules?: Iterable; +export type GetBuilder = ( + // FIXME can we somehow avoid leaking internal implementation? + unstable_decodeId: (encodedId: string) => [id: string, name: string] +) => Promise<{ + [pathStr: string]: { + elements?: Iterable< + readonly [rscId: string, props: unknown, skipPrefetch?: boolean] + >; + customCode?: string; // optional code to inject + }; }>; -// For build-time optimization -export type Prerenderer = () => Promise<{ - entryItems?: Iterable; - paths?: Iterable; - unstable_customCode?: ( - path: string, - decodeId: (encodedId: string) => [id: string, name: string] - ) => string; +// This is for ignored dynamic imports +// XXX Are there any better ways? +export type GetCustomModules = () => Promise<{ + [name: string]: string; }>; diff --git a/src/types.d.ts b/src/types.d.ts index a2d043aab..83441e7ee 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,4 @@ -declare module "react-server-dom-webpack/node-register"; +declare module "react-server-dom-webpack/node-loader"; declare module "react-server-dom-webpack/server"; declare module "react-server-dom-webpack/server.node.unbundled"; declare module "react-server-dom-webpack/client"; diff --git a/website/entries.ts b/website/entries.ts index b749bdb90..7a6ddc3a7 100644 --- a/website/entries.ts +++ b/website/entries.ts @@ -3,6 +3,7 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; -export const { getEntry, prefetcher, prerenderer } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") +export const { getEntry, getBuilder, getCustomModules } = fileRouter( + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); diff --git a/website/routes/practices/router.tsx b/website/routes/practices/router.tsx index d8d59dd0a..866ce31f0 100644 --- a/website/routes/practices/router.tsx +++ b/website/routes/practices/router.tsx @@ -13,7 +13,8 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; export const { getEntry, prefetcher, prerenderer } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); `; diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 000000000..7781cf1ae --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "downlevelIteration": true, + "esModuleInterop": true, + "module": "nodenext", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "jsx": "react-jsx" + } +}