webstudio 0.269.0 → 0.271.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.269.0",
3
+ "version": "0.271.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -43,8 +43,9 @@
43
43
  "warn-once": "^0.1.1",
44
44
  "yargs": "^17.7.2",
45
45
  "zod": "^3.24.2",
46
- "@webstudio-is/project-migrations": "0.269.0",
47
- "@webstudio-is/trpc-interface": "0.269.0"
46
+ "@webstudio-is/protocol": "0.271.0",
47
+ "@webstudio-is/project-migrations": "0.271.0",
48
+ "@webstudio-is/trpc-interface": "0.271.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@cloudflare/vite-plugin": "^1.1.0",
@@ -74,18 +75,18 @@
74
75
  "vite": "^6.3.4",
75
76
  "vitest": "^3.1.2",
76
77
  "wrangler": "^3.63.2",
77
- "@webstudio-is/http-client": "0.269.0",
78
- "@webstudio-is/image": "0.269.0",
79
- "@webstudio-is/react-sdk": "0.269.0",
80
- "@webstudio-is/sdk-components-animation": "0.269.0",
81
- "@webstudio-is/css-engine": "0.269.0",
82
- "@webstudio-is/sdk": "0.269.0",
83
- "@webstudio-is/sdk-components-react": "0.269.0",
84
- "@webstudio-is/sdk-components-react-radix": "0.269.0",
85
- "@webstudio-is/sdk-components-react-remix": "0.269.0",
86
- "@webstudio-is/sdk-components-react-router": "0.269.0",
78
+ "@webstudio-is/css-engine": "0.271.0",
79
+ "@webstudio-is/react-sdk": "0.271.0",
80
+ "@webstudio-is/image": "0.271.0",
81
+ "@webstudio-is/http-client": "0.271.0",
82
+ "@webstudio-is/sdk": "0.271.0",
83
+ "@webstudio-is/sdk-components-animation": "0.271.0",
84
+ "@webstudio-is/sdk-components-react-radix": "0.271.0",
85
+ "@webstudio-is/sdk-components-react": "0.271.0",
87
86
  "@webstudio-is/tsconfig": "1.0.7",
88
- "@webstudio-is/wsauth": "0.269.0"
87
+ "@webstudio-is/wsauth": "0.271.0",
88
+ "@webstudio-is/sdk-components-react-router": "0.271.0",
89
+ "@webstudio-is/sdk-components-react-remix": "0.271.0"
89
90
  },
90
91
  "scripts": {
91
92
  "typecheck": "tsgo --noEmit",
@@ -12,7 +12,7 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
- "@webstudio-is/wsauth": "0.269.0",
15
+ "@webstudio-is/wsauth": "0.271.0",
16
16
  "@remix-run/cloudflare": "2.16.5",
17
17
  "@remix-run/cloudflare-pages": "2.16.5"
18
18
  },
@@ -1,4 +1,21 @@
1
- import { createPath, generatePath, parsePath } from "@remix-run/react";
1
+ import {
2
+ createPath,
3
+ generatePath,
4
+ matchPath,
5
+ parsePath,
6
+ } from "@remix-run/react";
7
+ import { redirect } from "@remix-run/server-runtime";
8
+
9
+ type RedirectItem = {
10
+ old: string;
11
+ new: string;
12
+ status?: number | string;
13
+ };
14
+
15
+ type MatchedRedirect = {
16
+ url: string;
17
+ status: number;
18
+ };
2
19
 
3
20
  /**
4
21
  * Expands route params in local redirect targets.
@@ -9,13 +26,142 @@ export const generateRedirectUrl = (
9
26
  url: string,
10
27
  params: Record<string, string | undefined>
11
28
  ) => {
12
- if (url.startsWith("/") === false || url.startsWith("//")) {
29
+ if (
30
+ /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url) ||
31
+ url.startsWith("//") ||
32
+ url.startsWith("?") ||
33
+ url.startsWith("#")
34
+ ) {
13
35
  return url;
14
36
  }
15
37
 
38
+ const targetParams = Object.fromEntries(
39
+ Object.entries(params).map(([name, value]) => [
40
+ name,
41
+ name === "*" ? value : value?.replaceAll("/", "%2F"),
42
+ ])
43
+ );
16
44
  const path = parsePath(url);
17
- return createPath({
18
- ...path,
19
- pathname: generatePath(path.pathname ?? "/", params),
20
- });
45
+ try {
46
+ return createPath({
47
+ ...path,
48
+ pathname: generatePath(path.pathname ?? "/", targetParams),
49
+ });
50
+ } catch {
51
+ return url;
52
+ }
53
+ };
54
+
55
+ const stripHash = (source: string) => {
56
+ const hashIndex = source.indexOf("#");
57
+ return hashIndex === -1 ? source : source.slice(0, hashIndex);
58
+ };
59
+
60
+ const getRedirectStatus = (status: RedirectItem["status"]) => {
61
+ return Number(status) === 302 ? 302 : 301;
62
+ };
63
+
64
+ const decodePathname = (pathname: string) => {
65
+ try {
66
+ return decodeURI(pathname);
67
+ } catch {
68
+ return pathname;
69
+ }
70
+ };
71
+
72
+ const getPathnameVariants = (pathname: string) => {
73
+ return Array.from(new Set([pathname, decodePathname(pathname)]));
74
+ };
75
+
76
+ const isOptionalSegmentMarker = (source: string, index: number) => {
77
+ const nextChar = source[index + 1];
78
+ if (nextChar !== undefined && nextChar !== "/") {
79
+ return false;
80
+ }
81
+
82
+ const segmentStart = source.lastIndexOf("/", index - 1);
83
+ const segment = source.slice(segmentStart + 1, index);
84
+ return segment !== "";
85
+ };
86
+
87
+ const getSourceSearchIndex = (source: string) => {
88
+ for (let index = 0; index < source.length; index += 1) {
89
+ if (
90
+ source[index] === "?" &&
91
+ isOptionalSegmentMarker(source, index) === false
92
+ ) {
93
+ return index;
94
+ }
95
+ }
96
+ return -1;
97
+ };
98
+
99
+ const isRedirectPattern = (source: string) => {
100
+ return (
101
+ /(^|\/):[^/]+/.test(source) ||
102
+ /(^|\/)\*(?=\/|$)/.test(source) ||
103
+ /(^|\/)[^/]+\?(?=\/|$)/.test(source)
104
+ );
105
+ };
106
+
107
+ export const matchRedirect = (
108
+ requestUrl: string,
109
+ redirects: RedirectItem[]
110
+ ): MatchedRedirect | undefined => {
111
+ const url = new URL(requestUrl);
112
+ const requestPathnames = getPathnameVariants(url.pathname);
113
+
114
+ for (const redirect of redirects) {
115
+ const source = stripHash(redirect.old);
116
+ if (getSourceSearchIndex(source) !== -1) {
117
+ const exactMatch = requestPathnames.some(
118
+ (requestPathname) => source === `${requestPathname}${url.search}`
119
+ );
120
+ if (exactMatch) {
121
+ return {
122
+ url: generateRedirectUrl(redirect.new, {}),
123
+ status: getRedirectStatus(redirect.status),
124
+ };
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (isRedirectPattern(source) === false) {
130
+ if (requestPathnames.includes(source)) {
131
+ return {
132
+ url: generateRedirectUrl(redirect.new, {}),
133
+ status: getRedirectStatus(redirect.status),
134
+ };
135
+ }
136
+ continue;
137
+ }
138
+
139
+ const match = requestPathnames
140
+ .map((requestPathname) =>
141
+ matchPath(
142
+ { path: source, caseSensitive: true, end: true },
143
+ requestPathname
144
+ )
145
+ )
146
+ .find((match) => match !== null);
147
+ if (match === undefined) {
148
+ continue;
149
+ }
150
+
151
+ return {
152
+ url: generateRedirectUrl(redirect.new, match.params),
153
+ status: getRedirectStatus(redirect.status),
154
+ };
155
+ }
156
+ };
157
+
158
+ export const redirectRequest = (
159
+ request: Request,
160
+ redirects: RedirectItem[]
161
+ ): Response | undefined => {
162
+ const matchedRedirect = matchRedirect(request.url, redirects);
163
+ if (matchedRedirect === undefined) {
164
+ return;
165
+ }
166
+ return redirect(matchedRedirect.url, matchedRedirect.status);
21
167
  };
@@ -1,10 +1,25 @@
1
1
  /* eslint-disable @typescript-eslint/ban-ts-comment */
2
2
 
3
3
  import { Links, Meta, Outlet, useMatches } from "@remix-run/react";
4
- import type { HeadersFunction } from "@remix-run/server-runtime";
4
+ import type {
5
+ HeadersFunction,
6
+ LoaderFunctionArgs,
7
+ } from "@remix-run/server-runtime";
8
+ import { redirectRequest } from "./redirect-url";
5
9
  // @todo think about how to make __generated__ typeable
6
10
  // @ts-ignore
7
11
  import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
12
+ // @ts-ignore
13
+ import { redirects } from "./__generated__/$resources.redirects";
14
+
15
+ export const loader = ({ request }: LoaderFunctionArgs) => {
16
+ const redirectResponse = redirectRequest(request, redirects);
17
+ if (redirectResponse !== undefined) {
18
+ return redirectResponse;
19
+ }
20
+
21
+ return null;
22
+ };
8
23
 
9
24
  export const headers: HeadersFunction = ({ errorHeaders }) => {
10
25
  if (errorHeaders) {
@@ -11,14 +11,14 @@
11
11
  "@remix-run/node": "2.16.5",
12
12
  "@remix-run/react": "2.16.5",
13
13
  "@remix-run/server-runtime": "2.16.5",
14
- "@webstudio-is/image": "0.269.0",
15
- "@webstudio-is/react-sdk": "0.269.0",
16
- "@webstudio-is/sdk": "0.269.0",
17
- "@webstudio-is/sdk-components-react": "0.269.0",
18
- "@webstudio-is/sdk-components-animation": "0.269.0",
19
- "@webstudio-is/sdk-components-react-radix": "0.269.0",
20
- "@webstudio-is/sdk-components-react-remix": "0.269.0",
21
- "@webstudio-is/wsauth": "0.269.0",
14
+ "@webstudio-is/image": "0.271.0",
15
+ "@webstudio-is/react-sdk": "0.271.0",
16
+ "@webstudio-is/sdk": "0.271.0",
17
+ "@webstudio-is/sdk-components-react": "0.271.0",
18
+ "@webstudio-is/sdk-components-animation": "0.271.0",
19
+ "@webstudio-is/sdk-components-react-radix": "0.271.0",
20
+ "@webstudio-is/sdk-components-react-remix": "0.271.0",
21
+ "@webstudio-is/wsauth": "0.271.0",
22
22
  "isbot": "^5.1.25",
23
23
  "react": "18.3.0-canary-14898b6a9-20240318",
24
24
  "react-dom": "18.3.0-canary-14898b6a9-20240318"
@@ -1,4 +1,21 @@
1
- import { createPath, generatePath, parsePath } from "react-router";
1
+ import {
2
+ createPath,
3
+ generatePath,
4
+ matchPath,
5
+ parsePath,
6
+ redirect,
7
+ } from "react-router";
8
+
9
+ type RedirectItem = {
10
+ old: string;
11
+ new: string;
12
+ status?: number | string;
13
+ };
14
+
15
+ type MatchedRedirect = {
16
+ url: string;
17
+ status: number;
18
+ };
2
19
 
3
20
  /**
4
21
  * Expands route params in local redirect targets.
@@ -9,13 +26,142 @@ export const generateRedirectUrl = (
9
26
  url: string,
10
27
  params: Record<string, string | undefined>
11
28
  ) => {
12
- if (url.startsWith("/") === false || url.startsWith("//")) {
29
+ if (
30
+ /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url) ||
31
+ url.startsWith("//") ||
32
+ url.startsWith("?") ||
33
+ url.startsWith("#")
34
+ ) {
13
35
  return url;
14
36
  }
15
37
 
38
+ const targetParams = Object.fromEntries(
39
+ Object.entries(params).map(([name, value]) => [
40
+ name,
41
+ name === "*" ? value : value?.replaceAll("/", "%2F"),
42
+ ])
43
+ );
16
44
  const path = parsePath(url);
17
- return createPath({
18
- ...path,
19
- pathname: generatePath(path.pathname ?? "/", params),
20
- });
45
+ try {
46
+ return createPath({
47
+ ...path,
48
+ pathname: generatePath(path.pathname ?? "/", targetParams),
49
+ });
50
+ } catch {
51
+ return url;
52
+ }
53
+ };
54
+
55
+ const stripHash = (source: string) => {
56
+ const hashIndex = source.indexOf("#");
57
+ return hashIndex === -1 ? source : source.slice(0, hashIndex);
58
+ };
59
+
60
+ const getRedirectStatus = (status: RedirectItem["status"]) => {
61
+ return Number(status) === 302 ? 302 : 301;
62
+ };
63
+
64
+ const decodePathname = (pathname: string) => {
65
+ try {
66
+ return decodeURI(pathname);
67
+ } catch {
68
+ return pathname;
69
+ }
70
+ };
71
+
72
+ const getPathnameVariants = (pathname: string) => {
73
+ return Array.from(new Set([pathname, decodePathname(pathname)]));
74
+ };
75
+
76
+ const isOptionalSegmentMarker = (source: string, index: number) => {
77
+ const nextChar = source[index + 1];
78
+ if (nextChar !== undefined && nextChar !== "/") {
79
+ return false;
80
+ }
81
+
82
+ const segmentStart = source.lastIndexOf("/", index - 1);
83
+ const segment = source.slice(segmentStart + 1, index);
84
+ return segment !== "";
85
+ };
86
+
87
+ const getSourceSearchIndex = (source: string) => {
88
+ for (let index = 0; index < source.length; index += 1) {
89
+ if (
90
+ source[index] === "?" &&
91
+ isOptionalSegmentMarker(source, index) === false
92
+ ) {
93
+ return index;
94
+ }
95
+ }
96
+ return -1;
97
+ };
98
+
99
+ const isRedirectPattern = (source: string) => {
100
+ return (
101
+ /(^|\/):[^/]+/.test(source) ||
102
+ /(^|\/)\*(?=\/|$)/.test(source) ||
103
+ /(^|\/)[^/]+\?(?=\/|$)/.test(source)
104
+ );
105
+ };
106
+
107
+ export const matchRedirect = (
108
+ requestUrl: string,
109
+ redirects: RedirectItem[]
110
+ ): MatchedRedirect | undefined => {
111
+ const url = new URL(requestUrl);
112
+ const requestPathnames = getPathnameVariants(url.pathname);
113
+
114
+ for (const redirect of redirects) {
115
+ const source = stripHash(redirect.old);
116
+ if (getSourceSearchIndex(source) !== -1) {
117
+ const exactMatch = requestPathnames.some(
118
+ (requestPathname) => source === `${requestPathname}${url.search}`
119
+ );
120
+ if (exactMatch) {
121
+ return {
122
+ url: generateRedirectUrl(redirect.new, {}),
123
+ status: getRedirectStatus(redirect.status),
124
+ };
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (isRedirectPattern(source) === false) {
130
+ if (requestPathnames.includes(source)) {
131
+ return {
132
+ url: generateRedirectUrl(redirect.new, {}),
133
+ status: getRedirectStatus(redirect.status),
134
+ };
135
+ }
136
+ continue;
137
+ }
138
+
139
+ const match = requestPathnames
140
+ .map((requestPathname) =>
141
+ matchPath(
142
+ { path: source, caseSensitive: true, end: true },
143
+ requestPathname
144
+ )
145
+ )
146
+ .find((match) => match !== null);
147
+ if (match === undefined) {
148
+ continue;
149
+ }
150
+
151
+ return {
152
+ url: generateRedirectUrl(redirect.new, match.params),
153
+ status: getRedirectStatus(redirect.status),
154
+ };
155
+ }
156
+ };
157
+
158
+ export const redirectRequest = (
159
+ request: Request,
160
+ redirects: RedirectItem[]
161
+ ): Response | undefined => {
162
+ const matchedRedirect = matchRedirect(request.url, redirects);
163
+ if (matchedRedirect === undefined) {
164
+ return;
165
+ }
166
+ return redirect(matchedRedirect.url, matchedRedirect.status);
21
167
  };
@@ -2,14 +2,27 @@
2
2
 
3
3
  import {
4
4
  type HeadersFunction,
5
+ type LoaderFunctionArgs,
5
6
  Links,
6
7
  Meta,
7
8
  Outlet,
8
9
  useMatches,
9
10
  } from "react-router";
11
+ import { redirectRequest } from "./redirect-url";
10
12
  // @todo think about how to make __generated__ typeable
11
13
  // @ts-ignore
12
14
  import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
15
+ // @ts-ignore
16
+ import { redirects } from "./__generated__/$resources.redirects";
17
+
18
+ export const loader = ({ request }: LoaderFunctionArgs) => {
19
+ const redirectResponse = redirectRequest(request, redirects);
20
+ if (redirectResponse !== undefined) {
21
+ return redirectResponse;
22
+ }
23
+
24
+ return null;
25
+ };
13
26
 
14
27
  export const headers: HeadersFunction = ({ errorHeaders }) => {
15
28
  if (errorHeaders) {
@@ -10,14 +10,14 @@
10
10
  "dependencies": {
11
11
  "@react-router/dev": "^7.5.3",
12
12
  "@react-router/fs-routes": "^7.5.3",
13
- "@webstudio-is/image": "0.269.0",
14
- "@webstudio-is/react-sdk": "0.269.0",
15
- "@webstudio-is/sdk": "0.269.0",
16
- "@webstudio-is/sdk-components-animation": "0.269.0",
17
- "@webstudio-is/sdk-components-react-radix": "0.269.0",
18
- "@webstudio-is/sdk-components-react-router": "0.269.0",
19
- "@webstudio-is/sdk-components-react": "0.269.0",
20
- "@webstudio-is/wsauth": "0.269.0",
13
+ "@webstudio-is/image": "0.271.0",
14
+ "@webstudio-is/react-sdk": "0.271.0",
15
+ "@webstudio-is/sdk": "0.271.0",
16
+ "@webstudio-is/sdk-components-animation": "0.271.0",
17
+ "@webstudio-is/sdk-components-react-radix": "0.271.0",
18
+ "@webstudio-is/sdk-components-react-router": "0.271.0",
19
+ "@webstudio-is/sdk-components-react": "0.271.0",
20
+ "@webstudio-is/wsauth": "0.271.0",
21
21
  "isbot": "^5.1.25",
22
22
  "react": "18.3.0-canary-14898b6a9-20240318",
23
23
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@cloudflare/vite-plugin": "^1.1.0",
10
- "@webstudio-is/wsauth": "0.269.0",
10
+ "@webstudio-is/wsauth": "0.271.0",
11
11
  "wrangler": "^4.14.1"
12
12
  }
13
13
  }
@@ -8,12 +8,12 @@
8
8
  "typecheck": "tsgo --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@webstudio-is/image": "0.269.0",
12
- "@webstudio-is/react-sdk": "0.269.0",
13
- "@webstudio-is/sdk": "0.269.0",
14
- "@webstudio-is/sdk-components-react": "0.269.0",
15
- "@webstudio-is/sdk-components-animation": "0.269.0",
16
- "@webstudio-is/sdk-components-react-radix": "0.269.0",
11
+ "@webstudio-is/image": "0.271.0",
12
+ "@webstudio-is/react-sdk": "0.271.0",
13
+ "@webstudio-is/sdk": "0.271.0",
14
+ "@webstudio-is/sdk-components-react": "0.271.0",
15
+ "@webstudio-is/sdk-components-animation": "0.271.0",
16
+ "@webstudio-is/sdk-components-react-radix": "0.271.0",
17
17
  "react": "18.3.0-canary-14898b6a9-20240318",
18
18
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
19
19
  "vike": "^0.4.229"
@@ -1,7 +0,0 @@
1
- import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
2
- import { generateRedirectUrl } from "../redirect-url";
3
- import { url, status } from "__REDIRECT__";
4
-
5
- export const loader = (arg: LoaderFunctionArgs) => {
6
- return redirect(generateRedirectUrl(url, arg.params), status);
7
- };
@@ -1,7 +0,0 @@
1
- import { type LoaderFunctionArgs, redirect } from "react-router";
2
- import { generateRedirectUrl } from "../redirect-url";
3
- import { url, status } from "__REDIRECT__";
4
-
5
- export const loader = (arg: LoaderFunctionArgs) => {
6
- throw redirect(generateRedirectUrl(url, arg.params), status);
7
- };