From 1f48985a1c538686dd9d42b843686a67279cec16 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 19 Jan 2025 14:04:31 +0400 Subject: [PATCH] experimental: add docker template to CLI (#4744) Here added first react-router template which additionally adds docker support. For now only adds Dockerfile + .dockerignore. Will add support for image transforms separately. Mostly duplicated defaults template - replaced remix packages with react-router ones - replaced `return redirect()` with `throw redirect()` The template for now is self contained. Will split base react-router template when will add more integrations. --- fixtures/react-router-docker/.dockerignore | 4 + fixtures/react-router-docker/.gitignore | 6 + fixtures/react-router-docker/.npmrc | 3 + fixtures/react-router-docker/.template/.npmrc | 3 + .../.template/package.json | 12 + .../.template/tsconfig.json | 5 + .../.webstudio/config.json | 3 + .../react-router-docker/.webstudio/data.json | 500 ++++++++++++++++++ fixtures/react-router-docker/Dockerfile | 22 + fixtures/react-router-docker/README.md | 20 + .../__generated__/$resources.sitemap.xml.ts | 6 + .../[another-page]._index.server.tsx | 39 ++ .../__generated__/[another-page]._index.tsx | 37 ++ .../app/__generated__/_index.server.tsx | 39 ++ .../app/__generated__/_index.tsx | 64 +++ .../app/__generated__/index.css | 118 +++++ .../react-router-docker/app/constants.mjs | 13 + fixtures/react-router-docker/app/extension.ts | 13 + fixtures/react-router-docker/app/root.tsx | 35 ++ fixtures/react-router-docker/app/routes.ts | 4 + .../app/routes/[another-page]._index.tsx | 295 +++++++++++ .../app/routes/[robots.txt].tsx | 24 + .../app/routes/[sitemap.xml]._index.tsx | 34 ++ .../react-router-docker/app/routes/_index.tsx | 295 +++++++++++ fixtures/react-router-docker/package.json | 49 ++ ...at-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg | Bin 0 -> 64701 bytes ...verted-converted_zMaMiAAutUl8XrITgz7d1.svg | 1 + .../react-router-docker/public/favicon.ico | Bin 0 -> 15086 bytes fixtures/react-router-docker/tsconfig.json | 19 + fixtures/react-router-docker/vite.config.ts | 6 + packages/cli/package.json | 3 + packages/cli/src/config.ts | 5 + packages/cli/src/framework-react-router.ts | 87 +++ packages/cli/src/prebuild.ts | 3 + .../react-router-docker/.dockerignore | 4 + .../templates/react-router-docker/.gitignore | 6 + .../templates/react-router-docker/Dockerfile | 22 + .../react-router-docker/app/constants.mjs | 13 + .../react-router-docker/app/extension.ts | 13 + .../react-router-docker/app/root.tsx | 35 ++ .../app/route-templates/default-sitemap.tsx | 34 ++ .../app/route-templates/html.tsx | 295 +++++++++++ .../app/route-templates/redirect.tsx | 6 + .../app/route-templates/xml.tsx | 85 +++ .../app/routes/[robots.txt].tsx | 24 + .../react-router-docker/package.json | 34 ++ .../react-router-docker/public/favicon.ico | Bin 0 -> 15086 bytes .../react-router-docker/tsconfig.json | 18 + .../react-router-docker/vite.config.ts | 6 + .../sdk-components-react-router/src/metas.ts | 2 +- pnpm-lock.yaml | 381 ++++++++++--- 51 files changed, 2668 insertions(+), 77 deletions(-) create mode 100644 fixtures/react-router-docker/.dockerignore create mode 100644 fixtures/react-router-docker/.gitignore create mode 100644 fixtures/react-router-docker/.npmrc create mode 100644 fixtures/react-router-docker/.template/.npmrc create mode 100644 fixtures/react-router-docker/.template/package.json create mode 100644 fixtures/react-router-docker/.template/tsconfig.json create mode 100644 fixtures/react-router-docker/.webstudio/config.json create mode 100644 fixtures/react-router-docker/.webstudio/data.json create mode 100644 fixtures/react-router-docker/Dockerfile create mode 100644 fixtures/react-router-docker/README.md create mode 100644 fixtures/react-router-docker/app/__generated__/$resources.sitemap.xml.ts create mode 100644 fixtures/react-router-docker/app/__generated__/[another-page]._index.server.tsx create mode 100644 fixtures/react-router-docker/app/__generated__/[another-page]._index.tsx create mode 100644 fixtures/react-router-docker/app/__generated__/_index.server.tsx create mode 100644 fixtures/react-router-docker/app/__generated__/_index.tsx create mode 100644 fixtures/react-router-docker/app/__generated__/index.css create mode 100644 fixtures/react-router-docker/app/constants.mjs create mode 100644 fixtures/react-router-docker/app/extension.ts create mode 100644 fixtures/react-router-docker/app/root.tsx create mode 100644 fixtures/react-router-docker/app/routes.ts create mode 100644 fixtures/react-router-docker/app/routes/[another-page]._index.tsx create mode 100644 fixtures/react-router-docker/app/routes/[robots.txt].tsx create mode 100644 fixtures/react-router-docker/app/routes/[sitemap.xml]._index.tsx create mode 100644 fixtures/react-router-docker/app/routes/_index.tsx create mode 100644 fixtures/react-router-docker/package.json create mode 100644 fixtures/react-router-docker/public/assets/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg create mode 100644 fixtures/react-router-docker/public/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg create mode 100644 fixtures/react-router-docker/public/favicon.ico create mode 100644 fixtures/react-router-docker/tsconfig.json create mode 100644 fixtures/react-router-docker/vite.config.ts create mode 100644 packages/cli/src/framework-react-router.ts create mode 100644 packages/cli/templates/react-router-docker/.dockerignore create mode 100644 packages/cli/templates/react-router-docker/.gitignore create mode 100644 packages/cli/templates/react-router-docker/Dockerfile create mode 100644 packages/cli/templates/react-router-docker/app/constants.mjs create mode 100644 packages/cli/templates/react-router-docker/app/extension.ts create mode 100644 packages/cli/templates/react-router-docker/app/root.tsx create mode 100644 packages/cli/templates/react-router-docker/app/route-templates/default-sitemap.tsx create mode 100644 packages/cli/templates/react-router-docker/app/route-templates/html.tsx create mode 100644 packages/cli/templates/react-router-docker/app/route-templates/redirect.tsx create mode 100644 packages/cli/templates/react-router-docker/app/route-templates/xml.tsx create mode 100644 packages/cli/templates/react-router-docker/app/routes/[robots.txt].tsx create mode 100644 packages/cli/templates/react-router-docker/package.json create mode 100644 packages/cli/templates/react-router-docker/public/favicon.ico create mode 100644 packages/cli/templates/react-router-docker/tsconfig.json create mode 100644 packages/cli/templates/react-router-docker/vite.config.ts diff --git a/fixtures/react-router-docker/.dockerignore b/fixtures/react-router-docker/.dockerignore new file mode 100644 index 000000000000..54f78fa7b671 --- /dev/null +++ b/fixtures/react-router-docker/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md diff --git a/fixtures/react-router-docker/.gitignore b/fixtures/react-router-docker/.gitignore new file mode 100644 index 000000000000..9b7c041f96ea --- /dev/null +++ b/fixtures/react-router-docker/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/fixtures/react-router-docker/.npmrc b/fixtures/react-router-docker/.npmrc new file mode 100644 index 000000000000..43ce02f127ad --- /dev/null +++ b/fixtures/react-router-docker/.npmrc @@ -0,0 +1,3 @@ +force=true +# to support using NODE_OPTIONS for windows tests +shell-emulator=true diff --git a/fixtures/react-router-docker/.template/.npmrc b/fixtures/react-router-docker/.template/.npmrc new file mode 100644 index 000000000000..43ce02f127ad --- /dev/null +++ b/fixtures/react-router-docker/.template/.npmrc @@ -0,0 +1,3 @@ +force=true +# to support using NODE_OPTIONS for windows tests +shell-emulator=true diff --git a/fixtures/react-router-docker/.template/package.json b/fixtures/react-router-docker/.template/package.json new file mode 100644 index 000000000000..b21dce86c93a --- /dev/null +++ b/fixtures/react-router-docker/.template/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@webstudio-is/image": "workspace:*", + "@webstudio-is/react-sdk": "workspace:*", + "@webstudio-is/sdk": "workspace:*", + "@webstudio-is/sdk-components-animation": "workspace:*", + "@webstudio-is/sdk-components-react": "workspace:*", + "@webstudio-is/sdk-components-react-radix": "workspace:*", + "@webstudio-is/sdk-components-react-router": "workspace:*", + "webstudio": "workspace:*" + } +} diff --git a/fixtures/react-router-docker/.template/tsconfig.json b/fixtures/react-router-docker/.template/tsconfig.json new file mode 100644 index 000000000000..75cac78946fd --- /dev/null +++ b/fixtures/react-router-docker/.template/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "customConditions": ["webstudio"] + } +} diff --git a/fixtures/react-router-docker/.webstudio/config.json b/fixtures/react-router-docker/.webstudio/config.json new file mode 100644 index 000000000000..3bad791718c2 --- /dev/null +++ b/fixtures/react-router-docker/.webstudio/config.json @@ -0,0 +1,3 @@ +{ + "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce" +} diff --git a/fixtures/react-router-docker/.webstudio/data.json b/fixtures/react-router-docker/.webstudio/data.json new file mode 100644 index 000000000000..7dc933cfb601 --- /dev/null +++ b/fixtures/react-router-docker/.webstudio/data.json @@ -0,0 +1,500 @@ +{ + "build": { + "id": "f565d527-32e7-4731-bc71-aca9e9574587", + "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", + "version": 43, + "createdAt": "2025-01-04T11:01:50.091+00:00", + "updatedAt": "2025-01-04T11:01:50.091+00:00", + "pages": { + "meta": { + "siteName": "", + "faviconAssetId": "d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd", + "code": "" + }, + "homePage": { + "id": "9di_L14CzctvSruIoKVvE", + "name": "Home", + "title": "\"Home\"", + "rootInstanceId": "MMimeobf_zi4ZkRGXapju", + "systemDataSourceId": "2KT4-bRzToj9cAGAN_woK", + "meta": {}, + "path": "" + }, + "pages": [ + { + "id": "WPPAbLFyJD_02vhjRd8P4", + "name": "Another page", + "title": "\"Another page\"", + "history": ["/another-page"], + "rootInstanceId": "n_VBMr7klpx25buS0NV7R", + "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", + "meta": { + "description": "\"\"", + "excludePageFromSearch": "true", + "language": "\"\"", + "socialImageUrl": "\"\"", + "status": "200", + "redirect": "\"\"", + "documentType": "html", + "custom": [ + { + "property": "", + "content": "\"\"" + } + ] + }, + "marketplace": { + "include": false + }, + "path": "/another-page" + } + ], + "folders": [ + { + "id": "root", + "name": "Root", + "slug": "", + "children": ["9di_L14CzctvSruIoKVvE", "WPPAbLFyJD_02vhjRd8P4"] + } + ] + }, + "breakpoints": [ + [ + "rKj-wYctg3-GnqL3WHN9I", + { + "id": "rKj-wYctg3-GnqL3WHN9I", + "label": "Base" + } + ], + [ + "yH9RXhqCyeaVkrOt8MzLc", + { + "id": "yH9RXhqCyeaVkrOt8MzLc", + "label": "Tablet", + "maxWidth": 991 + } + ], + [ + "8nSCZbeS002IVwkTdoIes", + { + "id": "8nSCZbeS002IVwkTdoIes", + "label": "Mobile landscape", + "maxWidth": 767 + } + ], + [ + "7gBD25KrrbBdJYNDlhPz7", + { + "id": "7gBD25KrrbBdJYNDlhPz7", + "label": "Mobile portrait", + "maxWidth": 479 + } + ] + ], + "styles": [ + [ + "7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "7_QL45cpvP-zG8Hkgf4cr", + "property": "display", + "value": { + "type": "keyword", + "value": "flex" + } + } + ], + [ + "7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "7_QL45cpvP-zG8Hkgf4cr", + "property": "alignItems", + "value": { + "type": "keyword", + "value": "center" + } + } + ], + [ + "7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "7_QL45cpvP-zG8Hkgf4cr", + "property": "justifyContent", + "value": { + "type": "keyword", + "value": "center" + } + } + ], + [ + "7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "7_QL45cpvP-zG8Hkgf4cr", + "property": "flexDirection", + "value": { + "type": "keyword", + "value": "column" + } + } + ], + [ + "0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "0KA68BwP9gdTzE1ESO2Zp", + "property": "marginBottom", + "value": { + "type": "unit", + "unit": "em", + "value": 1 + } + } + ], + [ + "mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:", + { + "breakpointId": "rKj-wYctg3-GnqL3WHN9I", + "styleSourceId": "mf2C07UBmGT7y_G4Du3yg", + "property": "width", + "value": { + "type": "unit", + "unit": "px", + "value": 400 + } + } + ] + ], + "styleSources": [ + [ + "7_QL45cpvP-zG8Hkgf4cr", + { + "type": "local", + "id": "7_QL45cpvP-zG8Hkgf4cr" + } + ], + [ + "0KA68BwP9gdTzE1ESO2Zp", + { + "type": "local", + "id": "0KA68BwP9gdTzE1ESO2Zp" + } + ], + [ + "mf2C07UBmGT7y_G4Du3yg", + { + "type": "local", + "id": "mf2C07UBmGT7y_G4Du3yg" + } + ] + ], + "styleSourceSelections": [ + [ + "MMimeobf_zi4ZkRGXapju", + { + "instanceId": "MMimeobf_zi4ZkRGXapju", + "values": ["7_QL45cpvP-zG8Hkgf4cr"] + } + ], + [ + "BMJfjOzunWs8XkQgvvx1e", + { + "instanceId": "BMJfjOzunWs8XkQgvvx1e", + "values": ["0KA68BwP9gdTzE1ESO2Zp"] + } + ], + [ + "uHB3Fjb7-NELG-bnH7bXB", + { + "instanceId": "uHB3Fjb7-NELG-bnH7bXB", + "values": ["mf2C07UBmGT7y_G4Du3yg"] + } + ] + ], + "props": [ + [ + "1p34InvRgqoKVqeNZ1uBb", + { + "id": "1p34InvRgqoKVqeNZ1uBb", + "instanceId": "pjkZo5EiBqaeUXBcyHf_O", + "name": "href", + "type": "page", + "value": "WPPAbLFyJD_02vhjRd8P4" + } + ], + [ + "su3ag3OxH9WTBjJg5eIyg", + { + "id": "su3ag3OxH9WTBjJg5eIyg", + "instanceId": "uHB3Fjb7-NELG-bnH7bXB", + "name": "src", + "type": "asset", + "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" + } + ], + [ + "vGCYpBBB1QUPIPPIdyexn", + { + "id": "vGCYpBBB1QUPIPPIdyexn", + "instanceId": "uHB3Fjb7-NELG-bnH7bXB", + "name": "width", + "type": "asset", + "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" + } + ], + [ + "JKAGY7DWpciEl0UdnWuKL", + { + "id": "JKAGY7DWpciEl0UdnWuKL", + "instanceId": "uHB3Fjb7-NELG-bnH7bXB", + "name": "height", + "type": "asset", + "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" + } + ], + [ + "CAkmmL8-JAgokmeopoFXh", + { + "id": "CAkmmL8-JAgokmeopoFXh", + "instanceId": "2sIE8GxbKRBaav_zdhaZ1", + "name": "src", + "type": "string", + "value": "https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale" + } + ] + ], + "dataSources": [ + [ + "2KT4-bRzToj9cAGAN_woK", + { + "type": "parameter", + "id": "2KT4-bRzToj9cAGAN_woK", + "scopeInstanceId": "MMimeobf_zi4ZkRGXapju", + "name": "system" + } + ], + [ + "tdXe9gFf83hSo9BLWU6xl", + { + "type": "parameter", + "id": "tdXe9gFf83hSo9BLWU6xl", + "scopeInstanceId": "n_VBMr7klpx25buS0NV7R", + "name": "system" + } + ] + ], + "resources": [], + "instances": [ + [ + "MMimeobf_zi4ZkRGXapju", + { + "type": "instance", + "id": "MMimeobf_zi4ZkRGXapju", + "component": "Body", + "children": [ + { + "type": "id", + "value": "MYDt0guk1-vzc7yzqyN6A" + }, + { + "type": "id", + "value": "BMJfjOzunWs8XkQgvvx1e" + }, + { + "type": "id", + "value": "pjkZo5EiBqaeUXBcyHf_O" + }, + { + "type": "id", + "value": "uHB3Fjb7-NELG-bnH7bXB" + }, + { + "type": "id", + "value": "2sIE8GxbKRBaav_zdhaZ1" + } + ] + } + ], + [ + "MYDt0guk1-vzc7yzqyN6A", + { + "type": "instance", + "id": "MYDt0guk1-vzc7yzqyN6A", + "component": "Heading", + "label": "xD", + "children": [ + { + "type": "text", + "value": "Simple Project to test CLI" + } + ] + } + ], + [ + "BMJfjOzunWs8XkQgvvx1e", + { + "type": "instance", + "id": "BMJfjOzunWs8XkQgvvx1e", + "component": "Text", + "children": [ + { + "type": "text", + "value": "Please don't change directly in the fixture" + } + ] + } + ], + [ + "pjkZo5EiBqaeUXBcyHf_O", + { + "type": "instance", + "id": "pjkZo5EiBqaeUXBcyHf_O", + "component": "Link", + "children": [ + { + "type": "text", + "value": "Test another page link" + } + ] + } + ], + [ + "n_VBMr7klpx25buS0NV7R", + { + "type": "instance", + "id": "n_VBMr7klpx25buS0NV7R", + "component": "Body", + "children": [ + { + "type": "id", + "value": "wthNByqb3RPmheb-56VYI" + } + ] + } + ], + [ + "wthNByqb3RPmheb-56VYI", + { + "type": "instance", + "id": "wthNByqb3RPmheb-56VYI", + "component": "Heading", + "children": [ + { + "type": "text", + "value": "Another page" + } + ] + } + ], + [ + "uHB3Fjb7-NELG-bnH7bXB", + { + "type": "instance", + "id": "uHB3Fjb7-NELG-bnH7bXB", + "component": "Image", + "children": [] + } + ], + [ + "2sIE8GxbKRBaav_zdhaZ1", + { + "type": "instance", + "id": "2sIE8GxbKRBaav_zdhaZ1", + "component": "Image", + "children": [] + } + ] + ], + "deployment": { + "destination": "saas", + "domains": ["cli-basic-test-d0osr"], + "assetsDomain": "cli-basic-test-d0osr", + "excludeWstdDomainFromSearch": false + } + }, + "page": { + "id": "9di_L14CzctvSruIoKVvE", + "name": "Home", + "title": "\"Home\"", + "rootInstanceId": "MMimeobf_zi4ZkRGXapju", + "systemDataSourceId": "2KT4-bRzToj9cAGAN_woK", + "meta": {}, + "path": "" + }, + "pages": [ + { + "id": "9di_L14CzctvSruIoKVvE", + "name": "Home", + "title": "\"Home\"", + "rootInstanceId": "MMimeobf_zi4ZkRGXapju", + "systemDataSourceId": "2KT4-bRzToj9cAGAN_woK", + "meta": {}, + "path": "" + }, + { + "id": "WPPAbLFyJD_02vhjRd8P4", + "name": "Another page", + "title": "\"Another page\"", + "history": ["/another-page"], + "rootInstanceId": "n_VBMr7klpx25buS0NV7R", + "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", + "meta": { + "description": "\"\"", + "excludePageFromSearch": "true", + "language": "\"\"", + "socialImageUrl": "\"\"", + "status": "200", + "redirect": "\"\"", + "documentType": "html", + "custom": [ + { + "property": "", + "content": "\"\"" + } + ] + }, + "marketplace": { + "include": false + }, + "path": "/another-page" + } + ], + "assets": [ + { + "id": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee", + "name": "iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg", + "description": null, + "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", + "size": 999, + "type": "image", + "format": "svg", + "createdAt": "2024-07-26T13:39:48.678+00:00", + "meta": { + "width": 14, + "height": 16 + } + }, + { + "id": "d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd", + "name": "147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg", + "description": null, + "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", + "size": 64701, + "type": "image", + "format": "jpg", + "createdAt": "2024-12-06T14:36:07.046+00:00", + "meta": { + "width": 820, + "height": 985 + } + } + ], + "user": { + "email": "hello@webstudio.is" + }, + "projectDomain": "cli-basic-test-d0osr", + "projectTitle": "cli-basic-test", + "origin": "https://main.development.webstudio.is" +} diff --git a/fixtures/react-router-docker/Dockerfile b/fixtures/react-router-docker/Dockerfile new file mode 100644 index 000000000000..1385cf9a5f14 --- /dev/null +++ b/fixtures/react-router-docker/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm install + +FROM node:22-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm install --omit=dev + +FROM node:22-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:22-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] diff --git a/fixtures/react-router-docker/README.md b/fixtures/react-router-docker/README.md new file mode 100644 index 000000000000..c6f8cfa5b960 --- /dev/null +++ b/fixtures/react-router-docker/README.md @@ -0,0 +1,20 @@ +# Fixture to test/play with webstudio + +## How to develop + +```bash +# Terminal 1 +cd packages/webstudio +pnpm dev +``` + +```bash +# Terminal 2 +pnpm fixtures:link + +pnpm fixtures:sync +# data.json generated + +pnpm fixtures:build +# exec `pnpm run dev` to see result +``` diff --git a/fixtures/react-router-docker/app/__generated__/$resources.sitemap.xml.ts b/fixtures/react-router-docker/app/__generated__/$resources.sitemap.xml.ts new file mode 100644 index 000000000000..181eac4621f3 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/$resources.sitemap.xml.ts @@ -0,0 +1,6 @@ +export const sitemap = [ + { + path: "/", + lastModified: "2025-01-04", + }, +]; diff --git a/fixtures/react-router-docker/app/__generated__/[another-page]._index.server.tsx b/fixtures/react-router-docker/app/__generated__/[another-page]._index.server.tsx new file mode 100644 index 000000000000..dc6510942c47 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/[another-page]._index.server.tsx @@ -0,0 +1,39 @@ +/* eslint-disable */ +/* This is a auto generated file for building the project */ + +import type { PageMeta } from "@webstudio-is/sdk"; +import type { System, ResourceRequest } from "@webstudio-is/sdk"; +export const getResources = (_props: { system: System }) => { + const _data = new Map([]); + const _action = new Map([]); + return { data: _data, action: _action }; +}; + +export const getPageMeta = ({ + system, + resources, +}: { + system: System; + resources: Record; +}): PageMeta => { + return { + title: "Another page", + description: "", + excludePageFromSearch: true, + language: "", + socialImageAssetName: undefined, + socialImageUrl: "", + status: 200, + redirect: "", + custom: [], + }; +}; + +type Params = Record; +export const getRemixParams = ({ ...params }: Params): Params => { + return params; +}; + +export const projectId = "d845c167-ea07-4875-b08d-83e97c09dcce"; + +export const contactEmail = "hello@webstudio.is"; diff --git a/fixtures/react-router-docker/app/__generated__/[another-page]._index.tsx b/fixtures/react-router-docker/app/__generated__/[another-page]._index.tsx new file mode 100644 index 000000000000..0f11ceedef83 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/[another-page]._index.tsx @@ -0,0 +1,37 @@ +/* eslint-disable */ +/* This is a auto generated file for building the project */ + +import { Fragment, useState } from "react"; +import type { FontAsset, ImageAsset } from "@webstudio-is/sdk"; +import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime"; +import { Body as Body } from "@webstudio-is/sdk-components-react-router"; +import { Heading as Heading } from "@webstudio-is/sdk-components-react"; + +export const siteName = ""; + +export const favIconAsset: ImageAsset | undefined = { + id: "d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd", + name: "147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg", + description: null, + projectId: "d845c167-ea07-4875-b08d-83e97c09dcce", + size: 64701, + type: "image", + format: "jpg", + createdAt: "2024-12-06T14:36:07.046+00:00", + meta: { width: 820, height: 985 }, +}; + +// Font assets on current page (can be preloaded) +export const pageFontAssets: FontAsset[] = []; + +export const pageBackgroundImageAssets: ImageAsset[] = []; + +const Page = ({}: { system: any }) => { + return ( + + {"Another page"} + + ); +}; + +export { Page }; diff --git a/fixtures/react-router-docker/app/__generated__/_index.server.tsx b/fixtures/react-router-docker/app/__generated__/_index.server.tsx new file mode 100644 index 000000000000..83d760a977b9 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/_index.server.tsx @@ -0,0 +1,39 @@ +/* eslint-disable */ +/* This is a auto generated file for building the project */ + +import type { PageMeta } from "@webstudio-is/sdk"; +import type { System, ResourceRequest } from "@webstudio-is/sdk"; +export const getResources = (_props: { system: System }) => { + const _data = new Map([]); + const _action = new Map([]); + return { data: _data, action: _action }; +}; + +export const getPageMeta = ({ + system, + resources, +}: { + system: System; + resources: Record; +}): PageMeta => { + return { + title: "Home", + description: undefined, + excludePageFromSearch: undefined, + language: undefined, + socialImageAssetName: undefined, + socialImageUrl: undefined, + status: undefined, + redirect: undefined, + custom: [], + }; +}; + +type Params = Record; +export const getRemixParams = ({ ...params }: Params): Params => { + return params; +}; + +export const projectId = "d845c167-ea07-4875-b08d-83e97c09dcce"; + +export const contactEmail = "hello@webstudio.is"; diff --git a/fixtures/react-router-docker/app/__generated__/_index.tsx b/fixtures/react-router-docker/app/__generated__/_index.tsx new file mode 100644 index 000000000000..9c073984cf91 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/_index.tsx @@ -0,0 +1,64 @@ +/* eslint-disable */ +/* This is a auto generated file for building the project */ + +import { Fragment, useState } from "react"; +import type { FontAsset, ImageAsset } from "@webstudio-is/sdk"; +import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime"; +import { + Body as Body, + Link as Link, +} from "@webstudio-is/sdk-components-react-router"; +import { + Heading as Heading, + Text as Text, + Image as Image, +} from "@webstudio-is/sdk-components-react"; + +export const siteName = ""; + +export const favIconAsset: ImageAsset | undefined = { + id: "d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd", + name: "147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg", + description: null, + projectId: "d845c167-ea07-4875-b08d-83e97c09dcce", + size: 64701, + type: "image", + format: "jpg", + createdAt: "2024-12-06T14:36:07.046+00:00", + meta: { width: 820, height: 985 }, +}; + +// Font assets on current page (can be preloaded) +export const pageFontAssets: FontAsset[] = []; + +export const pageBackgroundImageAssets: ImageAsset[] = []; + +export const CustomCode = () => { + return <>; +}; + +const Page = ({}: { system: any }) => { + return ( + + {"Simple Project to test CLI"} + + {"Please don't change directly in the fixture"} + + + {"Test another page link"} + + + + + ); +}; + +export { Page }; diff --git a/fixtures/react-router-docker/app/__generated__/index.css b/fixtures/react-router-docker/app/__generated__/index.css new file mode 100644 index 000000000000..dd85fdf13b99 --- /dev/null +++ b/fixtures/react-router-docker/app/__generated__/index.css @@ -0,0 +1,118 @@ +@media all { + :root { + display: grid; + min-height: 100%; + font-family: Arial, Roboto, sans-serif; + font-size: 16px; + line-height: 1.2; + white-space: pre-wrap; + white-space-collapse: preserve; + } + :where(body.w-body) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin: 0; + } + :where(h1.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(h2.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(h3.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(h4.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(h5.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(h6.w-heading) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + } + :where(div.w-text) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + min-height: 1em; + } + :where(a.w-link) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + display: inline-block; + } + :where(img.w-image) { + box-sizing: border-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + outline-width: 1px; + max-width: 100%; + display: block; + height: auto; + } +} +@media all { + .c1jaw2zx { + display: flex; + } + .cbipm55 { + align-items: center; + } + .ctniqj4 { + justify-content: center; + } + .ctgx88l { + flex-direction: column; + } + .cn3rfux { + margin-bottom: 1em; + } + .c161qeci { + width: 400px; + } +} diff --git a/fixtures/react-router-docker/app/constants.mjs b/fixtures/react-router-docker/app/constants.mjs new file mode 100644 index 000000000000..dc5dbb0f5cb7 --- /dev/null +++ b/fixtures/react-router-docker/app/constants.mjs @@ -0,0 +1,13 @@ +/** + * We use mjs extension as constants in this file is shared with the build script + * and we use `node --eval` to extract the constants. + */ +export const assetBaseUrl = "/assets/"; +export const imageBaseUrl = "/assets/"; + +/** + * @type {import("@webstudio-is/image").ImageLoader} + */ +export const imageLoader = ({ src }) => { + return src; +}; diff --git a/fixtures/react-router-docker/app/extension.ts b/fixtures/react-router-docker/app/extension.ts new file mode 100644 index 000000000000..bffd05d48e17 --- /dev/null +++ b/fixtures/react-router-docker/app/extension.ts @@ -0,0 +1,13 @@ +import { ResourceRequest } from "@webstudio-is/sdk"; + +declare module "react-router" { + interface AppLoadContext { + EXCLUDE_FROM_SEARCH: boolean; + getDefaultActionResource?: (options: { + url: URL; + projectId: string; + contactEmail: string; + formData: FormData; + }) => ResourceRequest; + } +} diff --git a/fixtures/react-router-docker/app/root.tsx b/fixtures/react-router-docker/app/root.tsx new file mode 100644 index 000000000000..aa2a8c416496 --- /dev/null +++ b/fixtures/react-router-docker/app/root.tsx @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import { Links, Meta, Outlet, useMatches } from "react-router"; +// @todo think about how to make __generated__ typeable +// @ts-ignore +import { CustomCode } from "./__generated__/_index"; + +const Root = () => { + // Get language from matches + const matches = useMatches(); + + const lastMatchWithLanguage = matches.findLast((match) => { + // @ts-ignore + const language = match?.data?.pageMeta?.language; + return language != null; + }); + + // @ts-ignore + const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? "en"; + + return ( + + + + + + + + + + + ); +}; + +export default Root; diff --git a/fixtures/react-router-docker/app/routes.ts b/fixtures/react-router-docker/app/routes.ts new file mode 100644 index 000000000000..4c05936cb638 --- /dev/null +++ b/fixtures/react-router-docker/app/routes.ts @@ -0,0 +1,4 @@ +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; diff --git a/fixtures/react-router-docker/app/routes/[another-page]._index.tsx b/fixtures/react-router-docker/app/routes/[another-page]._index.tsx new file mode 100644 index 000000000000..e9d062f92306 --- /dev/null +++ b/fixtures/react-router-docker/app/routes/[another-page]._index.tsx @@ -0,0 +1,295 @@ +import { + type MetaFunction, + type LinksFunction, + type LinkDescriptor, + type ActionFunctionArgs, + type LoaderFunctionArgs, + type HeadersFunction, + data, + redirect, + useLoaderData, +} from "react-router"; +import { + isLocalResource, + loadResource, + loadResources, + formIdFieldName, + formBotFieldName, +} from "@webstudio-is/sdk/runtime"; +import { + ReactSdkContext, + PageSettingsMeta, + PageSettingsTitle, +} from "@webstudio-is/react-sdk/runtime"; +import { + Page, + siteName, + favIconAsset, + pageFontAssets, + pageBackgroundImageAssets, +} from "../__generated__/[another-page]._index"; +import { + getResources, + getPageMeta, + getRemixParams, + projectId, + contactEmail, +} from "../__generated__/[another-page]._index.server"; +import { assetBaseUrl, imageLoader } from "../constants.mjs"; +import css from "../__generated__/index.css?url"; +import { sitemap } from "../__generated__/$resources.sitemap.xml"; + +const customFetch: typeof fetch = (input, init) => { + if (typeof input !== "string") { + return fetch(input, init); + } + + if (isLocalResource(input, "sitemap.xml")) { + // @todo: dynamic import sitemap ??? + const response = new Response(JSON.stringify(sitemap)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); + } + + return fetch(input, init); +}; + +export const loader = async (arg: LoaderFunctionArgs) => { + const url = new URL(arg.request.url); + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + url.host = host; + url.protocol = "https"; + + const params = getRemixParams(arg.params); + const system = { + params, + search: Object.fromEntries(url.searchParams), + origin: url.origin, + }; + + const resources = await loadResources( + customFetch, + getResources({ system }).data + ); + const pageMeta = getPageMeta({ system, resources }); + + if (pageMeta.redirect) { + const status = + pageMeta.status === 301 || pageMeta.status === 302 + ? pageMeta.status + : 302; + throw redirect(pageMeta.redirect, status); + } + + // typecheck + arg.context.EXCLUDE_FROM_SEARCH satisfies boolean; + + if (arg.context.EXCLUDE_FROM_SEARCH) { + pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH; + } + + return data( + { + host, + url: url.href, + system, + resources, + pageMeta, + }, + // No way for current information to change, so add cache for 10 minutes + // In case of CRM Data, this should be set to 0 + { + status: pageMeta.status, + headers: { + "Cache-Control": "public, max-age=600", + }, + } + ); +}; + +export const headers: HeadersFunction = () => { + return { + "Cache-Control": "public, max-age=0, must-revalidate", + }; +}; + +export const meta: MetaFunction = ({ data }) => { + const metas: ReturnType = []; + if (data === undefined) { + return metas; + } + + const origin = `https://${data.host}`; + + if (siteName) { + metas.push({ + "script:ld+json": { + "@context": "https://schema.org", + "@type": "WebSite", + name: siteName, + url: origin, + }, + }); + } + + return metas; +}; + +export const links: LinksFunction = () => { + const result: LinkDescriptor[] = []; + + result.push({ + rel: "stylesheet", + href: css, + }); + + if (favIconAsset) { + result.push({ + rel: "icon", + href: imageLoader({ + src: `${assetBaseUrl}${favIconAsset.name}`, + // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search + width: 144, + height: 144, + fit: "pad", + quality: 100, + format: "auto", + }), + type: undefined, + }); + } + + for (const asset of pageFontAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${asset.name}`, + as: "font", + crossOrigin: "anonymous", + }); + } + + for (const backgroundImageAsset of pageBackgroundImageAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${backgroundImageAsset.name}`, + as: "image", + }); + } + + return result; +}; + +const getRequestHost = (request: Request): string => + request.headers.get("x-forwarded-host") || request.headers.get("host") || ""; + +export const action = async ({ + request, + context, +}: ActionFunctionArgs): Promise< + { success: true } | { success: false; errors: string[] } +> => { + try { + const url = new URL(request.url); + url.host = getRequestHost(request); + + const formData = await request.formData(); + + const system = { + params: {}, + search: {}, + origin: url.origin, + }; + + const resourceName = formData.get(formIdFieldName); + let resource = + typeof resourceName === "string" + ? getResources({ system }).action.get(resourceName) + : undefined; + + const formBotValue = formData.get(formBotFieldName); + + if (formBotValue == null || typeof formBotValue !== "string") { + throw new Error("Form bot field not found"); + } + + const submitTime = parseInt(formBotValue, 16); + // Assumes that the difference between the server time and the form submission time, + // including any client-server time drift, is within a 5-minute range. + // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes. + // Example: `formBotValue: jsdom`, or `formBotValue: headless-env` + if ( + Number.isNaN(submitTime) || + Math.abs(Date.now() - submitTime) > 1000 * 60 * 5 + ) { + throw new Error(`Form bot value invalid ${formBotValue}`); + } + + formData.delete(formIdFieldName); + formData.delete(formBotFieldName); + + if (resource) { + resource.headers.push({ + name: "Content-Type", + value: "application/json", + }); + resource.body = Object.fromEntries(formData); + } else { + if (contactEmail === undefined) { + throw new Error("Contact email not found"); + } + + resource = context.getDefaultActionResource?.({ + url, + projectId, + contactEmail, + formData, + }); + } + + if (resource === undefined) { + throw Error("Resource not found"); + } + const { ok, statusText } = await loadResource(fetch, resource); + if (ok) { + return { success: true }; + } + return { success: false, errors: [statusText] }; + } catch (error) { + console.error(error); + + return { + success: false, + errors: [error instanceof Error ? error.message : "Unknown error"], + }; + } +}; + +const Outlet = () => { + const { system, resources, url, pageMeta, host } = + useLoaderData(); + return ( + + {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */} + + + {pageMeta.title} + + ); +}; + +export default Outlet; diff --git a/fixtures/react-router-docker/app/routes/[robots.txt].tsx b/fixtures/react-router-docker/app/routes/[robots.txt].tsx new file mode 100644 index 000000000000..cb8fe07b8f6f --- /dev/null +++ b/fixtures/react-router-docker/app/routes/[robots.txt].tsx @@ -0,0 +1,24 @@ +import type { LoaderFunctionArgs } from "react-router"; + +export const loader = (arg: LoaderFunctionArgs) => { + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + + return new Response( + ` +User-agent: * +Disallow: /api/ + +Sitemap: https://${host}/sitemap.xml + + `, + { + headers: { + "Content-Type": "text/plain", + }, + status: 200, + } + ); +}; diff --git a/fixtures/react-router-docker/app/routes/[sitemap.xml]._index.tsx b/fixtures/react-router-docker/app/routes/[sitemap.xml]._index.tsx new file mode 100644 index 000000000000..8362eebee742 --- /dev/null +++ b/fixtures/react-router-docker/app/routes/[sitemap.xml]._index.tsx @@ -0,0 +1,34 @@ +import type { LoaderFunctionArgs } from "react-router"; +import { sitemap } from "../__generated__/$resources.sitemap.xml"; + +export const loader = (arg: LoaderFunctionArgs) => { + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + + const urls = sitemap.map((page) => { + const url = new URL(`https://${host}${page.path}`); + + return ` + + ${url.href} + ${page.lastModified.split("T")[0]} + + `; + }); + + return new Response( + ` + +${urls.join("")} + + `, + { + headers: { + "Content-Type": "application/xml", + }, + status: 200, + } + ); +}; diff --git a/fixtures/react-router-docker/app/routes/_index.tsx b/fixtures/react-router-docker/app/routes/_index.tsx new file mode 100644 index 000000000000..5ecd622ce95e --- /dev/null +++ b/fixtures/react-router-docker/app/routes/_index.tsx @@ -0,0 +1,295 @@ +import { + type MetaFunction, + type LinksFunction, + type LinkDescriptor, + type ActionFunctionArgs, + type LoaderFunctionArgs, + type HeadersFunction, + data, + redirect, + useLoaderData, +} from "react-router"; +import { + isLocalResource, + loadResource, + loadResources, + formIdFieldName, + formBotFieldName, +} from "@webstudio-is/sdk/runtime"; +import { + ReactSdkContext, + PageSettingsMeta, + PageSettingsTitle, +} from "@webstudio-is/react-sdk/runtime"; +import { + Page, + siteName, + favIconAsset, + pageFontAssets, + pageBackgroundImageAssets, +} from "../__generated__/_index"; +import { + getResources, + getPageMeta, + getRemixParams, + projectId, + contactEmail, +} from "../__generated__/_index.server"; +import { assetBaseUrl, imageLoader } from "../constants.mjs"; +import css from "../__generated__/index.css?url"; +import { sitemap } from "../__generated__/$resources.sitemap.xml"; + +const customFetch: typeof fetch = (input, init) => { + if (typeof input !== "string") { + return fetch(input, init); + } + + if (isLocalResource(input, "sitemap.xml")) { + // @todo: dynamic import sitemap ??? + const response = new Response(JSON.stringify(sitemap)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); + } + + return fetch(input, init); +}; + +export const loader = async (arg: LoaderFunctionArgs) => { + const url = new URL(arg.request.url); + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + url.host = host; + url.protocol = "https"; + + const params = getRemixParams(arg.params); + const system = { + params, + search: Object.fromEntries(url.searchParams), + origin: url.origin, + }; + + const resources = await loadResources( + customFetch, + getResources({ system }).data + ); + const pageMeta = getPageMeta({ system, resources }); + + if (pageMeta.redirect) { + const status = + pageMeta.status === 301 || pageMeta.status === 302 + ? pageMeta.status + : 302; + throw redirect(pageMeta.redirect, status); + } + + // typecheck + arg.context.EXCLUDE_FROM_SEARCH satisfies boolean; + + if (arg.context.EXCLUDE_FROM_SEARCH) { + pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH; + } + + return data( + { + host, + url: url.href, + system, + resources, + pageMeta, + }, + // No way for current information to change, so add cache for 10 minutes + // In case of CRM Data, this should be set to 0 + { + status: pageMeta.status, + headers: { + "Cache-Control": "public, max-age=600", + }, + } + ); +}; + +export const headers: HeadersFunction = () => { + return { + "Cache-Control": "public, max-age=0, must-revalidate", + }; +}; + +export const meta: MetaFunction = ({ data }) => { + const metas: ReturnType = []; + if (data === undefined) { + return metas; + } + + const origin = `https://${data.host}`; + + if (siteName) { + metas.push({ + "script:ld+json": { + "@context": "https://schema.org", + "@type": "WebSite", + name: siteName, + url: origin, + }, + }); + } + + return metas; +}; + +export const links: LinksFunction = () => { + const result: LinkDescriptor[] = []; + + result.push({ + rel: "stylesheet", + href: css, + }); + + if (favIconAsset) { + result.push({ + rel: "icon", + href: imageLoader({ + src: `${assetBaseUrl}${favIconAsset.name}`, + // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search + width: 144, + height: 144, + fit: "pad", + quality: 100, + format: "auto", + }), + type: undefined, + }); + } + + for (const asset of pageFontAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${asset.name}`, + as: "font", + crossOrigin: "anonymous", + }); + } + + for (const backgroundImageAsset of pageBackgroundImageAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${backgroundImageAsset.name}`, + as: "image", + }); + } + + return result; +}; + +const getRequestHost = (request: Request): string => + request.headers.get("x-forwarded-host") || request.headers.get("host") || ""; + +export const action = async ({ + request, + context, +}: ActionFunctionArgs): Promise< + { success: true } | { success: false; errors: string[] } +> => { + try { + const url = new URL(request.url); + url.host = getRequestHost(request); + + const formData = await request.formData(); + + const system = { + params: {}, + search: {}, + origin: url.origin, + }; + + const resourceName = formData.get(formIdFieldName); + let resource = + typeof resourceName === "string" + ? getResources({ system }).action.get(resourceName) + : undefined; + + const formBotValue = formData.get(formBotFieldName); + + if (formBotValue == null || typeof formBotValue !== "string") { + throw new Error("Form bot field not found"); + } + + const submitTime = parseInt(formBotValue, 16); + // Assumes that the difference between the server time and the form submission time, + // including any client-server time drift, is within a 5-minute range. + // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes. + // Example: `formBotValue: jsdom`, or `formBotValue: headless-env` + if ( + Number.isNaN(submitTime) || + Math.abs(Date.now() - submitTime) > 1000 * 60 * 5 + ) { + throw new Error(`Form bot value invalid ${formBotValue}`); + } + + formData.delete(formIdFieldName); + formData.delete(formBotFieldName); + + if (resource) { + resource.headers.push({ + name: "Content-Type", + value: "application/json", + }); + resource.body = Object.fromEntries(formData); + } else { + if (contactEmail === undefined) { + throw new Error("Contact email not found"); + } + + resource = context.getDefaultActionResource?.({ + url, + projectId, + contactEmail, + formData, + }); + } + + if (resource === undefined) { + throw Error("Resource not found"); + } + const { ok, statusText } = await loadResource(fetch, resource); + if (ok) { + return { success: true }; + } + return { success: false, errors: [statusText] }; + } catch (error) { + console.error(error); + + return { + success: false, + errors: [error instanceof Error ? error.message : "Unknown error"], + }; + } +}; + +const Outlet = () => { + const { system, resources, url, pageMeta, host } = + useLoaderData(); + return ( + + {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */} + + + {pageMeta.title} + + ); +}; + +export default Outlet; diff --git a/fixtures/react-router-docker/package.json b/fixtures/react-router-docker/package.json new file mode 100644 index 000000000000..63d2828a6e2f --- /dev/null +++ b/fixtures/react-router-docker/package.json @@ -0,0 +1,49 @@ +{ + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "tsc", + "cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio", + "fixtures:link": "pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'", + "fixtures:sync": "pnpm cli sync --buildId f565d527-32e7-4731-bc71-aca9e9574587 && pnpm prettier --write ./.webstudio/", + "fixtures:build": "pnpm cli build --template react-router-docker --template .template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json" + }, + "dependencies": { + "@react-router/dev": "^7.1.1", + "@react-router/fs-routes": "^7.1.1", + "@react-router/node": "^7.1.1", + "@react-router/serve": "^7.1.1", + "@webstudio-is/image": "workspace:*", + "@webstudio-is/react-sdk": "workspace:*", + "@webstudio-is/sdk": "workspace:*", + "@webstudio-is/sdk-components-animation": "workspace:*", + "@webstudio-is/sdk-components-react": "workspace:*", + "@webstudio-is/sdk-components-react-radix": "workspace:*", + "@webstudio-is/sdk-components-react-router": "workspace:*", + "isbot": "^5.1.19", + "react": "18.3.0-canary-14898b6a9-20240318", + "react-dom": "18.3.0-canary-14898b6a9-20240318", + "react-router": "^7.1.1", + "vite": "^5.4.11", + "webstudio": "workspace:*" + }, + "private": true, + "sideEffects": false, + "devDependencies": { + "@types/react": "^18.2.70", + "@types/react-dom": "^18.2.25", + "typescript": "5.7.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "name": "webstudio-react-router-docker", + "version": "0.0.1", + "description": "", + "main": "index.js", + "keywords": [], + "author": "", + "license": "AGPL-3.0-or-later", + "type": "module" +} diff --git a/fixtures/react-router-docker/public/assets/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg b/fixtures/react-router-docker/public/assets/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9d5e2b88439ace5f8de36737ff779f8b70599d56 GIT binary patch literal 64701 zcmeFZc|4ST-#0!&LK*S>!LkbmtHz`x#o4H4MKKEtlT&UOT{M}Uo8fNi%I z0t45{`Ipaz{M#4X9(E4SyYkBI0YF>(2$3dfG0(9qPhrV}eEgwg9h-^RTkyKPYOa1C(k%@r z`u5>9)bW=rd6+45kS+&^AvKr#5VJYNv?y|`xwJbTctHB~?TCQ8S%c$8R~Kj53JxJu z)-GhG2wMNFZn+NpJIP(hE`%cXa^r z?P`Phsgk>zzp`jNMV0b&j`B zX@l%PTTV}+@xC!nZ^oy<1)5C)@8xKB%07OsKj>cg^Yb>Y-zuqX#;qEC1&FK6`S9Zz zFx+cb*7*3FwoY>Hhc6%QeOj(4f^NAmBI4J=y>}rGPVPcz!MIrw*pD4>&3`-%VIBB^ z%J0qLoW=4ZgT&Yb#X_8Z?SBxvFJiplb?Uy`pYHfPcI>QT|Mi>Cz35Yrv#2^hw>wY{ zKXbz_Nr6Hy_MOi$Khsyjd@wouxShg6m74r-^Jg)cCfH0n06qgO^|yuD*3+;F>$&y~ z&9xnsPFZe~4-!}Q`3DSz<{>CzjI&?!52Vlr$lo$Q69;zkX%c;Or+qS!xLZ?4<|6m1 zC0}Zri%^3| zK`h|H-LC@r=Y0n_Zg#A7!>Nu4;KV@WMN{LQcD&bG+ObF+H3NNiqTQWaN;^%Y;I_ra zm%YR25iZ7=QN$-VA^|uP#Nt&YaUT}MOifFlXu1z+F;E2Ds83kh?5ul>4uGoFr289E z(kf^C*Y3`Kswz|J7pMwm)ZrNt%`^`036-AhAEYOQSjBZ784Gck#890&B>G!g)YSI` z5@s&3b@4#DKux%usK^8iKzr(1#S7OM4EfWn=!#U^dc5jvv(;Xv2evi29Vz=c4Iw zj@g?%o+*2XoZYtz@%A!+Ie!pvPa}uMYVJa~jQkbd z)ZT_ZbJ!_}JQ$_qH~LucP~y%Rz=jdR>feP(P{=RApD!{+XiqLZ8!;_I?LvA8-j8%S ztxk-6p)anUd;gl#EMd6}5`i$bYkAe&o_L0$;`^;ASH$dTYu*`hww9P(s#$KT!OoM+ z6Tq3`VKO3-Ab=L5{y5i~=v_L&M39wL4O@=y3Cs9p`ONwCc$>m|w+HT-hq@n+aFvWA zhD<48;O1IkyVUWwrY3?BahRht7)5PKQC;KsQ|9sdly)5zo4{kZzS;I{9m9lME{(hn zDhgLQGmak++J&4#G$WYOe2ftERq^a3h9>hg;LAAeC90?-vQ3#kg*K;sGS7JKGUtlq zw+DQ>L#+xe!kD_%rV=O3mVNU-vhOH|=-XM0-mVRia;{HmD$^dN|&S7 zbt9ANlEh!Wioa!PfK1ynx27YVixuOPZ=GsR(JaooXmjWjCz6SSd)3Vi(BJLUs4jQz zl)$|a$uJSwLQ-vxF%6!gnSfUGR96d}^^8_lQAVqlsVx1L=a=qte~IU$;-?04xeRj! zxy%f!3WW^N)9_=x2!I}gX!1awmCI1Y3@+*d4^AWq4)no=n-_Gu^86n~SDLZJMlN+k z1aQP1C}&{aUB+QF-C~$(JH=SyI_Ltl`_;odiF^nW{xzS-x*6UL2qRtVm>`*u`0(+1 zmH}0(^=458)DO9CBM>!a-{@JGFY|Zzp3Yi?HOV!a)3EefU~WmH;L%%T*iwzp*qbWP zErk1c?QNnOm#n-t*u(xm+QZdTKOtj_nY3VR@6#_ zFtEgBrceoe6JQVH-01e7?ODP`FD^VRJqi49o>1mdm9}tP&9SRL#PiuvCNakR5M+A2 z?zKOlE6d`^q2^XA>xI>z)q%AqGy9gbFuK%`j@YsQaonOrL3oe#ievTnH>ZEd7qk?f zr6AtB2(htZ5slMK={TyJy)XRIPC4AeN*p+4^trZ#4E|fW@pe0RUVgc7EH4o6)#{c0 zsb!ib_x|GbeSQ5&t!8!?asYN8*P=kV!cg%UMIsu9DV#Ka z66frt9KkN4zvSBX(M-(Ou1KF*sphYvA@RJ&RDCP$UFKf0qb(Czak~%?ETD^H@u*Xg z3|%weX9@H4PImuGi4ba`7`vVuYrz5d95pmOmCTOy9 z``YQw!>G~)6oQnMHCAQfr$kcQh4`aVPao|~Px885U3b&@Xo(1Mf^8>be!^l;x+NPOdwfHF#UeSsmJi zXc6YzSiFXyFBRPBD4$d^(8i3o9;*ki73Qi;M?{>RwlpW;m))SI=ow!8wXqK#UmPDG z0X$(i=AJoz#66K&FZW!jH{;Up-f-1iFC)Sdfe*xzw9c( z7`jYLBi`-sC1ay@AqU$7sB7ZG4CtY)7oXC zKIYYqJ|~M5wm7DLwlajlJZlt3y|@dRt!PR+fId%)tzn#CnoMw2&HqB2#V2m)DX8k3t(419d(3hDzfW@sAGsYoxb5t`czi$vTQ{P+$t>)8}_$nqO%8 z&*ac!Xj?rLeLv%&ji|lPwwHS-E(v?CR00yt;&HLH?s;mTHOVWZJ_3h_I_6JthOqkZ z*M|W;|8tWEQi!-Y2I%u;w^Hcg7HP={u9CtH1cHw=VV^992Oa4;a+773X!XJqlB;_w4Q_aMV)sNEuIua(* zaH+T^?JoFC$?JP{y~C-y5J9Fqb)d^pzr9uT9wG5(xaBEPfx|(3k8R8v3C{J0QfJ4G zdjy{4O+^9)pdn{sy9iU+{$yw@oO;P>Pt8uZ=2YG;1jah*{mFan{!K<`B$NL`rc3?& zkS~^eT$yNo(;VKgQS%9MAL|8c%At5Ov@vfC*xP|D#s#JaRka(s&$0BW?uIDRmLA$x zJyQN?%0POAtd1;{otQE3+8pA&G@<&*`C;pIZdf0dDFkOh`FgiEdWHc7{~o2eV=wCw zsRWVRXA3>M;(eqAs6v75cd1oXmY!i`i`>sITQjusjmMX54VhOkqwr=Zg_GfcHk`m5 zV~t`((B3p;|MtTS^;MS2l#2h-^H}r!kv+!atw(c?jE&JdOVL3~6t4NtIr`=jx ztW-|&OO*)!;QMED<$wJBToq_JV1X7+)K1a>i)OSqkhTPiEmR&?NF)zCQ!%fwPh92d zUu3}5s=uVD$l9^BS4C$Z2|I?|UI&jf6g22XoHXWOZ32}+|3*E<45d2nogH9_rjP2LAX z5#tCFA9an4=!HicoE*i-pp|I&M2s|QsV66$@YPj`>i6;)5Q-S9CCijfITq#w_=fUg zc`FAS&MQkMzIk8HfP%t+RczM+OadbegEGNVJ@P)_!O}t5Sk7nu#SVTOvCm3@jG#z)pJmg&q+$EJvY79O~m^m`+$A9{;^Yrw8+rhJ)_yf-> z)`8V+3SH*#{ll!w{sQ1ZgsvF>Cl?|eOSRqrlws>gFew_OHm@?RX$7sxn3MDN7VuaD zZb0SRv^A};Jn%~AhuQXkcO!uRCC|ApuB*d)1%4ZlEWGIl*||9tQUmL0vn{S&+paBm z;+1Dv&|h4+y4_6aEO_eOB9gARW!E}A9cbk1jr8TwYaG_A5V}&L+wkltJ{=F7fH9@Q z8QQEN3KvipJ9$hG$3nEZIEruk1$23wZ+n33pN+d9Zg*u*s^V?y0Mwb73ciR z#%WFbv8%f*0M`DI1=ubW*4H06?pINXLiR2yz68t}@Q(2URU)=$$K;3AsA`RDPDFL* z=?OB*AAJM;$}Rm#=kZb_9d6~|kEMHUnHELR=lI*uxn+9oP8%~cd}(9Wj=T#=)C&*S zf8V&W&DXa%)hS42URrd)ZnM(T4Ds<^Jh5=?A zE(NB<{_)~=y&yig3n62ZFo#gXW-k)Sd^SDe%|nUv@sG7v<`UOVoyZ!}dRK zzrA{P9?7Tz_sj*`HNVJDi!PJ^e$S9#Bg*XuYv21ey|S8B=*mOak-}#ddc8xIb|Kue z)ve-cH+zPs^zTJ%o4`8_-PLfs(m;U}7lbz~VldaDOxzman$t#&BcvjHA^(| zBdiF3`db6SD-U)dZo3fRxK?f%>eP>`&44JwWG7E!f+YZqUu{X`yG3!6ju?1=HqxG< z=oxy;RUJ+ojcKWp#2|3)bw_F-82ad0nfrzu0>7EcM2$qvi~rN0_dgFXD<%R7G}vo6 zx1K~D(+DV7f*nBdzFrg=|E7uH2kQFnHD@OjuZTxf`zlRqx1}&goVpV$@5V-a*Amq9 z^sxB_o)G78a2H!JZwb_k{&Cu_M}cwjN6obOP`dS7A}*>^{AJ54+CbulmRi&f?|XLh zvB3w+t8EFfvl@(8(-Y;)UO2G=L3M=B5?DOxOUO)`Y;UMEJ0MoJsv1O0yI&}PZ)>Gu zvLr2A#;s|Jv8f1A;L&-Z&Wg&4mZtPFapTwm$#JD3*ysjRc#d_Tw_|cR(O?hgQ8n`- zv@PJs(dOYc<~! zx2r)dlVOP=1Tmne*RIh=hY-&+t#I*~Z7B=8^Z8TW7y5UHTY=`?wZ!yi+~SqT*wg=! z+z66+pIf0wG83>H1GNS2hM29Qr%xzr5FX$l1}B&o^L|A-4wKTjklJmxtKSQ;RY_bO z7y9&Fd|h(SJ@&IOjVp-p(Dk`p$lhX5w95us`MMkN_h9#TA)YCFmj}I7I%R?TF36Kr zkTJPJN$RF!YMEqZxle+?c5q<&w8@0t8S0?n zSi4xcV(r-I91!QIv$Iaq_2cM_^F2~~GB6MV|L~!yTEiD{tx&>6^}O zME=X=tT$Iv57m&EpEqL+cmpKW#{L>l7*%aj3z8c^HtT=95B71AeK^ zp$$N+MwRPzy6@cQY7A^Fc$2#|vgmp$9|W7U(NhN~*imS-+fHw()AUndP44L()cS5> zQ#BGM;IDC~_EefmL#MR3vq1ePbIo}#QfMtstITr8FTO&NVut zhWqe*XrYl3x8>}C-bM9sP4(oIx5CL8shCR#;L|&@X8i&JTu+W&`2{&X!N#(~><72h z0#2-zJ}w~T)PrN!WB=lIhZfow{jeuJ^9#0Hz6ez+)d79zzWmfwbF(~V7eX9@0mnG0 zX)M8wG+3imT*AO34PUDPS2^yrrg@Y_4}-SRsCSRX$`-oJ^^4$zqk7m;*n%1vSo$R3 zQo8K73t8WYSv)eOwY=f`1MN`eH#hffdA7sO1YC)N=0io$U2;*@p3bL{#jRd5@@1!s zw6XkkzvlYQ&iJ7uVH*u#Ep?CruHzkI@BWB7`T|WZ?v4VOQ0>?F82jNAJgoM8sPvr$ zt}RP{Cs5C_5ZtktNro5tEXG7HjSYoSfhQSwOuSTR**Acm-jWAh@BwWE)dSHjGW-$r zq45EMYINKl)LlQd59q4+GsEZhK|;7HmR0cJKjhNF^fAEN9G?4}j*Ar!Ca6-O3&;^> zPs%{?`d$=k-Kz*nMA-cyiMjet4C9`?W8ujVMC8H6)b{0dJpXO(rE$qEimNW!A2`)Y zPi*)BCmq~r#0VjQ;hdf4R6=j36k+AnI!kW4#j{hJj2}Ib2&P3KB5(-XRRE`2QzSm* zbYlg<$h8X*o$Kc|`%EiOY2q_MM~q4}5A-nRP7b`tv_5;l!0jze6m1Ny|KcIWK&f_O^BuHmGRn|Qk0+a#(J%U2xt-hj7~@vo~&o_Sn1gn$7oZ0dYK z^Q23Jm=W|%q+)O9an^f6BW^%3v>6zt!iZaa;bF4;O;`Z~3BagdcJSK1%9^?r(e->G zNQ{`nNgiPtem0iqgx&UcW&kL00G9aME=2O?tLzQB!x%<_hhNlU+Tq@4u)& zn>8$h9H>(@H|kK~*CsS9kr&!xx4`C1)ys8FUfx%sf<{5!p|@$n9HTl_a_MdLGC>Z( zV8;b)x^aL<6IEq!l({dnn*hcmVi5vFA%$52=WjRpDlS<@TZV&icj|)3A~)LDXP^I} zuPq7n^JZlsZNqEkSDvGTZ9YPJD-3y zxdQi0)l}m*&yi*o=j2ZH-sgRGEG;nsyLXMMr@C0Sjb0P+?u<={9I8Mth}Us0_f9g&-ZxU-F4-FLG7& z;H2>8#mv={O*(}YY-;`Vsf>4xphpX`RPJrC(rd8P8`EpFRBXx^R7!UElwm4gu#j-; zu6%BS9N0RUHqb$%fesL~FsH%#@F^@k8donIhI+akz60(EA9K1fC(k6n*IP36X2bLQ zFqPR4lSf)3<*%kdgqJq|UQJjZ#MI)bXaWEc=sy2_(xLsLO2^qAEHcl*7ngV)NOqeSW|9Qn;|yx612*t3&_ zC(h%5qtNw`y(v}L?(7DDL4#fVek*f;AY}^`tLQwtKht}Ww@{AilDsG2%dGN;lnTTQ z#n|P7iS;@g%LCS6#}xcQr|-P*zn(_Fy_3t512)cQ2l#8Jss!TzW0thT3)vx2P_ZuN z!DIK29R4BK&^qx=bFX2%ul(Js{;P^~Q_#}Y4U|z)40Xf~csO2!11O%i%-lK*j4Ap& zr%!wU4GUutcQ7ItO7y|9X`_(#&*zk1JW z8Z1Unb%{^M7uHuOIl{CobB|b~!DtB8M|XBE!=qNYSDq-_H_4mQDm9 zh7c66^xAuUv7Dazl5q$#PP=gTt#ZzNjLwR@@Tcn^l*zQ=7-b75Ly4QmjHhGjzJ#3c zY};Q=+4+9uENsb;;f(DP#uCf$&ux4{31V=MVb~%{{o+9j?cTcBZ8T1>6IS-n2|vE^ za~HC&whVmOX-X_xLI^o|<;WW<`<3#;mC6$ButF^@s8N$~MrF-o@4O76Hg9EI6BX48o)mGcm`fxMD-G!30Z&hCg75ZQ? zcUwSigWg^LvV~VyCRCdgQNhCkt_2^}f&2?o2(N3lF7a2{nZ94^C^__bQ2)-bGyy41 ze-TVCLQJDBDNhOE)z%6G(GJUikk;V`p=Q~(;_G}zqpB?=k9SVh3dPB_B^bM zSg(zb%k;vX3_Y7raKC32iMTYD2vVf6xecGt4v#>Vxt@G$H~usI;7aJ*BTy}M|KN2u zk6YK?6hZU;9%pG27DEv3ltmw;O~JI`i2xuJxMU(`Ilx(6XIr@Qnn_%hsXV{N(}!%mCLr@u1$XPwJUuz zv7zB{mEsz51VmY5yK%yHEg~-sU;>7toze z;tvwtai>4nW`D)F_#L}xEvNio=!15r+sUy;kTQS+mDA?q)#N-vH%p{7PfYL15{`dg zlH*rb$#LF<4Hfy{ie41ht?;(<@VGqJiv!qnY6LHYG^2HtOKpGOv-g zx#IShqN3CAqo5eJaAFOi$&@Lqy`;oC38Yjkpxj8Ru^XyK06S{X#g2TI0D;Fl!|a+S zFK{he%C1jzM9$NC@@aCtjN|ODyQ5EV?@s(35?*J;P8R+qb~cJWNt?{+*_der4JN5m z9%*N%Hn-sFE$;D}%1W-a3;FP}Zcu#Xo=dyg#Rkoxcld|ZZ&DH=zZ^TC?Bu|2;%9O6 zFeC7eo&bd%WTMJ2{!^R77_j+t+j@M{KN~K#}+M z-n?YN)1TU2ruOeEWL$pveRLy4arq^_GhcmSen?&YHz-+=#SOZ{y>}grq|=s$fn%Y4 zMj?TiVHAM_!;g3?_4xXF`Z}mTE5(VcB&wrC683=Otekbl$?!O(SMc*c}| zikgCpg5TPR1lyq=PaV6{2O~r`R#0s6F!?oifVuv(h#!V5BwS9B?NEG8}Saam1K9FOPRj#OLfjVx{{1`6Ay8}ce23%cD@Jd%iOmhe>| zgAV-$u8+VI((&Gw9h#&u7jz&kB+%%PM%qEAulFY6(@v~>;g^F`cJX(z1Ew_&fW7lukt7zPxD01oIr|5ZcGk!LIYl|5=Mbm@y27NU)TOTtVLhADw~Kfo{j44>dGX zx-IQM#X;0CNt0D9`<6rtI-$IrpvR^^XqKnaC>fTG)Zi^&joIUJUQY28WziBfR zSkrJ24)9HjGB^ZK7&^e#1sdUjo+VPU(Bm;prKZT|oF=$fFuwi}Uo)N#p0V!=zj3$n z-P)IPW3OrNb|Dw00dRl1Kro|{&;%xpOG~~vy;d>e&Hb;aYfkM=N#vZa9cy|BwJtT- z{Z8iTI92H!vZ7PqvcqAhA@*26VS_28P6rJ%GL6YS(s>#Up;>lp z2K8{ZCbvVedIG2((ndV93^~N!+vfOYtV=G?Pb+jZ?)gMbi}S4I5w%&iEz5s^KKGAM z;e`Lda8H0sNk?;mkDi!eosItCWXFz<{zeULM{G}8Tz1aox mRMneXO=C?}CjJ_Z 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 \ No newline at end of file diff --git a/fixtures/react-router-docker/public/favicon.ico b/fixtures/react-router-docker/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9cb8c815dc0abbc6b9038edea292275bf56e2537 GIT binary patch literal 15086 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 NPS;av>ny9R_rKT{U=08O literal 0 HcmV?d00001 diff --git a/fixtures/react-router-docker/tsconfig.json b/fixtures/react-router-docker/tsconfig.json new file mode 100644 index 000000000000..0744b14c56a2 --- /dev/null +++ b/fixtures/react-router-docker/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client", "@webstudio-is/react-sdk/placeholder"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "skipLibCheck": true, + "customConditions": ["webstudio"] + } +} diff --git a/fixtures/react-router-docker/vite.config.ts b/fixtures/react-router-docker/vite.config.ts new file mode 100644 index 000000000000..317d7dc0a1c6 --- /dev/null +++ b/fixtures/react-router-docker/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import { reactRouter } from "@react-router/dev/vite"; + +export default defineConfig({ + plugins: [reactRouter()], +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index eeff03360be3..284928bf29f9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,6 +52,7 @@ "devDependencies": { "@netlify/remix-adapter": "^2.5.1", "@netlify/remix-edge-adapter": "3.4.2", + "@react-router/dev": "^7.1.1", "@remix-run/cloudflare": "^2.15.2", "@remix-run/cloudflare-pages": "^2.15.2", "@remix-run/dev": "^2.15.2", @@ -70,9 +71,11 @@ "@webstudio-is/sdk-components-animation": "workspace:*", "@webstudio-is/sdk-components-react-radix": "workspace:*", "@webstudio-is/sdk-components-react-remix": "workspace:*", + "@webstudio-is/sdk-components-react-router": "workspace:*", "@webstudio-is/tsconfig": "workspace:*", "prettier": "3.4.2", "react-dom": "18.3.0-canary-14898b6a9-20240318", + "react-router": "^7.1.1", "ts-expect": "^1.3.0", "vike": "^0.4.210", "vite": "^5.4.11", diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index ba4487827960..a5077a908537 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -89,4 +89,9 @@ export const INTERNAL_TEMPLATES = [ label: "Cloudflare", expand: ["defaults", "cloudflare"], }, + { + value: "react-router-docker", + label: "Dokcer", + expand: ["react-router-docker"], + }, ]; diff --git a/packages/cli/src/framework-react-router.ts b/packages/cli/src/framework-react-router.ts new file mode 100644 index 000000000000..010608d4560e --- /dev/null +++ b/packages/cli/src/framework-react-router.ts @@ -0,0 +1,87 @@ +import { join } from "node:path"; +import { readFile, rm } from "node:fs/promises"; +import type { WsComponentMeta } from "@webstudio-is/sdk"; +import { generateRemixRoute, namespaceMeta } from "@webstudio-is/react-sdk"; +import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas"; +import * as reactRouterComponentMetas from "@webstudio-is/sdk-components-react-router/metas"; +import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas"; +import type { Framework } from "./framework"; + +export const createFramework = async (): Promise => { + const routeTemplatesDir = join("app", "route-templates"); + + const htmlTemplate = await readFile( + join(routeTemplatesDir, "html.tsx"), + "utf8" + ); + const xmlTemplate = await readFile( + join(routeTemplatesDir, "xml.tsx"), + "utf8" + ); + const defaultSitemapTemplate = await readFile( + join(routeTemplatesDir, "default-sitemap.tsx"), + "utf8" + ); + const redirectTemplate = await readFile( + join(routeTemplatesDir, "redirect.tsx"), + "utf8" + ); + + // cleanup route templates after reading to not bloat generated code + await rm(routeTemplatesDir, { recursive: true, force: true }); + + const radixComponentNamespacedMetas: Record = {}; + for (const [name, meta] of Object.entries(radixComponentMetas)) { + const namespace = "@webstudio-is/sdk-components-react-radix"; + radixComponentNamespacedMetas[`${namespace}:${name}`] = namespaceMeta( + meta, + namespace, + new Set(Object.keys(radixComponentMetas)) + ); + } + + return { + components: [ + { + source: "@webstudio-is/sdk-components-react", + metas: baseComponentMetas, + }, + { + source: "@webstudio-is/sdk-components-react-radix", + metas: radixComponentNamespacedMetas, + }, + { + source: "@webstudio-is/sdk-components-react-router", + metas: reactRouterComponentMetas, + }, + ], + html: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`), + template: htmlTemplate, + }, + ], + xml: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`), + template: xmlTemplate, + }, + ], + redirect: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.ts`), + template: redirectTemplate, + }, + ], + defaultSitemap: () => [ + { + file: join( + "app", + "routes", + `${generateRemixRoute("/sitemap.xml")}.tsx` + ), + template: defaultSitemapTemplate, + }, + ], + }; +}; diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index b33a6e6a4ee3..e3707fc1f987 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -59,6 +59,7 @@ import { import type * as sharedConstants from "../templates/defaults/app/constants.mjs"; import { htmlToJsx } from "./html-to-jsx"; import { createFramework as createRemixFramework } from "./framework-remix"; +import { createFramework as createReactRouterFramework } from "./framework-react-router"; import { createFramework as createVikeSsgFramework } from "./framework-vike-ssg"; const limit = pLimit(10); @@ -261,6 +262,8 @@ export const prebuild = async (options: { let framework; if (options.template.includes("ssg")) { framework = await createVikeSsgFramework(); + } else if (options.template.includes("react-router-docker")) { + framework = await createReactRouterFramework(); } else { framework = await createRemixFramework(); } diff --git a/packages/cli/templates/react-router-docker/.dockerignore b/packages/cli/templates/react-router-docker/.dockerignore new file mode 100644 index 000000000000..54f78fa7b671 --- /dev/null +++ b/packages/cli/templates/react-router-docker/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md diff --git a/packages/cli/templates/react-router-docker/.gitignore b/packages/cli/templates/react-router-docker/.gitignore new file mode 100644 index 000000000000..9b7c041f96ea --- /dev/null +++ b/packages/cli/templates/react-router-docker/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/packages/cli/templates/react-router-docker/Dockerfile b/packages/cli/templates/react-router-docker/Dockerfile new file mode 100644 index 000000000000..1385cf9a5f14 --- /dev/null +++ b/packages/cli/templates/react-router-docker/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm install + +FROM node:22-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm install --omit=dev + +FROM node:22-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:22-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] diff --git a/packages/cli/templates/react-router-docker/app/constants.mjs b/packages/cli/templates/react-router-docker/app/constants.mjs new file mode 100644 index 000000000000..dc5dbb0f5cb7 --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/constants.mjs @@ -0,0 +1,13 @@ +/** + * We use mjs extension as constants in this file is shared with the build script + * and we use `node --eval` to extract the constants. + */ +export const assetBaseUrl = "/assets/"; +export const imageBaseUrl = "/assets/"; + +/** + * @type {import("@webstudio-is/image").ImageLoader} + */ +export const imageLoader = ({ src }) => { + return src; +}; diff --git a/packages/cli/templates/react-router-docker/app/extension.ts b/packages/cli/templates/react-router-docker/app/extension.ts new file mode 100644 index 000000000000..bffd05d48e17 --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/extension.ts @@ -0,0 +1,13 @@ +import { ResourceRequest } from "@webstudio-is/sdk"; + +declare module "react-router" { + interface AppLoadContext { + EXCLUDE_FROM_SEARCH: boolean; + getDefaultActionResource?: (options: { + url: URL; + projectId: string; + contactEmail: string; + formData: FormData; + }) => ResourceRequest; + } +} diff --git a/packages/cli/templates/react-router-docker/app/root.tsx b/packages/cli/templates/react-router-docker/app/root.tsx new file mode 100644 index 000000000000..aa2a8c416496 --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/root.tsx @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import { Links, Meta, Outlet, useMatches } from "react-router"; +// @todo think about how to make __generated__ typeable +// @ts-ignore +import { CustomCode } from "./__generated__/_index"; + +const Root = () => { + // Get language from matches + const matches = useMatches(); + + const lastMatchWithLanguage = matches.findLast((match) => { + // @ts-ignore + const language = match?.data?.pageMeta?.language; + return language != null; + }); + + // @ts-ignore + const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? "en"; + + return ( + + + + + + + + + + + ); +}; + +export default Root; diff --git a/packages/cli/templates/react-router-docker/app/route-templates/default-sitemap.tsx b/packages/cli/templates/react-router-docker/app/route-templates/default-sitemap.tsx new file mode 100644 index 000000000000..5794be7a079d --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/route-templates/default-sitemap.tsx @@ -0,0 +1,34 @@ +import type { LoaderFunctionArgs } from "react-router"; +import { sitemap } from "__SITEMAP__"; + +export const loader = (arg: LoaderFunctionArgs) => { + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + + const urls = sitemap.map((page) => { + const url = new URL(`https://${host}${page.path}`); + + return ` + + ${url.href} + ${page.lastModified.split("T")[0]} + + `; + }); + + return new Response( + ` + +${urls.join("")} + + `, + { + headers: { + "Content-Type": "application/xml", + }, + status: 200, + } + ); +}; diff --git a/packages/cli/templates/react-router-docker/app/route-templates/html.tsx b/packages/cli/templates/react-router-docker/app/route-templates/html.tsx new file mode 100644 index 000000000000..db13bf4a630a --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/route-templates/html.tsx @@ -0,0 +1,295 @@ +import { + type MetaFunction, + type LinksFunction, + type LinkDescriptor, + type ActionFunctionArgs, + type LoaderFunctionArgs, + type HeadersFunction, + data, + redirect, + useLoaderData, +} from "react-router"; +import { + isLocalResource, + loadResource, + loadResources, + formIdFieldName, + formBotFieldName, +} from "@webstudio-is/sdk/runtime"; +import { + ReactSdkContext, + PageSettingsMeta, + PageSettingsTitle, +} from "@webstudio-is/react-sdk/runtime"; +import { + Page, + siteName, + favIconAsset, + pageFontAssets, + pageBackgroundImageAssets, +} from "__CLIENT__"; +import { + getResources, + getPageMeta, + getRemixParams, + projectId, + contactEmail, +} from "__SERVER__"; +import { assetBaseUrl, imageLoader } from "__CONSTANTS__"; +import css from "__CSS__?url"; +import { sitemap } from "__SITEMAP__"; + +const customFetch: typeof fetch = (input, init) => { + if (typeof input !== "string") { + return fetch(input, init); + } + + if (isLocalResource(input, "sitemap.xml")) { + // @todo: dynamic import sitemap ??? + const response = new Response(JSON.stringify(sitemap)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); + } + + return fetch(input, init); +}; + +export const loader = async (arg: LoaderFunctionArgs) => { + const url = new URL(arg.request.url); + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + url.host = host; + url.protocol = "https"; + + const params = getRemixParams(arg.params); + const system = { + params, + search: Object.fromEntries(url.searchParams), + origin: url.origin, + }; + + const resources = await loadResources( + customFetch, + getResources({ system }).data + ); + const pageMeta = getPageMeta({ system, resources }); + + if (pageMeta.redirect) { + const status = + pageMeta.status === 301 || pageMeta.status === 302 + ? pageMeta.status + : 302; + throw redirect(pageMeta.redirect, status); + } + + // typecheck + arg.context.EXCLUDE_FROM_SEARCH satisfies boolean; + + if (arg.context.EXCLUDE_FROM_SEARCH) { + pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH; + } + + return data( + { + host, + url: url.href, + system, + resources, + pageMeta, + }, + // No way for current information to change, so add cache for 10 minutes + // In case of CRM Data, this should be set to 0 + { + status: pageMeta.status, + headers: { + "Cache-Control": "public, max-age=600", + }, + } + ); +}; + +export const headers: HeadersFunction = () => { + return { + "Cache-Control": "public, max-age=0, must-revalidate", + }; +}; + +export const meta: MetaFunction = ({ data }) => { + const metas: ReturnType = []; + if (data === undefined) { + return metas; + } + + const origin = `https://${data.host}`; + + if (siteName) { + metas.push({ + "script:ld+json": { + "@context": "https://schema.org", + "@type": "WebSite", + name: siteName, + url: origin, + }, + }); + } + + return metas; +}; + +export const links: LinksFunction = () => { + const result: LinkDescriptor[] = []; + + result.push({ + rel: "stylesheet", + href: css, + }); + + if (favIconAsset) { + result.push({ + rel: "icon", + href: imageLoader({ + src: `${assetBaseUrl}${favIconAsset.name}`, + // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search + width: 144, + height: 144, + fit: "pad", + quality: 100, + format: "auto", + }), + type: undefined, + }); + } + + for (const asset of pageFontAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${asset.name}`, + as: "font", + crossOrigin: "anonymous", + }); + } + + for (const backgroundImageAsset of pageBackgroundImageAssets) { + result.push({ + rel: "preload", + href: `${assetBaseUrl}${backgroundImageAsset.name}`, + as: "image", + }); + } + + return result; +}; + +const getRequestHost = (request: Request): string => + request.headers.get("x-forwarded-host") || request.headers.get("host") || ""; + +export const action = async ({ + request, + context, +}: ActionFunctionArgs): Promise< + { success: true } | { success: false; errors: string[] } +> => { + try { + const url = new URL(request.url); + url.host = getRequestHost(request); + + const formData = await request.formData(); + + const system = { + params: {}, + search: {}, + origin: url.origin, + }; + + const resourceName = formData.get(formIdFieldName); + let resource = + typeof resourceName === "string" + ? getResources({ system }).action.get(resourceName) + : undefined; + + const formBotValue = formData.get(formBotFieldName); + + if (formBotValue == null || typeof formBotValue !== "string") { + throw new Error("Form bot field not found"); + } + + const submitTime = parseInt(formBotValue, 16); + // Assumes that the difference between the server time and the form submission time, + // including any client-server time drift, is within a 5-minute range. + // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes. + // Example: `formBotValue: jsdom`, or `formBotValue: headless-env` + if ( + Number.isNaN(submitTime) || + Math.abs(Date.now() - submitTime) > 1000 * 60 * 5 + ) { + throw new Error(`Form bot value invalid ${formBotValue}`); + } + + formData.delete(formIdFieldName); + formData.delete(formBotFieldName); + + if (resource) { + resource.headers.push({ + name: "Content-Type", + value: "application/json", + }); + resource.body = Object.fromEntries(formData); + } else { + if (contactEmail === undefined) { + throw new Error("Contact email not found"); + } + + resource = context.getDefaultActionResource?.({ + url, + projectId, + contactEmail, + formData, + }); + } + + if (resource === undefined) { + throw Error("Resource not found"); + } + const { ok, statusText } = await loadResource(fetch, resource); + if (ok) { + return { success: true }; + } + return { success: false, errors: [statusText] }; + } catch (error) { + console.error(error); + + return { + success: false, + errors: [error instanceof Error ? error.message : "Unknown error"], + }; + } +}; + +const Outlet = () => { + const { system, resources, url, pageMeta, host } = + useLoaderData(); + return ( + + {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */} + + + {pageMeta.title} + + ); +}; + +export default Outlet; diff --git a/packages/cli/templates/react-router-docker/app/route-templates/redirect.tsx b/packages/cli/templates/react-router-docker/app/route-templates/redirect.tsx new file mode 100644 index 000000000000..0e7d00e3b8c1 --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/route-templates/redirect.tsx @@ -0,0 +1,6 @@ +import { redirect } from "react-router"; +import { url, status } from "__REDIRECT__"; + +export const loader = () => { + throw redirect(url, status); +}; diff --git a/packages/cli/templates/react-router-docker/app/route-templates/xml.tsx b/packages/cli/templates/react-router-docker/app/route-templates/xml.tsx new file mode 100644 index 000000000000..f48c4d28568e --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/route-templates/xml.tsx @@ -0,0 +1,85 @@ +import { renderToString } from "react-dom/server"; +import { type LoaderFunctionArgs, redirect } from "react-router"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + ReactSdkContext, + xmlNodeTagSuffix, +} from "@webstudio-is/react-sdk/runtime"; +import { Page } from "__CLIENT__"; +import { getPageMeta, getRemixParams, getResources } from "__SERVER__"; +import { assetBaseUrl, imageLoader } from "__CONSTANTS__"; +import { sitemap } from "__SITEMAP__"; + +const customFetch: typeof fetch = (input, init) => { + if (typeof input !== "string") { + return fetch(input, init); + } + + if (isLocalResource(input, "sitemap.xml")) { + // @todo: dynamic import sitemap ??? + const response = new Response(JSON.stringify(sitemap)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); + } + + return fetch(input, init); +}; + +export const loader = async (arg: LoaderFunctionArgs) => { + const url = new URL(arg.request.url); + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + url.host = host; + url.protocol = "https"; + + const params = getRemixParams(arg.params); + + const system = { + params, + search: Object.fromEntries(url.searchParams), + origin: url.origin, + }; + + const resources = await loadResources( + customFetch, + getResources({ system }).data + ); + const pageMeta = getPageMeta({ system, resources }); + + if (pageMeta.redirect) { + const status = + pageMeta.status === 301 || pageMeta.status === 302 + ? pageMeta.status + : 302; + return redirect(pageMeta.redirect, status); + } + + // typecheck + arg.context.EXCLUDE_FROM_SEARCH satisfies boolean; + + let text = renderToString( + + + + ); + + // Xml is wrapped with to prevent React from hoisting elements like , , and out of their intended scope during rendering. + // More details: https://github.com/facebook/react/blob/7c8e5e7ab8bb63de911637892392c5efd8ce1d0f/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3083 + text = text.replace(/^/g, "").replace(/<\/svg>$/g, ""); + + // React has issues rendering certain elements, such as errors when a element has children. + // To render XML, we wrap it with an tag and add a suffix to avoid React's default behavior on these elements. + text = text.replaceAll(xmlNodeTagSuffix, ""); + + return new Response(`\n${text}`, { + headers: { "Content-Type": "application/xml" }, + }); +}; diff --git a/packages/cli/templates/react-router-docker/app/routes/[robots.txt].tsx b/packages/cli/templates/react-router-docker/app/routes/[robots.txt].tsx new file mode 100644 index 000000000000..cb8fe07b8f6f --- /dev/null +++ b/packages/cli/templates/react-router-docker/app/routes/[robots.txt].tsx @@ -0,0 +1,24 @@ +import type { LoaderFunctionArgs } from "react-router"; + +export const loader = (arg: LoaderFunctionArgs) => { + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + + return new Response( + ` +User-agent: * +Disallow: /api/ + +Sitemap: https://${host}/sitemap.xml + + `, + { + headers: { + "Content-Type": "text/plain", + }, + status: 200, + } + ); +}; diff --git a/packages/cli/templates/react-router-docker/package.json b/packages/cli/templates/react-router-docker/package.json new file mode 100644 index 000000000000..37d4b13e2702 --- /dev/null +++ b/packages/cli/templates/react-router-docker/package.json @@ -0,0 +1,34 @@ +{ + "type": "module", + "private": true, + "sideEffects": false, + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "typecheck": "tsc" + }, + "dependencies": { + "@react-router/dev": "^7.1.1", + "@react-router/fs-routes": "^7.1.1", + "@react-router/node": "^7.1.1", + "@webstudio-is/image": "0.0.0-webstudio-version", + "@webstudio-is/react-sdk": "0.0.0-webstudio-version", + "@webstudio-is/sdk": "0.0.0-webstudio-version", + "@webstudio-is/sdk-components-animation": "0.0.0-webstudio-version", + "@webstudio-is/sdk-components-react-radix": "0.0.0-webstudio-version", + "@webstudio-is/sdk-components-react-router": "0.0.0-webstudio-version", + "@webstudio-is/sdk-components-react": "0.0.0-webstudio-version", + "isbot": "^5.1.19", + "react": "18.3.0-canary-14898b6a9-20240318", + "react-dom": "18.3.0-canary-14898b6a9-20240318", + "vite": "^5.4.11" + }, + "devDependencies": { + "@types/react": "^18.2.70", + "@types/react-dom": "^18.2.25", + "typescript": "5.7.2" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/cli/templates/react-router-docker/public/favicon.ico b/packages/cli/templates/react-router-docker/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9cb8c815dc0abbc6b9038edea292275bf56e2537 GIT binary patch literal 15086 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 NPS;av>ny9R_rKT{U=08O literal 0 HcmV?d00001 diff --git a/packages/cli/templates/react-router-docker/tsconfig.json b/packages/cli/templates/react-router-docker/tsconfig.json new file mode 100644 index 000000000000..441f6e6fcb61 --- /dev/null +++ b/packages/cli/templates/react-router-docker/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client", "@webstudio-is/react-sdk/placeholder"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/packages/cli/templates/react-router-docker/vite.config.ts b/packages/cli/templates/react-router-docker/vite.config.ts new file mode 100644 index 000000000000..317d7dc0a1c6 --- /dev/null +++ b/packages/cli/templates/react-router-docker/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import { reactRouter } from "@react-router/dev/vite"; + +export default defineConfig({ + plugins: [reactRouter()], +}); diff --git a/packages/sdk-components-react-router/src/metas.ts b/packages/sdk-components-react-router/src/metas.ts index e56e6b28f9fb..b71fcc9b1486 100644 --- a/packages/sdk-components-react-router/src/metas.ts +++ b/packages/sdk-components-react-router/src/metas.ts @@ -4,4 +4,4 @@ export { RichTextLink, Form, RemixForm, -} from "@webstudio-is/sdk-components-react"; +} from "@webstudio-is/sdk-components-react/metas"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c0c6f231177..1c06af7b665d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,70 @@ importers: specifier: ^2.1.8 version: 2.1.8(@types/node@22.10.2)(jsdom@20.0.3) + fixtures/react-router-docker: + dependencies: + '@react-router/dev': + specifier: ^7.1.1 + version: 7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)) + '@react-router/fs-routes': + specifier: ^7.1.1 + version: 7.1.1(@react-router/dev@7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)))(typescript@5.7.2) + '@react-router/node': + specifier: ^7.1.1 + version: 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + '@react-router/serve': + specifier: ^7.1.1 + version: 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + '@webstudio-is/image': + specifier: workspace:* + version: link:../../packages/image + '@webstudio-is/react-sdk': + specifier: workspace:* + version: link:../../packages/react-sdk + '@webstudio-is/sdk': + specifier: workspace:* + version: link:../../packages/sdk + '@webstudio-is/sdk-components-animation': + specifier: workspace:* + version: link:../../packages/sdk-components-animation + '@webstudio-is/sdk-components-react': + specifier: workspace:* + version: link:../../packages/sdk-components-react + '@webstudio-is/sdk-components-react-radix': + specifier: workspace:* + version: link:../../packages/sdk-components-react-radix + '@webstudio-is/sdk-components-react-router': + specifier: workspace:* + version: link:../../packages/sdk-components-react-router + isbot: + specifier: ^5.1.19 + version: 5.1.19 + react: + specifier: 18.3.0-canary-14898b6a9-20240318 + version: 18.3.0-canary-14898b6a9-20240318 + react-dom: + specifier: 18.3.0-canary-14898b6a9-20240318 + version: 18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318) + react-router: + specifier: ^7.1.1 + version: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + vite: + specifier: ^5.4.11 + version: 5.4.11(@types/node@22.10.2) + webstudio: + specifier: workspace:* + version: link:../../packages/cli + devDependencies: + '@types/react': + specifier: ^18.2.70 + version: 18.2.79 + '@types/react-dom': + specifier: ^18.2.25 + version: 18.2.25 + typescript: + specifier: 5.7.2 + version: 5.7.2 + fixtures/ssg: dependencies: '@webstudio-is/image': @@ -1090,6 +1154,9 @@ importers: '@netlify/remix-edge-adapter': specifier: 3.4.2 version: 3.4.2(@remix-run/react@2.15.2(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)(typescript@5.7.2))(@remix-run/serve@2.15.2(typescript@5.7.2))(@remix-run/server-runtime@2.15.2(typescript@5.7.2))(@types/node@22.10.2)(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)) + '@react-router/dev': + specifier: ^7.1.1 + version: 7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)) '@remix-run/cloudflare': specifier: ^2.15.2 version: 2.15.2(@cloudflare/workers-types@4.20240701.0)(typescript@5.7.2) @@ -1144,6 +1211,9 @@ importers: '@webstudio-is/sdk-components-react-remix': specifier: workspace:* version: link:../sdk-components-react-remix + '@webstudio-is/sdk-components-react-router': + specifier: workspace:* + version: link:../sdk-components-react-router '@webstudio-is/tsconfig': specifier: workspace:* version: link:../tsconfig @@ -1153,6 +1223,9 @@ importers: react-dom: specifier: 18.3.0-canary-14898b6a9-20240318 version: 18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318) + react-router: + specifier: ^7.1.1 + version: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) ts-expect: specifier: ^1.3.0 version: 1.3.0 @@ -3395,10 +3468,6 @@ packages: typescript: optional: true - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -3407,26 +3476,16 @@ packages: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.1.2': - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.18': - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -3557,6 +3616,9 @@ packages: resolution: {integrity: sha512-PYn05ET2USfBAeXF6NZfWl0O32KVyE8ncQ/ngysrh3hoIV7l3qGGH7ubeFx+D8VWQ682qYhwGygUzQv2j1tGGg==} engines: {node: '>=16.13'} + '@mjackson/node-fetch-server@0.2.0': + resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} + '@nanostores/react@0.8.0': resolution: {integrity: sha512-MhbVB7NQLboq/Z9fRTDen9zib/YCffe6mn+3Xg5MOYByMX5Xx98SOZjk/Nd3yvOd/g7GjlQqwXj0KF2lPb6CEQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4345,6 +4407,62 @@ packages: peerDependencies: react: 18.3.0-canary-14898b6a9-20240318 + '@react-router/dev@7.1.1': + resolution: {integrity: sha512-+UCrQZBAmdRcC7Bx1ho89T/DeP+FzEErkzrTvdBCpstr8AzOQ6mKlaglXGty15o3fgihBSFF4/J67jGveYIR8Q==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@react-router/serve': ^7.1.1 + react-router: ^7.1.1 + typescript: ^5.1.0 + vite: ^5.1.0 || ^6.0.0 + wrangler: ^3.28.2 + peerDependenciesMeta: + '@react-router/serve': + optional: true + typescript: + optional: true + wrangler: + optional: true + + '@react-router/express@7.1.1': + resolution: {integrity: sha512-oiL2ADor3byuh7piajLTPr6007GmVPZ1Gh4HiN0uuZlz3vQ1rd0xZMSD9LnSrXhsrKEbPFaeCk8E2O67ZoABsg==} + engines: {node: '>=20.0.0'} + peerDependencies: + express: ^4.17.1 + react-router: 7.1.1 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@react-router/fs-routes@7.1.1': + resolution: {integrity: sha512-FXe4d5sKRwa3Yj1xz4rtnxpmwG1IENr+Jd8fQd4IxbC6u/HJrIURacq1l83cTr62Uerf5N4h3v3pGgDEmmGwcg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@react-router/dev': ^7.1.1 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@react-router/node@7.1.1': + resolution: {integrity: sha512-5X79SfJ1IEEsttt0oo9rhO9kgxXyBTKdVBsz3h0WHTkRzbRk0VEpVpBW3PQ1RpkgEaAHwJ8obVl4k4brdDSExA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react-router: 7.1.1 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@react-router/serve@7.1.1': + resolution: {integrity: sha512-rhV1yp72ZZQn4giQUzUiLVo/7/7dhxD98Z5pdDm6mKOTJPGoQ8TBPccQaKxzJIFNRHcn0sEdehfLOxl5ydnUKw==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + react-router: 7.1.1 + '@react-stately/utils@3.10.5': resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==} peerDependencies: @@ -5182,6 +5300,9 @@ packages: axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + babel-dead-code-elimination@1.0.8: + resolution: {integrity: sha512-og6HQERk0Cmm+nTT4Od2wbPtgABXFMPaHACjbKLulZIFMkYyXZLkUGuAxdgpMJBrxyt/XFpSz++lNzjbcMnPkQ==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -5244,11 +5365,6 @@ packages: browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - browserslist@4.24.0: - resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.24.3: resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5298,9 +5414,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001667: - resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==} - caniuse-lite@1.0.30001690: resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} @@ -5344,10 +5457,14 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -5570,6 +5687,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -5703,9 +5829,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.32: - resolution: {integrity: sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==} - electron-to-chromium@1.5.76: resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} @@ -7155,9 +7278,6 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -7751,6 +7871,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + recast@0.23.9: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} @@ -8403,6 +8527,10 @@ packages: resolution: {integrity: sha512-VviMt2tlMg1BvQ0FKXxrz1eJuyrcISrL2sPfBf7ZskX/FCEc/7LeThQaoygsMJpNqrATWQIsRVx+1Dpe4jaYuQ==} engines: {node: '>=18.17'} + undici@6.21.0: + resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} + engines: {node: '>=18.17'} + unenv-nightly@1.10.0-1717606461.a117952: resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} @@ -8608,6 +8736,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@3.0.0-beta.2: + resolution: {integrity: sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@5.4.11: resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8874,8 +9007,8 @@ snapshots: '@ampproject/remapping@2.2.1': dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.0': dependencies: @@ -8958,7 +9091,7 @@ snapshots: dependencies: '@babel/compat-data': 7.26.2 '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.0 + browserslist: 4.24.3 lru-cache: 5.1.1 semver: 6.3.1 @@ -9810,39 +9943,24 @@ snapshots: optionalDependencies: typescript: 5.7.2 - '@jridgewell/gen-mapping@0.3.2': - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.0': {} - '@jridgewell/set-array@1.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.4.14': {} - '@jridgewell/sourcemap-codec@1.4.15': {} '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.18': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping@0.3.9': dependencies: @@ -10094,6 +10212,8 @@ snapshots: dependencies: '@miniflare/shared': 2.14.4 + '@mjackson/node-fetch-server@0.2.0': {} + '@nanostores/react@0.8.0(nanostores@0.11.3)(react@18.3.0-canary-14898b6a9-20240318)': dependencies: nanostores: 0.11.3 @@ -10942,6 +11062,95 @@ snapshots: clsx: 2.1.1 react: 18.3.0-canary-14898b6a9-20240318 + '@react-router/dev@7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0))': + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/plugin-syntax-decorators': 7.24.1(@babel/core@7.26.0) + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.26.0) + '@babel/preset-typescript': 7.24.1(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + '@npmcli/package-json': 4.0.1 + '@react-router/node': 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + arg: 5.0.2 + babel-dead-code-elimination: 1.0.8 + chokidar: 4.0.3 + dedent: 1.5.3 + es-module-lexer: 1.5.4 + exit-hook: 2.2.1 + fs-extra: 10.1.0 + gunzip-maybe: 1.4.2 + jsesc: 3.0.2 + lodash: 4.17.21 + pathe: 1.1.2 + picocolors: 1.1.1 + picomatch: 2.3.1 + prettier: 2.8.7 + react-refresh: 0.14.2 + react-router: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + semver: 7.6.3 + set-cookie-parser: 2.6.0 + valibot: 0.41.0(typescript@5.7.2) + vite: 5.4.11(@types/node@22.10.2) + vite-node: 3.0.0-beta.2(@types/node@22.10.2) + optionalDependencies: + '@react-router/serve': 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + typescript: 5.7.2 + wrangler: 3.63.2(@cloudflare/workers-types@4.20240701.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - bluebird + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + '@react-router/express@7.1.1(express@4.21.1)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)': + dependencies: + '@react-router/node': 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + express: 4.21.1 + react-router: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + optionalDependencies: + typescript: 5.7.2 + + '@react-router/fs-routes@7.1.1(@react-router/dev@7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)))(typescript@5.7.2)': + dependencies: + '@react-router/dev': 7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2))(@types/node@22.10.2)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2))(wrangler@3.63.2(@cloudflare/workers-types@4.20240701.0)) + minimatch: 9.0.4 + optionalDependencies: + typescript: 5.7.2 + + '@react-router/node@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + react-router: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.21.0 + optionalDependencies: + typescript: 5.7.2 + + '@react-router/serve@7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2)': + dependencies: + '@react-router/express': 7.1.1(express@4.21.1)(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + '@react-router/node': 7.1.1(react-router@7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318))(typescript@5.7.2) + compression: 1.7.4 + express: 4.21.1 + get-port: 5.1.1 + morgan: 1.10.0 + react-router: 7.1.1(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + source-map-support: 0.5.21 + transitivePeerDependencies: + - supports-color + - typescript + '@react-stately/utils@3.10.5(react@18.3.0-canary-14898b6a9-20240318)': dependencies: '@swc/helpers': 0.5.15 @@ -10987,7 +11196,7 @@ snapshots: arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 - chokidar: 3.5.3 + chokidar: 3.6.0 cross-spawn: 7.0.6 dotenv: 16.3.1 es-module-lexer: 1.5.4 @@ -11082,7 +11291,7 @@ snapshots: dependencies: '@remix-run/express': 2.15.2(express@4.21.1)(typescript@5.7.2) '@remix-run/node': 2.15.2(typescript@5.7.2) - chokidar: 3.5.3 + chokidar: 3.6.0 compression: 1.7.4 express: 4.21.1 get-port: 5.1.1 @@ -11885,7 +12094,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color optional: true @@ -12013,6 +12222,15 @@ snapshots: dependencies: dequal: 2.0.3 + babel-dead-code-elimination@1.0.8: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -12089,13 +12307,6 @@ snapshots: dependencies: pako: 0.2.9 - browserslist@4.24.0: - dependencies: - caniuse-lite: 1.0.30001667 - electron-to-chromium: 1.5.32 - node-releases: 2.0.18 - update-browserslist-db: 1.1.1(browserslist@4.24.0) - browserslist@4.24.3: dependencies: caniuse-lite: 1.0.30001690 @@ -12156,8 +12367,6 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001667: {} - caniuse-lite@1.0.30001690: {} capnp-ts@0.7.0: @@ -12202,7 +12411,7 @@ snapshots: check-error@2.1.1: {} - chokidar@3.5.3: + chokidar@3.6.0: dependencies: anymatch: 3.1.3 braces: 3.0.2 @@ -12214,6 +12423,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.1 + chownr@1.1.4: {} chownr@2.0.0: {} @@ -12408,6 +12621,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decimal.js@10.4.3: optional: true @@ -12547,8 +12764,6 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.32: {} - electron-to-chromium@1.5.76: {} emittery@0.12.1: {} @@ -13317,7 +13532,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color optional: true @@ -13325,7 +13540,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color optional: true @@ -14517,8 +14732,6 @@ snapshots: node-forge@1.3.1: {} - node-releases@2.0.18: {} - node-releases@2.0.19: {} normalize-package-data@2.5.0: @@ -15146,6 +15359,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.1: {} + recast@0.23.9: dependencies: ast-types: 0.16.1 @@ -15816,6 +16031,8 @@ snapshots: undici@6.15.0: {} + undici@6.21.0: {} + unenv-nightly@1.10.0-1717606461.a117952: dependencies: consola: 3.2.3 @@ -15918,12 +16135,6 @@ snapshots: untruncate-json@0.0.1: {} - update-browserslist-db@1.1.1(browserslist@4.24.0): - dependencies: - browserslist: 4.24.0 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.1(browserslist@4.24.3): dependencies: browserslist: 4.24.3 @@ -16081,6 +16292,24 @@ snapshots: - supports-color - terser + vite-node@3.0.0-beta.2(@types/node@22.10.2): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.5.4 + pathe: 1.1.2 + vite: 5.4.11(@types/node@22.10.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite@5.4.11(@types/node@22.10.2): dependencies: esbuild: 0.21.5 @@ -16242,7 +16471,7 @@ snapshots: '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 - chokidar: 3.5.3 + chokidar: 3.6.0 date-fns: 3.6.0 esbuild: 0.17.19 miniflare: 3.20240701.0