webstudio 0.266.0 → 0.268.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/lib/cli.js CHANGED
@@ -374,7 +374,7 @@ const getApiCompatibilityPayload = (value) => {
374
374
  return findPayload(value, /* @__PURE__ */ new WeakSet());
375
375
  };
376
376
  const name = "webstudio";
377
- const version = "0.266.0";
377
+ const version = "0.268.0";
378
378
  const description = "Webstudio CLI";
379
379
  const author = "Webstudio <github@webstudio.is>";
380
380
  const homepage = "https://webstudio.is";
@@ -386,7 +386,7 @@ const scripts = { "typecheck": "tsgo --noEmit", "build": "rm -rf lib && vite bui
386
386
  const license = "AGPL-3.0-or-later";
387
387
  const engines = { "node": ">=22" };
388
388
  const dependencies = { "@clack/prompts": "^0.10.0", "@emotion/hash": "^0.9.2", "@trpc/client": "^10.45.2", "@webstudio-is/project-migrations": "workspace:*", "@webstudio-is/trpc-interface": "workspace:*", "acorn": "^8.14.1", "acorn-walk": "^8.3.4", "change-case": "^5.4.4", "deepmerge": "^4.3.1", "env-paths": "^3.0.0", "nanoid": "^5.1.5", "p-limit": "^6.2.0", "parse5": "7.3.0", "picocolors": "^1.1.1", "reserved-identifiers": "^1.0.0", "tinyexec": "^0.3.2", "warn-once": "^0.1.1", "yargs": "^17.7.2", "zod": "^3.24.2" };
389
- const devDependencies = { "@cloudflare/vite-plugin": "^1.1.0", "@netlify/vite-plugin-react-router": "^1.0.1", "@react-router/dev": "^7.5.3", "@react-router/fs-routes": "^7.5.3", "@remix-run/cloudflare": "^2.16.5", "@remix-run/cloudflare-pages": "^2.16.5", "@remix-run/dev": "^2.16.5", "@remix-run/node": "^2.16.5", "@remix-run/react": "^2.16.5", "@remix-run/server-runtime": "^2.16.5", "@types/react": "^18.2.70", "@types/react-dom": "^18.2.25", "@types/yargs": "^17.0.33", "@vercel/react-router": "^1.1.0", "@vitejs/plugin-react": "^4.4.1", "@webstudio-is/css-engine": "workspace:*", "@webstudio-is/http-client": "workspace:*", "@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-remix": "workspace:*", "@webstudio-is/sdk-components-react-router": "workspace:*", "@webstudio-is/tsconfig": "workspace:*", "h3": "^1.15.1", "ipx": "^3.0.3", "isbot": "^5.1.25", "prettier": "3.5.3", "react": "18.3.0-canary-14898b6a9-20240318", "react-dom": "18.3.0-canary-14898b6a9-20240318", "react-router": "^7.5.3", "ts-expect": "^1.3.0", "vike": "^0.4.229", "vite": "^6.3.4", "vitest": "^3.1.2", "wrangler": "^3.63.2" };
389
+ const devDependencies = { "@cloudflare/vite-plugin": "^1.1.0", "@netlify/vite-plugin-react-router": "^1.0.1", "@react-router/dev": "^7.5.3", "@react-router/fs-routes": "^7.5.3", "@remix-run/cloudflare": "^2.16.5", "@remix-run/cloudflare-pages": "^2.16.5", "@remix-run/dev": "^2.16.5", "@remix-run/node": "^2.16.5", "@remix-run/react": "^2.16.5", "@remix-run/server-runtime": "^2.16.5", "@types/react": "^18.2.70", "@types/react-dom": "^18.2.25", "@types/yargs": "^17.0.33", "@vercel/react-router": "^1.1.0", "@vitejs/plugin-react": "^4.4.1", "@webstudio-is/css-engine": "workspace:*", "@webstudio-is/http-client": "workspace:*", "@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-remix": "workspace:*", "@webstudio-is/sdk-components-react-router": "workspace:*", "@webstudio-is/tsconfig": "workspace:*", "@webstudio-is/wsauth": "workspace:*", "h3": "^1.15.1", "ipx": "^3.0.3", "isbot": "^5.1.25", "prettier": "3.5.3", "react": "18.3.0-canary-14898b6a9-20240318", "react-dom": "18.3.0-canary-14898b6a9-20240318", "react-router": "^7.5.3", "ts-expect": "^1.3.0", "vike": "^0.4.229", "vite": "^6.3.4", "vitest": "^3.1.2", "wrangler": "^3.63.2" };
390
390
  const packageJson = {
391
391
  name,
392
392
  version,
@@ -892,6 +892,293 @@ new Map(
892
892
  return weightData.names.map((name2) => [name2, weight]);
893
893
  }).flat()
894
894
  );
895
+ var basicLoginErrors = (login) => {
896
+ const issues = [];
897
+ if (login.length === 0) {
898
+ issues.push({ path: ["login"], message: "Login is required" });
899
+ }
900
+ if (login.includes(":")) {
901
+ issues.push({ path: ["login"], message: "Login can't contain a colon" });
902
+ }
903
+ if (/\s/.test(login)) {
904
+ issues.push({
905
+ path: ["login"],
906
+ message: "Login can't contain whitespace"
907
+ });
908
+ }
909
+ return issues;
910
+ };
911
+ var basicPasswordErrors = (password) => {
912
+ const issues = [];
913
+ if (password.length === 0) {
914
+ issues.push({ path: ["password"], message: "Password is required" });
915
+ }
916
+ if (/\s/.test(password)) {
917
+ issues.push({
918
+ path: ["password"],
919
+ message: "Password can't contain whitespace"
920
+ });
921
+ }
922
+ return issues;
923
+ };
924
+ var validateBasicAuth = ({
925
+ login,
926
+ password
927
+ }) => {
928
+ const issues = [...basicLoginErrors(login), ...basicPasswordErrors(password)];
929
+ if (issues.length > 0) {
930
+ const loginErrors = issues.filter((issue) => issue.path[0] === "login").map((issue) => issue.message);
931
+ const passwordErrors = issues.filter((issue) => issue.path[0] === "password").map((issue) => issue.message);
932
+ return {
933
+ issues,
934
+ errors: {
935
+ login: loginErrors.length > 0 ? loginErrors : void 0,
936
+ password: passwordErrors.length > 0 ? passwordErrors : void 0
937
+ }
938
+ };
939
+ }
940
+ return {
941
+ auth: {
942
+ method: "basic",
943
+ login,
944
+ password,
945
+ credentials: `${login}:${password}`
946
+ }
947
+ };
948
+ };
949
+ var createBasicAuthRoute = ({
950
+ route,
951
+ login,
952
+ password
953
+ }) => {
954
+ const routeError = validateWsAuthRoute(route);
955
+ if (routeError) {
956
+ throw new Error(routeError);
957
+ }
958
+ const auth = validateBasicAuth({ login, password }).auth;
959
+ if (auth === void 0) {
960
+ throw new Error(
961
+ 'Basic auth requires non-empty login and password; login cannot contain ":" and neither field can contain whitespace'
962
+ );
963
+ }
964
+ return { route, auth };
965
+ };
966
+ var createWsAuthRouteFromPage = ({
967
+ route,
968
+ auth
969
+ }) => {
970
+ if (auth === void 0) {
971
+ return;
972
+ }
973
+ if ("method" in auth && auth.method !== "basic" || "type" in auth && auth.type !== "basic") {
974
+ throw new Error(`Unsupported auth method for route "${route}"`);
975
+ }
976
+ return createBasicAuthRoute({
977
+ route,
978
+ login: auth.login,
979
+ password: auth.password
980
+ });
981
+ };
982
+ var isRecord = (value) => {
983
+ return typeof value === "object" && value !== null && Array.isArray(value) === false;
984
+ };
985
+ var parameterSegment = /^:\w+[?*]?$/;
986
+ var validateWsAuthRoute = (route) => {
987
+ if (route.startsWith("/") === false) {
988
+ return 'Route must start with "/"';
989
+ }
990
+ if (route === "/") {
991
+ return;
992
+ }
993
+ if (route !== "/" && route.endsWith("/")) {
994
+ return 'Route must not end with "/"';
995
+ }
996
+ if (route.includes("//")) {
997
+ return 'Route must not contain repeating "/"';
998
+ }
999
+ const segments = route.slice(1).split("/");
1000
+ for (let index = 0; index < segments.length; index += 1) {
1001
+ const segment = segments[index];
1002
+ if (segment === void 0 || segment === "") {
1003
+ return "Route contains an empty segment";
1004
+ }
1005
+ if (segment === "*" || /^:\w+\*$/.test(segment)) {
1006
+ if (index !== segments.length - 1) {
1007
+ return "Wildcard route segment must be the last segment";
1008
+ }
1009
+ continue;
1010
+ }
1011
+ if (segment.startsWith(":") && parameterSegment.test(segment) === false) {
1012
+ return `Invalid route parameter "${segment}"`;
1013
+ }
1014
+ if (segment.includes("*")) {
1015
+ return "Wildcard can only be used as * or :name*";
1016
+ }
1017
+ }
1018
+ };
1019
+ var parseJson = (content, errors) => {
1020
+ if (content.trim() === "") {
1021
+ return { version: 1, routes: {} };
1022
+ }
1023
+ try {
1024
+ return JSON.parse(content);
1025
+ } catch (error) {
1026
+ errors.push({
1027
+ path: "$",
1028
+ message: error instanceof Error ? error.message : "Invalid JSON"
1029
+ });
1030
+ }
1031
+ };
1032
+ var parseWsAuth = (content) => {
1033
+ const routes = [];
1034
+ const errors = [];
1035
+ const json = parseJson(content, errors);
1036
+ if (json === void 0) {
1037
+ return { routes, errors };
1038
+ }
1039
+ if (isRecord(json) === false) {
1040
+ errors.push({ path: "$", message: "Auth config must be an object" });
1041
+ return { routes, errors };
1042
+ }
1043
+ if (json.version !== 1) {
1044
+ errors.push({ path: "version", message: "Version must be 1" });
1045
+ }
1046
+ if (isRecord(json.routes) === false) {
1047
+ errors.push({ path: "routes", message: "Routes must be an object" });
1048
+ return { routes, errors };
1049
+ }
1050
+ for (const [route, authInput] of Object.entries(json.routes)) {
1051
+ const routeError = validateWsAuthRoute(route);
1052
+ if (routeError) {
1053
+ errors.push({
1054
+ path: `routes.${JSON.stringify(route)}`,
1055
+ message: routeError
1056
+ });
1057
+ continue;
1058
+ }
1059
+ if (isRecord(authInput) === false) {
1060
+ errors.push({
1061
+ path: `routes.${JSON.stringify(route)}`,
1062
+ message: "Auth rule must be an object"
1063
+ });
1064
+ continue;
1065
+ }
1066
+ if (authInput.method !== "basic") {
1067
+ errors.push({
1068
+ path: `routes.${JSON.stringify(route)}.method`,
1069
+ message: 'Auth method must be "basic"'
1070
+ });
1071
+ continue;
1072
+ }
1073
+ const login = authInput.login;
1074
+ const password = authInput.password;
1075
+ if (typeof login !== "string") {
1076
+ errors.push({
1077
+ path: `routes.${JSON.stringify(route)}.login`,
1078
+ message: "Login must be a string"
1079
+ });
1080
+ continue;
1081
+ }
1082
+ if (typeof password !== "string") {
1083
+ errors.push({
1084
+ path: `routes.${JSON.stringify(route)}.password`,
1085
+ message: "Password must be a string"
1086
+ });
1087
+ continue;
1088
+ }
1089
+ const validation = validateBasicAuth({ login, password });
1090
+ if (validation.auth === void 0) {
1091
+ for (const issue of validation.issues ?? []) {
1092
+ errors.push({
1093
+ path: `routes.${JSON.stringify(route)}.${issue.path[0]}`,
1094
+ message: issue.message
1095
+ });
1096
+ }
1097
+ continue;
1098
+ }
1099
+ const auth = validation.auth;
1100
+ routes.push({ route, auth });
1101
+ }
1102
+ return { routes, errors };
1103
+ };
1104
+ var parseWsAuthOrThrow = (content, sourceName) => {
1105
+ const result = parseWsAuth(content);
1106
+ if (result.errors.length > 0) {
1107
+ const message = result.errors.map((error) => `${sourceName}:${error.path} ${error.message}`).join("\n");
1108
+ throw new Error(message);
1109
+ }
1110
+ return result.routes;
1111
+ };
1112
+ var serializeWsAuth = (routes) => {
1113
+ const config = { version: 1, routes: {} };
1114
+ for (const { route, auth } of routes) {
1115
+ switch (auth.method) {
1116
+ case "basic":
1117
+ config.routes[route] = {
1118
+ method: "basic",
1119
+ login: auth.login,
1120
+ password: auth.password
1121
+ };
1122
+ break;
1123
+ }
1124
+ }
1125
+ return `${JSON.stringify(config, null, 2)}
1126
+ `;
1127
+ };
1128
+ var collectWsAuthRoutes = (sources) => {
1129
+ return sources.flatMap((source) => {
1130
+ if ("content" in source) {
1131
+ return parseWsAuthOrThrow(source.content, source.name);
1132
+ }
1133
+ return source.routes;
1134
+ });
1135
+ };
1136
+ var buildWsAuth = (sources) => {
1137
+ const content = serializeWsAuth(collectWsAuthRoutes(sources));
1138
+ const routes = parseWsAuthOrThrow(content, "Serialized auth config");
1139
+ return {
1140
+ routes,
1141
+ content
1142
+ };
1143
+ };
1144
+ var createWsAuthResources = ({
1145
+ projectContent = "",
1146
+ pages,
1147
+ projectSourceName = "Auth",
1148
+ generatedSourceName = "Generated page auth"
1149
+ }) => {
1150
+ const generatedRoutes = pages.flatMap((page) => {
1151
+ const route = createWsAuthRouteFromPage(page);
1152
+ return route === void 0 ? [] : [route];
1153
+ });
1154
+ const result = buildWsAuth([
1155
+ { name: projectSourceName, content: projectContent },
1156
+ { name: generatedSourceName, routes: generatedRoutes }
1157
+ ]);
1158
+ console.info("[wsauth] create resources", {
1159
+ projectContentLength: projectContent.length,
1160
+ projectContent,
1161
+ pageCount: pages.length,
1162
+ pages,
1163
+ generatedRouteCount: generatedRoutes.length,
1164
+ generatedRoutes,
1165
+ routeCount: result.routes.length,
1166
+ routes: result.routes
1167
+ });
1168
+ return {
1169
+ ...result,
1170
+ module: [
1171
+ `import type { WsAuthRoute } from "@webstudio-is/wsauth";`,
1172
+ "",
1173
+ `export const authRoutes: WsAuthRoute[] = ${JSON.stringify(
1174
+ result.routes,
1175
+ null,
1176
+ 2
1177
+ )};`,
1178
+ ""
1179
+ ].join("\n")
1180
+ };
1181
+ };
895
1182
  function dot3(a2, b2) {
896
1183
  return a2[0] * b2[0] + a2[1] * b2[1] + a2[2] * b2[2];
897
1184
  }
@@ -2687,8 +2974,19 @@ const oklch = new ColorSpace({
2687
2974
  }
2688
2975
  }
2689
2976
  });
2977
+ var isKeyword = (value, keyword) => {
2978
+ if (value.type === "keyword") {
2979
+ return value.value === keyword;
2980
+ }
2981
+ if (value.type === "layers") {
2982
+ return value.value.some((layer) => isKeyword(layer, keyword));
2983
+ }
2984
+ return false;
2985
+ };
2690
2986
  var prefixStyles = (styleMap) => {
2691
2987
  const newStyleMap = /* @__PURE__ */ new Map();
2988
+ const backgroundClip = styleMap.get("background-clip");
2989
+ const hasTextBackgroundClip = backgroundClip !== void 0 && isKeyword(backgroundClip, "text");
2692
2990
  for (const [property, value] of styleMap) {
2693
2991
  if (property === "background-clip") {
2694
2992
  newStyleMap.set("-webkit-background-clip", value);
@@ -2702,6 +3000,9 @@ var prefixStyles = (styleMap) => {
2702
3000
  if (property === "backdrop-filter") {
2703
3001
  newStyleMap.set("-webkit-backdrop-filter", value);
2704
3002
  }
3003
+ if (property === "color" && hasTextBackgroundClip && isKeyword(value, "transparent")) {
3004
+ newStyleMap.set("-webkit-text-fill-color", value);
3005
+ }
2705
3006
  if (property === "view-timeline-name" || property === "scroll-timeline-name" || property === "view-timeline-inset") {
2706
3007
  newStyleMap.set(`--${property}`, value);
2707
3008
  }
@@ -3807,7 +4108,36 @@ var PageTitle = z.string().refine(
3807
4108
  (val) => val.length >= MIN_TITLE_LENGTH,
3808
4109
  `Minimum ${MIN_TITLE_LENGTH} characters required`
3809
4110
  );
3810
- var documentTypes = ["html", "xml"];
4111
+ var documentTypes = ["html", "xml", "text"];
4112
+ var BasicAuthFields = {
4113
+ login: z.string(),
4114
+ password: z.string()
4115
+ };
4116
+ var validateBasicAuthFields = ({
4117
+ login,
4118
+ password
4119
+ }, context) => {
4120
+ for (const issue of validateBasicAuth({ login, password }).issues ?? []) {
4121
+ context.addIssue({
4122
+ code: z.ZodIssueCode.custom,
4123
+ path: issue.path,
4124
+ message: issue.message
4125
+ });
4126
+ }
4127
+ };
4128
+ var PageBasicAuth = z.object({
4129
+ method: z.literal("basic"),
4130
+ ...BasicAuthFields
4131
+ }).superRefine(validateBasicAuthFields);
4132
+ var LegacyPageBasicAuth = z.object({
4133
+ type: z.literal("basic"),
4134
+ ...BasicAuthFields
4135
+ }).superRefine(validateBasicAuthFields).transform(({ login, password }) => ({
4136
+ method: "basic",
4137
+ login,
4138
+ password
4139
+ }));
4140
+ var PageAuth = z.union([PageBasicAuth, LegacyPageBasicAuth]);
3811
4141
  var commonPageFields = {
3812
4142
  id: PageId,
3813
4143
  name: PageName,
@@ -3825,6 +4155,8 @@ var commonPageFields = {
3825
4155
  status: z.string().optional(),
3826
4156
  redirect: z.string().optional(),
3827
4157
  documentType: z.optional(z.enum(documentTypes)),
4158
+ content: z.string().optional(),
4159
+ auth: PageAuth.optional(),
3828
4160
  custom: z.array(
3829
4161
  z.object({
3830
4162
  property: z.string(),
@@ -3880,7 +4212,8 @@ var ProjectMeta = z.object({
3880
4212
  siteName: z.string().optional(),
3881
4213
  contactEmail: z.string().optional(),
3882
4214
  faviconAssetId: z.string().optional(),
3883
- code: z.string().optional()
4215
+ code: z.string().optional(),
4216
+ auth: z.string().optional()
3884
4217
  });
3885
4218
  var ProjectNewRedirectPath = z.string().min(1, "Path is required").refine((data) => {
3886
4219
  try {
@@ -4697,6 +5030,7 @@ var componentCategories = [
4697
5030
  "localization",
4698
5031
  "radix",
4699
5032
  "xml",
5033
+ "text",
4700
5034
  "other",
4701
5035
  "hidden",
4702
5036
  "internal"
@@ -5624,6 +5958,31 @@ var systemParameter = {
5624
5958
  type: "parameter",
5625
5959
  name: "system"
5626
5960
  };
5961
+ var stringMethodReturnKindByName = /* @__PURE__ */ new Map([
5962
+ ["toLowerCase", "string"],
5963
+ ["replace", "string"],
5964
+ ["split", "array"],
5965
+ ["slice", "string"],
5966
+ ["at", "unknown"],
5967
+ ["endsWith", "boolean"],
5968
+ ["includes", "boolean"],
5969
+ ["startsWith", "boolean"],
5970
+ ["toString", "string"],
5971
+ ["toUpperCase", "string"],
5972
+ ["toLocaleLowerCase", "string"],
5973
+ ["toLocaleUpperCase", "string"]
5974
+ ]);
5975
+ var arrayMethodReturnKindByName = /* @__PURE__ */ new Map([
5976
+ ["at", "unknown"],
5977
+ ["includes", "boolean"],
5978
+ ["join", "string"],
5979
+ ["slice", "array"],
5980
+ ["toString", "string"]
5981
+ ]);
5982
+ new Set(
5983
+ stringMethodReturnKindByName.keys()
5984
+ );
5985
+ new Set(arrayMethodReturnKindByName.keys());
5627
5986
  var transpileExpression = ({
5628
5987
  expression,
5629
5988
  executable = false,
@@ -6063,6 +6422,12 @@ var generatePageMeta = ({
6063
6422
  usedDataSources,
6064
6423
  scope: localScope
6065
6424
  });
6425
+ const contentExpression = generateExpression({
6426
+ expression: page.meta.content ?? "undefined",
6427
+ dataSources,
6428
+ usedDataSources,
6429
+ scope: localScope
6430
+ });
6066
6431
  let customExpression = "";
6067
6432
  customExpression += `[
6068
6433
  `;
@@ -6071,7 +6436,7 @@ var generatePageMeta = ({
6071
6436
  continue;
6072
6437
  }
6073
6438
  const propertyExpression = JSON.stringify(customMeta.property);
6074
- const contentExpression = generateExpression({
6439
+ const contentExpression2 = generateExpression({
6075
6440
  expression: customMeta.content,
6076
6441
  dataSources,
6077
6442
  usedDataSources,
@@ -6081,7 +6446,7 @@ var generatePageMeta = ({
6081
6446
  `;
6082
6447
  customExpression += ` property: ${propertyExpression},
6083
6448
  `;
6084
- customExpression += ` content: ${contentExpression},
6449
+ customExpression += ` content: ${contentExpression2},
6085
6450
  `;
6086
6451
  customExpression += ` },
6087
6452
  `;
@@ -6146,6 +6511,8 @@ var generatePageMeta = ({
6146
6511
  generated += ` status: ${statusExpression},
6147
6512
  `;
6148
6513
  generated += ` redirect: ${redirectExpression},
6514
+ `;
6515
+ generated += ` content: ${contentExpression},
6149
6516
  `;
6150
6517
  generated += ` custom: ${customExpression},
6151
6518
  `;
@@ -10547,6 +10914,10 @@ const createFramework$2 = async () => {
10547
10914
  join(routeTemplatesDir, "xml.tsx"),
10548
10915
  "utf8"
10549
10916
  );
10917
+ const textTemplate = await readFile(
10918
+ join(routeTemplatesDir, "text.tsx"),
10919
+ "utf8"
10920
+ );
10550
10921
  const defaultSitemapTemplate = await readFile(
10551
10922
  join(routeTemplatesDir, "default-sitemap.tsx"),
10552
10923
  "utf8"
@@ -10600,6 +10971,12 @@ const createFramework$2 = async () => {
10600
10971
  template: xmlTemplate
10601
10972
  }
10602
10973
  ],
10974
+ text: ({ pagePath }) => [
10975
+ {
10976
+ file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`),
10977
+ template: textTemplate
10978
+ }
10979
+ ],
10603
10980
  redirect: ({ pagePath }) => [
10604
10981
  {
10605
10982
  file: join("app", "routes", `${generateRemixRoute(pagePath)}.ts`),
@@ -10628,6 +11005,10 @@ const createFramework$1 = async () => {
10628
11005
  join(routeTemplatesDir, "xml.tsx"),
10629
11006
  "utf8"
10630
11007
  );
11008
+ const textTemplate = await readFile(
11009
+ join(routeTemplatesDir, "text.tsx"),
11010
+ "utf8"
11011
+ );
10631
11012
  const defaultSitemapTemplate = await readFile(
10632
11013
  join(routeTemplatesDir, "default-sitemap.tsx"),
10633
11014
  "utf8"
@@ -10681,6 +11062,12 @@ const createFramework$1 = async () => {
10681
11062
  template: xmlTemplate
10682
11063
  }
10683
11064
  ],
11065
+ text: ({ pagePath }) => [
11066
+ {
11067
+ file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`),
11068
+ template: textTemplate
11069
+ }
11070
+ ],
10684
11071
  redirect: ({ pagePath }) => [
10685
11072
  {
10686
11073
  file: join("app", "routes", `${generateRemixRoute(pagePath)}.ts`),
@@ -10765,11 +11152,13 @@ const createFramework = async () => {
10765
11152
  ];
10766
11153
  },
10767
11154
  xml: () => [],
11155
+ text: () => [],
10768
11156
  redirect: () => [],
10769
11157
  defaultSitemap: () => []
10770
11158
  };
10771
11159
  };
10772
11160
  const limit = pLimit(10);
11161
+ const wsAuthFile = ".webstudio/auth.json";
10773
11162
  const downloadAsset = async (url, name2, assetBaseUrl) => {
10774
11163
  const assetPath = join("public", assetBaseUrl, name2);
10775
11164
  const tempAssetPath = `${assetPath}.tmp`;
@@ -10813,6 +11202,40 @@ const mergeJsonInto = async (sourcePath, destinationPath) => {
10813
11202
  );
10814
11203
  await writeFile(destinationPath, content, "utf8");
10815
11204
  };
11205
+ const writeWsAuthResources = async (generatedDir, pages) => {
11206
+ var _a2, _b2, _c2, _d2;
11207
+ console.info("[wsauth] prebuild create auth config", {
11208
+ file: wsAuthFile,
11209
+ projectAuthContentLength: ((_b2 = (_a2 = pages.meta) == null ? void 0 : _a2.auth) == null ? void 0 : _b2.length) ?? 0,
11210
+ projectAuthContent: (_c2 = pages.meta) == null ? void 0 : _c2.auth,
11211
+ pages: getAllPages(pages).map((page) => ({
11212
+ id: page.id,
11213
+ name: page.name,
11214
+ route: getPagePath(page.id, pages),
11215
+ auth: page.meta.auth
11216
+ }))
11217
+ });
11218
+ const { content, module } = createWsAuthResources({
11219
+ projectContent: (_d2 = pages.meta) == null ? void 0 : _d2.auth,
11220
+ pages: getAllPages(pages).map((page) => ({
11221
+ route: getPagePath(page.id, pages),
11222
+ auth: page.meta.auth
11223
+ }))
11224
+ });
11225
+ console.info("[wsauth] prebuild write auth config", {
11226
+ file: wsAuthFile,
11227
+ contentLength: content.length,
11228
+ content,
11229
+ generatedModulePath: join(generatedDir, "$resources.wsauth.server.ts"),
11230
+ generatedModule: module
11231
+ });
11232
+ await createFolderIfNotExists(dirname(wsAuthFile));
11233
+ await writeFile(wsAuthFile, content);
11234
+ await createFileIfNotExists(
11235
+ join(generatedDir, "$resources.wsauth.server.ts"),
11236
+ module
11237
+ );
11238
+ };
10816
11239
  const isCliTemplate = async (template) => {
10817
11240
  const currentPath = fileURLToPath(new URL(import.meta.url));
10818
11241
  const templatesPath = normalize(
@@ -10913,6 +11336,7 @@ Please check webstudio --help for more details`
10913
11336
  );
10914
11337
  const pages = migratePages(siteData.build.pages);
10915
11338
  const allPages = getAllPages(pages);
11339
+ await writeWsAuthResources(generatedDir, pages);
10916
11340
  const siteDataByPage = {};
10917
11341
  const fontAssetsByPage = {};
10918
11342
  const backgroundImageAssetsByPage = {};
@@ -11149,6 +11573,8 @@ Please check webstudio --help for more details`
11149
11573
 
11150
11574
  export const projectId = "${siteData.build.projectId}";
11151
11575
 
11576
+ export const projectDomain = ${JSON.stringify(siteData.projectDomain)};
11577
+
11152
11578
  export const lastPublished = "${new Date(siteData.build.createdAt).toISOString()}";
11153
11579
 
11154
11580
  export const siteName = ${JSON.stringify(projectMeta == null ? void 0 : projectMeta.siteName)};
@@ -11221,7 +11647,7 @@ Please check webstudio --help for more details`
11221
11647
  await createFileIfNotExists(clientFile, pageExports);
11222
11648
  const serverFile = join(generatedDir, `${generatedBasename}.server.tsx`);
11223
11649
  await createFileIfNotExists(serverFile, serverExports);
11224
- const getTemplates = documentType === "html" ? framework.html : framework.xml;
11650
+ const getTemplates = framework[documentType];
11225
11651
  for (const { file, template } of getTemplates({ pagePath })) {
11226
11652
  const content = template.replaceAll("__CONSTANTS__", importFrom("./app/constants.mjs", file)).replaceAll(
11227
11653
  "__SITEMAP__",
@@ -11229,6 +11655,9 @@ Please check webstudio --help for more details`
11229
11655
  ).replaceAll(
11230
11656
  "__ASSETS__",
11231
11657
  importFrom(`./app/__generated__/$resources.assets`, file)
11658
+ ).replaceAll(
11659
+ "__AUTH__",
11660
+ importFrom(`./app/__generated__/$resources.wsauth.server`, file)
11232
11661
  ).replaceAll(
11233
11662
  "__CLIENT__",
11234
11663
  importFrom(`./app/__generated__/${generatedBasename}`, file)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.266.0",
3
+ "version": "0.268.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -43,8 +43,8 @@
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.266.0",
47
- "@webstudio-is/trpc-interface": "0.266.0"
46
+ "@webstudio-is/trpc-interface": "0.268.0",
47
+ "@webstudio-is/project-migrations": "0.268.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@cloudflare/vite-plugin": "^1.1.0",
@@ -74,16 +74,17 @@
74
74
  "vite": "^6.3.4",
75
75
  "vitest": "^3.1.2",
76
76
  "wrangler": "^3.63.2",
77
- "@webstudio-is/css-engine": "0.266.0",
78
- "@webstudio-is/http-client": "0.266.0",
79
- "@webstudio-is/image": "0.266.0",
80
- "@webstudio-is/sdk": "0.266.0",
81
- "@webstudio-is/sdk-components-animation": "0.266.0",
82
- "@webstudio-is/sdk-components-react-radix": "0.266.0",
83
- "@webstudio-is/sdk-components-react": "0.266.0",
84
- "@webstudio-is/react-sdk": "0.266.0",
85
- "@webstudio-is/sdk-components-react-remix": "0.266.0",
86
- "@webstudio-is/sdk-components-react-router": "0.266.0",
77
+ "@webstudio-is/css-engine": "0.268.0",
78
+ "@webstudio-is/http-client": "0.268.0",
79
+ "@webstudio-is/image": "0.268.0",
80
+ "@webstudio-is/react-sdk": "0.268.0",
81
+ "@webstudio-is/sdk": "0.268.0",
82
+ "@webstudio-is/sdk-components-animation": "0.268.0",
83
+ "@webstudio-is/sdk-components-react-radix": "0.268.0",
84
+ "@webstudio-is/sdk-components-react-remix": "0.268.0",
85
+ "@webstudio-is/sdk-components-react-router": "0.268.0",
86
+ "@webstudio-is/sdk-components-react": "0.268.0",
87
+ "@webstudio-is/wsauth": "0.268.0",
87
88
  "@webstudio-is/tsconfig": "1.0.7"
88
89
  },
89
90
  "scripts": {
@@ -12,6 +12,7 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
+ "@webstudio-is/wsauth": "0.268.0",
15
16
  "@remix-run/cloudflare": "2.16.5",
16
17
  "@remix-run/cloudflare-pages": "2.16.5"
17
18
  },
@@ -1,10 +1,19 @@
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
5
  // @todo think about how to make __generated__ typeable
5
6
  // @ts-ignore
6
7
  import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
7
8
 
9
+ export const headers: HeadersFunction = ({ errorHeaders }) => {
10
+ if (errorHeaders) {
11
+ return errorHeaders;
12
+ }
13
+
14
+ return {};
15
+ };
16
+
8
17
  const Root = () => {
9
18
  // Get language from matches
10
19
  const matches = useMatches();
@@ -17,6 +17,7 @@ import {
17
17
  formIdFieldName,
18
18
  formBotFieldName,
19
19
  } from "@webstudio-is/sdk/runtime";
20
+ import { authenticateRequest } from "@webstudio-is/wsauth";
20
21
  import {
21
22
  ReactSdkContext,
22
23
  PageSettingsMeta,
@@ -25,6 +26,7 @@ import {
25
26
  } from "@webstudio-is/react-sdk/runtime";
26
27
  import {
27
28
  projectId,
29
+ projectDomain,
28
30
  Page,
29
31
  siteName,
30
32
  favIconAsset,
@@ -42,6 +44,25 @@ import * as constants from "__CONSTANTS__";
42
44
  import css from "__CSS__?url";
43
45
  import { sitemap } from "__SITEMAP__";
44
46
  import { assets } from "__ASSETS__";
47
+ import { authRoutes } from "__AUTH__";
48
+
49
+ const authenticateProductionRequest = (request: Request) => {
50
+ const host =
51
+ request.headers.get("x-forwarded-host") ||
52
+ request.headers.get("host") ||
53
+ "";
54
+
55
+ const requestHost = host.split(":")[0];
56
+ if (
57
+ projectDomain !== undefined &&
58
+ (requestHost === projectDomain ||
59
+ requestHost.startsWith(`${projectDomain}.`))
60
+ ) {
61
+ return;
62
+ }
63
+
64
+ return authenticateRequest(request, authRoutes);
65
+ };
45
66
 
46
67
  const customFetch: typeof fetch = (input, init) => {
47
68
  if (typeof input !== "string") {
@@ -83,6 +104,8 @@ const customFetch: typeof fetch = (input, init) => {
83
104
  };
84
105
 
85
106
  export const loader = async (arg: LoaderFunctionArgs) => {
107
+ const authRoute = authenticateProductionRequest(arg.request);
108
+
86
109
  const url = new URL(arg.request.url);
87
110
  const host =
88
111
  arg.request.headers.get("x-forwarded-host") ||
@@ -133,13 +156,18 @@ export const loader = async (arg: LoaderFunctionArgs) => {
133
156
  {
134
157
  status: pageMeta.status,
135
158
  headers: {
136
- "Cache-Control": "public, max-age=600",
159
+ "Cache-Control":
160
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
137
161
  },
138
162
  }
139
163
  );
140
164
  };
141
165
 
142
- export const headers: HeadersFunction = () => {
166
+ export const headers: HeadersFunction = ({ errorHeaders }) => {
167
+ if (errorHeaders) {
168
+ return errorHeaders;
169
+ }
170
+
143
171
  return {
144
172
  "Cache-Control": "public, max-age=0, must-revalidate",
145
173
  };
@@ -220,6 +248,8 @@ export const action = async ({
220
248
  }: ActionFunctionArgs): Promise<
221
249
  { success: true } | { success: false; errors: string[] }
222
250
  > => {
251
+ authenticateProductionRequest(request);
252
+
223
253
  try {
224
254
  const url = new URL(request.url);
225
255
  url.host = getRequestHost(request);
@@ -0,0 +1,107 @@
1
+ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
2
+ import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
3
+ import { authenticateRequest } from "@webstudio-is/wsauth";
4
+ import { projectDomain } from "__CLIENT__";
5
+ import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
6
+ import { sitemap } from "__SITEMAP__";
7
+ import { assets } from "__ASSETS__";
8
+ import { authRoutes } from "__AUTH__";
9
+
10
+ const authenticateProductionRequest = (request: Request) => {
11
+ const host =
12
+ request.headers.get("x-forwarded-host") ||
13
+ request.headers.get("host") ||
14
+ "";
15
+
16
+ const requestHost = host.split(":")[0];
17
+ if (
18
+ projectDomain !== undefined &&
19
+ (requestHost === projectDomain ||
20
+ requestHost.startsWith(`${projectDomain}.`))
21
+ ) {
22
+ return;
23
+ }
24
+
25
+ return authenticateRequest(request, authRoutes);
26
+ };
27
+
28
+ const customFetch: typeof fetch = (input, init) => {
29
+ if (typeof input !== "string") {
30
+ return fetch(input, init);
31
+ }
32
+
33
+ if (isLocalResource(input, "sitemap.xml")) {
34
+ const response = new Response(JSON.stringify(sitemap));
35
+ response.headers.set("content-type", "application/json; charset=utf-8");
36
+ return Promise.resolve(response);
37
+ }
38
+
39
+ if (isLocalResource(input, "current-date")) {
40
+ const now = new Date();
41
+ const startOfDay = new Date(
42
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
43
+ );
44
+ const data = {
45
+ iso: startOfDay.toISOString(),
46
+ year: startOfDay.getUTCFullYear(),
47
+ month: startOfDay.getUTCMonth() + 1,
48
+ day: startOfDay.getUTCDate(),
49
+ timestamp: startOfDay.getTime(),
50
+ };
51
+ const response = new Response(JSON.stringify(data));
52
+ response.headers.set("content-type", "application/json; charset=utf-8");
53
+ return Promise.resolve(response);
54
+ }
55
+
56
+ if (isLocalResource(input, "assets")) {
57
+ const response = new Response(JSON.stringify(assets));
58
+ response.headers.set("content-type", "application/json; charset=utf-8");
59
+ return Promise.resolve(response);
60
+ }
61
+
62
+ return fetch(input, init);
63
+ };
64
+
65
+ export const loader = async (arg: LoaderFunctionArgs) => {
66
+ const authRoute = authenticateProductionRequest(arg.request);
67
+
68
+ const url = new URL(arg.request.url);
69
+ const host =
70
+ arg.request.headers.get("x-forwarded-host") ||
71
+ arg.request.headers.get("host") ||
72
+ "";
73
+ url.host = host;
74
+ url.protocol = "https";
75
+
76
+ const params = getRemixParams(arg.params);
77
+
78
+ const system = {
79
+ params,
80
+ search: Object.fromEntries(url.searchParams),
81
+ origin: url.origin,
82
+ pathname: url.pathname,
83
+ };
84
+
85
+ const resources = await loadResources(
86
+ customFetch,
87
+ getResources({ system }).data
88
+ );
89
+ const pageMeta = getPageMeta({ system, resources });
90
+
91
+ if (pageMeta.redirect) {
92
+ const status =
93
+ pageMeta.status === 301 || pageMeta.status === 302
94
+ ? pageMeta.status
95
+ : 302;
96
+ return redirect(pageMeta.redirect, status);
97
+ }
98
+
99
+ return new Response(pageMeta.content ?? "", {
100
+ status: pageMeta.status,
101
+ headers: {
102
+ "Content-Type": "text/plain; charset=utf-8",
103
+ "Cache-Control":
104
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
105
+ },
106
+ });
107
+ };
@@ -1,15 +1,35 @@
1
1
  import { renderToString } from "react-dom/server";
2
2
  import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
3
3
  import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
4
+ import { authenticateRequest } from "@webstudio-is/wsauth";
4
5
  import {
5
6
  ReactSdkContext,
6
7
  xmlNodeTagSuffix,
7
8
  } from "@webstudio-is/react-sdk/runtime";
8
- import { Page, breakpoints } from "__CLIENT__";
9
+ import { Page, breakpoints, projectDomain } from "__CLIENT__";
9
10
  import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
10
11
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
11
12
  import { sitemap } from "__SITEMAP__";
12
13
  import { assets } from "__ASSETS__";
14
+ import { authRoutes } from "__AUTH__";
15
+
16
+ const authenticateProductionRequest = (request: Request) => {
17
+ const host =
18
+ request.headers.get("x-forwarded-host") ||
19
+ request.headers.get("host") ||
20
+ "";
21
+
22
+ const requestHost = host.split(":")[0];
23
+ if (
24
+ projectDomain !== undefined &&
25
+ (requestHost === projectDomain ||
26
+ requestHost.startsWith(`${projectDomain}.`))
27
+ ) {
28
+ return;
29
+ }
30
+
31
+ return authenticateRequest(request, authRoutes);
32
+ };
13
33
 
14
34
  const customFetch: typeof fetch = (input, init) => {
15
35
  if (typeof input !== "string") {
@@ -51,6 +71,8 @@ const customFetch: typeof fetch = (input, init) => {
51
71
  };
52
72
 
53
73
  export const loader = async (arg: LoaderFunctionArgs) => {
74
+ const authRoute = authenticateProductionRequest(arg.request);
75
+
54
76
  const url = new URL(arg.request.url);
55
77
  const host =
56
78
  arg.request.headers.get("x-forwarded-host") ||
@@ -104,6 +126,10 @@ export const loader = async (arg: LoaderFunctionArgs) => {
104
126
  text = text.replaceAll(xmlNodeTagSuffix, "");
105
127
 
106
128
  return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n${text}`, {
107
- headers: { "Content-Type": "application/xml" },
129
+ headers: {
130
+ "Content-Type": "application/xml",
131
+ "Cache-Control":
132
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
133
+ },
108
134
  });
109
135
  };
@@ -11,13 +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.266.0",
15
- "@webstudio-is/react-sdk": "0.266.0",
16
- "@webstudio-is/sdk": "0.266.0",
17
- "@webstudio-is/sdk-components-react": "0.266.0",
18
- "@webstudio-is/sdk-components-animation": "0.266.0",
19
- "@webstudio-is/sdk-components-react-radix": "0.266.0",
20
- "@webstudio-is/sdk-components-react-remix": "0.266.0",
14
+ "@webstudio-is/image": "0.268.0",
15
+ "@webstudio-is/react-sdk": "0.268.0",
16
+ "@webstudio-is/sdk": "0.268.0",
17
+ "@webstudio-is/sdk-components-react": "0.268.0",
18
+ "@webstudio-is/sdk-components-animation": "0.268.0",
19
+ "@webstudio-is/sdk-components-react-radix": "0.268.0",
20
+ "@webstudio-is/sdk-components-react-remix": "0.268.0",
21
+ "@webstudio-is/wsauth": "0.268.0",
21
22
  "isbot": "^5.1.25",
22
23
  "react": "18.3.0-canary-14898b6a9-20240318",
23
24
  "react-dom": "18.3.0-canary-14898b6a9-20240318"
@@ -6,6 +6,7 @@
6
6
  "@webstudio-is/sdk-components-animation": "workspace:*",
7
7
  "@webstudio-is/sdk-components-react": "workspace:*",
8
8
  "@webstudio-is/sdk-components-react-radix": "workspace:*",
9
- "@webstudio-is/sdk-components-react-remix": "workspace:*"
9
+ "@webstudio-is/sdk-components-react-remix": "workspace:*",
10
+ "@webstudio-is/wsauth": "workspace:*"
10
11
  }
11
12
  }
@@ -1,10 +1,24 @@
1
1
  /* eslint-disable @typescript-eslint/ban-ts-comment */
2
2
 
3
- import { Links, Meta, Outlet, useMatches } from "react-router";
3
+ import {
4
+ type HeadersFunction,
5
+ Links,
6
+ Meta,
7
+ Outlet,
8
+ useMatches,
9
+ } from "react-router";
4
10
  // @todo think about how to make __generated__ typeable
5
11
  // @ts-ignore
6
12
  import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
7
13
 
14
+ export const headers: HeadersFunction = ({ errorHeaders }) => {
15
+ if (errorHeaders) {
16
+ return errorHeaders;
17
+ }
18
+
19
+ return {};
20
+ };
21
+
8
22
  const Root = () => {
9
23
  // Get language from matches
10
24
  const matches = useMatches();
@@ -17,6 +17,7 @@ import {
17
17
  formBotFieldName,
18
18
  cachedFetch,
19
19
  } from "@webstudio-is/sdk/runtime";
20
+ import { authenticateRequest } from "@webstudio-is/wsauth";
20
21
  import {
21
22
  ReactSdkContext,
22
23
  PageSettingsMeta,
@@ -24,6 +25,7 @@ import {
24
25
  } from "@webstudio-is/react-sdk/runtime";
25
26
  import {
26
27
  projectId,
28
+ projectDomain,
27
29
  Page,
28
30
  siteName,
29
31
  favIconAsset,
@@ -41,6 +43,25 @@ import * as constants from "__CONSTANTS__";
41
43
  import css from "__CSS__?url";
42
44
  import { sitemap } from "__SITEMAP__";
43
45
  import { assets } from "__ASSETS__";
46
+ import { authRoutes } from "__AUTH__";
47
+
48
+ const authenticateProductionRequest = (request: Request) => {
49
+ const host =
50
+ request.headers.get("x-forwarded-host") ||
51
+ request.headers.get("host") ||
52
+ "";
53
+
54
+ const requestHost = host.split(":")[0];
55
+ if (
56
+ projectDomain !== undefined &&
57
+ (requestHost === projectDomain ||
58
+ requestHost.startsWith(`${projectDomain}.`))
59
+ ) {
60
+ return;
61
+ }
62
+
63
+ return authenticateRequest(request, authRoutes);
64
+ };
44
65
 
45
66
  const customFetch: typeof fetch = (input, init) => {
46
67
  if (typeof input !== "string") {
@@ -82,6 +103,8 @@ const customFetch: typeof fetch = (input, init) => {
82
103
  };
83
104
 
84
105
  export const loader = async (arg: LoaderFunctionArgs) => {
106
+ const authRoute = authenticateProductionRequest(arg.request);
107
+
85
108
  const url = new URL(arg.request.url);
86
109
  const host =
87
110
  arg.request.headers.get("x-forwarded-host") ||
@@ -132,13 +155,18 @@ export const loader = async (arg: LoaderFunctionArgs) => {
132
155
  {
133
156
  status: pageMeta.status,
134
157
  headers: {
135
- "Cache-Control": "public, max-age=600",
158
+ "Cache-Control":
159
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
136
160
  },
137
161
  }
138
162
  );
139
163
  };
140
164
 
141
- export const headers: HeadersFunction = () => {
165
+ export const headers: HeadersFunction = ({ errorHeaders }) => {
166
+ if (errorHeaders) {
167
+ return errorHeaders;
168
+ }
169
+
142
170
  return {
143
171
  "Cache-Control": "public, max-age=0, must-revalidate",
144
172
  };
@@ -219,6 +247,8 @@ export const action = async ({
219
247
  }: ActionFunctionArgs): Promise<
220
248
  { success: true } | { success: false; errors: string[] }
221
249
  > => {
250
+ authenticateProductionRequest(request);
251
+
222
252
  try {
223
253
  const url = new URL(request.url);
224
254
  url.host = getRequestHost(request);
@@ -0,0 +1,107 @@
1
+ import { type LoaderFunctionArgs, redirect } from "react-router";
2
+ import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
3
+ import { authenticateRequest } from "@webstudio-is/wsauth";
4
+ import { projectDomain } from "__CLIENT__";
5
+ import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
6
+ import { sitemap } from "__SITEMAP__";
7
+ import { assets } from "__ASSETS__";
8
+ import { authRoutes } from "__AUTH__";
9
+
10
+ const authenticateProductionRequest = (request: Request) => {
11
+ const host =
12
+ request.headers.get("x-forwarded-host") ||
13
+ request.headers.get("host") ||
14
+ "";
15
+
16
+ const requestHost = host.split(":")[0];
17
+ if (
18
+ projectDomain !== undefined &&
19
+ (requestHost === projectDomain ||
20
+ requestHost.startsWith(`${projectDomain}.`))
21
+ ) {
22
+ return;
23
+ }
24
+
25
+ return authenticateRequest(request, authRoutes);
26
+ };
27
+
28
+ const customFetch: typeof fetch = (input, init) => {
29
+ if (typeof input !== "string") {
30
+ return fetch(input, init);
31
+ }
32
+
33
+ if (isLocalResource(input, "sitemap.xml")) {
34
+ const response = new Response(JSON.stringify(sitemap));
35
+ response.headers.set("content-type", "application/json; charset=utf-8");
36
+ return Promise.resolve(response);
37
+ }
38
+
39
+ if (isLocalResource(input, "current-date")) {
40
+ const now = new Date();
41
+ const startOfDay = new Date(
42
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
43
+ );
44
+ const data = {
45
+ iso: startOfDay.toISOString(),
46
+ year: startOfDay.getUTCFullYear(),
47
+ month: startOfDay.getUTCMonth() + 1,
48
+ day: startOfDay.getUTCDate(),
49
+ timestamp: startOfDay.getTime(),
50
+ };
51
+ const response = new Response(JSON.stringify(data));
52
+ response.headers.set("content-type", "application/json; charset=utf-8");
53
+ return Promise.resolve(response);
54
+ }
55
+
56
+ if (isLocalResource(input, "assets")) {
57
+ const response = new Response(JSON.stringify(assets));
58
+ response.headers.set("content-type", "application/json; charset=utf-8");
59
+ return Promise.resolve(response);
60
+ }
61
+
62
+ return fetch(input, init);
63
+ };
64
+
65
+ export const loader = async (arg: LoaderFunctionArgs) => {
66
+ const authRoute = authenticateProductionRequest(arg.request);
67
+
68
+ const url = new URL(arg.request.url);
69
+ const host =
70
+ arg.request.headers.get("x-forwarded-host") ||
71
+ arg.request.headers.get("host") ||
72
+ "";
73
+ url.host = host;
74
+ url.protocol = "https";
75
+
76
+ const params = getRemixParams(arg.params);
77
+
78
+ const system = {
79
+ params,
80
+ search: Object.fromEntries(url.searchParams),
81
+ origin: url.origin,
82
+ pathname: url.pathname,
83
+ };
84
+
85
+ const resources = await loadResources(
86
+ customFetch,
87
+ getResources({ system }).data
88
+ );
89
+ const pageMeta = getPageMeta({ system, resources });
90
+
91
+ if (pageMeta.redirect) {
92
+ const status =
93
+ pageMeta.status === 301 || pageMeta.status === 302
94
+ ? pageMeta.status
95
+ : 302;
96
+ return redirect(pageMeta.redirect, status);
97
+ }
98
+
99
+ return new Response(pageMeta.content ?? "", {
100
+ status: pageMeta.status,
101
+ headers: {
102
+ "Content-Type": "text/plain; charset=utf-8",
103
+ "Cache-Control":
104
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
105
+ },
106
+ });
107
+ };
@@ -1,15 +1,35 @@
1
1
  import { renderToString } from "react-dom/server";
2
2
  import { type LoaderFunctionArgs, redirect } from "react-router";
3
3
  import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
4
+ import { authenticateRequest } from "@webstudio-is/wsauth";
4
5
  import {
5
6
  ReactSdkContext,
6
7
  xmlNodeTagSuffix,
7
8
  } from "@webstudio-is/react-sdk/runtime";
8
- import { Page, breakpoints } from "__CLIENT__";
9
+ import { Page, breakpoints, projectDomain } from "__CLIENT__";
9
10
  import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
10
11
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
11
12
  import { sitemap } from "__SITEMAP__";
12
13
  import { assets } from "__ASSETS__";
14
+ import { authRoutes } from "__AUTH__";
15
+
16
+ const authenticateProductionRequest = (request: Request) => {
17
+ const host =
18
+ request.headers.get("x-forwarded-host") ||
19
+ request.headers.get("host") ||
20
+ "";
21
+
22
+ const requestHost = host.split(":")[0];
23
+ if (
24
+ projectDomain !== undefined &&
25
+ (requestHost === projectDomain ||
26
+ requestHost.startsWith(`${projectDomain}.`))
27
+ ) {
28
+ return;
29
+ }
30
+
31
+ return authenticateRequest(request, authRoutes);
32
+ };
13
33
 
14
34
  const customFetch: typeof fetch = (input, init) => {
15
35
  if (typeof input !== "string") {
@@ -51,6 +71,8 @@ const customFetch: typeof fetch = (input, init) => {
51
71
  };
52
72
 
53
73
  export const loader = async (arg: LoaderFunctionArgs) => {
74
+ const authRoute = authenticateProductionRequest(arg.request);
75
+
54
76
  const url = new URL(arg.request.url);
55
77
  const host =
56
78
  arg.request.headers.get("x-forwarded-host") ||
@@ -108,6 +130,10 @@ export const loader = async (arg: LoaderFunctionArgs) => {
108
130
  text = text.replaceAll(xmlNodeTagSuffix, "");
109
131
 
110
132
  return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n${text}`, {
111
- headers: { "Content-Type": "application/xml" },
133
+ headers: {
134
+ "Content-Type": "application/xml",
135
+ "Cache-Control":
136
+ authRoute === undefined ? "public, max-age=600" : "private, no-store",
137
+ },
112
138
  });
113
139
  };
@@ -10,13 +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.266.0",
14
- "@webstudio-is/react-sdk": "0.266.0",
15
- "@webstudio-is/sdk": "0.266.0",
16
- "@webstudio-is/sdk-components-animation": "0.266.0",
17
- "@webstudio-is/sdk-components-react-radix": "0.266.0",
18
- "@webstudio-is/sdk-components-react-router": "0.266.0",
19
- "@webstudio-is/sdk-components-react": "0.266.0",
13
+ "@webstudio-is/image": "0.268.0",
14
+ "@webstudio-is/react-sdk": "0.268.0",
15
+ "@webstudio-is/sdk": "0.268.0",
16
+ "@webstudio-is/sdk-components-animation": "0.268.0",
17
+ "@webstudio-is/sdk-components-react-radix": "0.268.0",
18
+ "@webstudio-is/sdk-components-react-router": "0.268.0",
19
+ "@webstudio-is/sdk-components-react": "0.268.0",
20
+ "@webstudio-is/wsauth": "0.268.0",
20
21
  "isbot": "^5.1.25",
21
22
  "react": "18.3.0-canary-14898b6a9-20240318",
22
23
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
@@ -7,6 +7,7 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@cloudflare/vite-plugin": "^1.1.0",
10
+ "@webstudio-is/wsauth": "0.268.0",
10
11
  "wrangler": "^4.14.1"
11
12
  }
12
13
  }
@@ -8,12 +8,12 @@
8
8
  "typecheck": "tsgo --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@webstudio-is/image": "0.266.0",
12
- "@webstudio-is/react-sdk": "0.266.0",
13
- "@webstudio-is/sdk": "0.266.0",
14
- "@webstudio-is/sdk-components-react": "0.266.0",
15
- "@webstudio-is/sdk-components-animation": "0.266.0",
16
- "@webstudio-is/sdk-components-react-radix": "0.266.0",
11
+ "@webstudio-is/image": "0.268.0",
12
+ "@webstudio-is/react-sdk": "0.268.0",
13
+ "@webstudio-is/sdk": "0.268.0",
14
+ "@webstudio-is/sdk-components-react": "0.268.0",
15
+ "@webstudio-is/sdk-components-animation": "0.268.0",
16
+ "@webstudio-is/sdk-components-react-radix": "0.268.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"