webstudio 0.238.0 → 0.252.2

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/README.md CHANGED
@@ -16,10 +16,10 @@ To install Node.js using NVM, first install NVM by running:
16
16
  curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
17
17
  ```
18
18
 
19
- Once NVM is installed, you can install Node.js version 18 by running:
19
+ Once NVM is installed, you can install Node.js version 22 by running:
20
20
 
21
21
  ```bash
22
- nvm install 18
22
+ nvm install 22
23
23
  ```
24
24
 
25
25
  Verify your Node.js installation by checking its version:
package/lib/cli.js CHANGED
@@ -24,6 +24,7 @@ import { pathToFileURL, fileURLToPath } from "node:url";
24
24
  import pLimit from "p-limit";
25
25
  import merge from "deepmerge";
26
26
  import hash from "@emotion/hash";
27
+ import "warn-once";
27
28
  import { parseExpressionAt } from "acorn";
28
29
  import { simple } from "acorn-walk";
29
30
  import reservedIdentifiers from "reserved-identifiers";
@@ -1437,12 +1438,16 @@ var MediaRule = (_c = class {
1437
1438
  return "";
1438
1439
  }
1439
1440
  let conditionText = "";
1440
- const { minWidth, maxWidth } = this.options;
1441
- if (minWidth !== void 0) {
1442
- conditionText = ` and (min-width: ${minWidth}px)`;
1443
- }
1444
- if (maxWidth !== void 0) {
1445
- conditionText += ` and (max-width: ${maxWidth}px)`;
1441
+ const { minWidth, maxWidth, condition } = this.options;
1442
+ if (condition !== void 0) {
1443
+ conditionText = ` and (${condition})`;
1444
+ } else {
1445
+ if (minWidth !== void 0) {
1446
+ conditionText = ` and (min-width: ${minWidth}px)`;
1447
+ }
1448
+ if (maxWidth !== void 0) {
1449
+ conditionText += ` and (max-width: ${maxWidth}px)`;
1450
+ }
1446
1451
  }
1447
1452
  return `@media ${__privateGet(this, _mediaType)}${conditionText} {
1448
1453
  ${rules.join(
@@ -1492,6 +1497,21 @@ var FontFaceRule = (_d = class {
1492
1497
  }
1493
1498
  }, _cached = new WeakMap(), _options = new WeakMap(), _d);
1494
1499
  var compareMedia = (optionA, optionB) => {
1500
+ if (optionA.condition !== void 0 && optionB.condition !== void 0) {
1501
+ return optionA.condition.localeCompare(optionB.condition);
1502
+ }
1503
+ if (optionA.condition !== void 0) {
1504
+ if (optionB.minWidth === void 0 && optionB.maxWidth === void 0) {
1505
+ return 1;
1506
+ }
1507
+ return -1;
1508
+ }
1509
+ if (optionB.condition !== void 0) {
1510
+ if (optionA.minWidth === void 0 && optionA.maxWidth === void 0) {
1511
+ return -1;
1512
+ }
1513
+ return 1;
1514
+ }
1495
1515
  if (optionA.minWidth === void 0 && optionA.maxWidth === void 0) {
1496
1516
  return -1;
1497
1517
  }
@@ -1660,7 +1680,7 @@ var StyleSheet = (_f = class {
1660
1680
  var StyleSheetRegular = class extends StyleSheet {
1661
1681
  };
1662
1682
  var createRegularStyleSheet = (options) => {
1663
- const element = new StyleElement(options == null ? void 0 : options.name);
1683
+ const element = (options == null ? void 0 : options.element) ?? new StyleElement(options == null ? void 0 : options.name);
1664
1684
  return new StyleSheetRegular(element);
1665
1685
  };
1666
1686
  var generateAtomic = (sheet, options) => {
@@ -1783,7 +1803,13 @@ var ImageAsset = z.object({
1783
1803
  meta: ImageMeta,
1784
1804
  type: z.literal("image")
1785
1805
  });
1786
- var Asset = z.union([FontAsset, ImageAsset]);
1806
+ var FileAsset = z.object({
1807
+ ...baseAsset,
1808
+ format: z.string(),
1809
+ meta: z.object({}),
1810
+ type: z.literal("file")
1811
+ });
1812
+ var Asset = z.union([FontAsset, ImageAsset, FileAsset]);
1787
1813
  z.map(AssetId, Asset);
1788
1814
  var MIN_TITLE_LENGTH = 2;
1789
1815
  var PageId = z.string();
@@ -2303,14 +2329,23 @@ var Breakpoint = z.object({
2303
2329
  id: BreakpointId,
2304
2330
  label: z.string(),
2305
2331
  minWidth: z.number().optional(),
2306
- maxWidth: z.number().optional()
2307
- }).refine(({ minWidth, maxWidth }) => {
2332
+ maxWidth: z.number().optional(),
2333
+ condition: z.string().optional()
2334
+ }).transform((data) => {
2335
+ if (data.condition !== void 0 && data.condition.trim() === "") {
2336
+ return { ...data, condition: void 0 };
2337
+ }
2338
+ return data;
2339
+ }).refine(({ minWidth, maxWidth, condition }) => {
2340
+ if (condition !== void 0) {
2341
+ return minWidth === void 0 && maxWidth === void 0;
2342
+ }
2308
2343
  return (
2309
2344
  // Either min or max width have to be defined
2310
2345
  minWidth !== void 0 && maxWidth === void 0 || minWidth === void 0 && maxWidth !== void 0 || // This is a base breakpoint
2311
2346
  minWidth === void 0 && maxWidth === void 0
2312
2347
  );
2313
- }, "Either minWidth or maxWidth should be defined");
2348
+ }, "Either minWidth, maxWidth, or condition should be defined, but not both");
2314
2349
  z.map(BreakpointId, Breakpoint);
2315
2350
  var StyleSourceId = z.string();
2316
2351
  var StyleSourceToken = z.object({
@@ -2617,6 +2652,170 @@ z.object({
2617
2652
  initialProps: z.array(z.string()).optional(),
2618
2653
  props: z.record(PropMeta).optional()
2619
2654
  });
2655
+ var ALLOWED_FILE_TYPES = {
2656
+ // Documents
2657
+ pdf: "application/pdf",
2658
+ doc: "application/msword",
2659
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2660
+ xls: "application/vnd.ms-excel",
2661
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2662
+ csv: "text/csv",
2663
+ ppt: "application/vnd.ms-powerpoint",
2664
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2665
+ // Code
2666
+ txt: "text/plain",
2667
+ md: "text/markdown",
2668
+ js: "text/javascript",
2669
+ css: "text/css",
2670
+ json: "application/json",
2671
+ html: "text/html",
2672
+ xml: "application/xml",
2673
+ // Archives
2674
+ zip: "application/zip",
2675
+ rar: "application/vnd.rar",
2676
+ // Audio
2677
+ mp3: "audio/mpeg",
2678
+ wav: "audio/wav",
2679
+ ogg: "audio/ogg",
2680
+ m4a: "audio/mp4",
2681
+ // Video
2682
+ mp4: "video/mp4",
2683
+ mov: "video/quicktime",
2684
+ avi: "video/x-msvideo",
2685
+ webm: "video/webm",
2686
+ // Images
2687
+ // Note: Cloudflare Image Resizing supports: jpg, jpeg, png, gif, webp, svg, avif
2688
+ // Other formats (bmp, ico, tif, tiff) are served as-is without optimization
2689
+ jpg: "image/jpeg",
2690
+ jpeg: "image/jpeg",
2691
+ png: "image/png",
2692
+ gif: "image/gif",
2693
+ svg: "image/svg+xml",
2694
+ webp: "image/webp",
2695
+ avif: "image/avif",
2696
+ ico: "image/vnd.microsoft.icon",
2697
+ // Used for favicons
2698
+ bmp: "image/bmp",
2699
+ // Served without optimization
2700
+ tif: "image/tiff",
2701
+ // Served without optimization
2702
+ tiff: "image/tiff",
2703
+ // Served without optimization
2704
+ // Fonts
2705
+ woff: "font/woff",
2706
+ woff2: "font/woff2",
2707
+ ttf: "font/ttf",
2708
+ otf: "font/otf"
2709
+ };
2710
+ new Set(
2711
+ Object.keys(ALLOWED_FILE_TYPES)
2712
+ );
2713
+ var MIME_CATEGORIES = [
2714
+ "image",
2715
+ "video",
2716
+ "audio",
2717
+ "font",
2718
+ "text",
2719
+ "application"
2720
+ ];
2721
+ var FILE_EXTENSIONS_BY_CATEGORY = (() => {
2722
+ const categories = Object.fromEntries(
2723
+ MIME_CATEGORIES.map((category) => [category, []])
2724
+ );
2725
+ Object.entries(ALLOWED_FILE_TYPES).forEach(([ext, mimeType]) => {
2726
+ const [category] = mimeType.split("/");
2727
+ if (category in categories) {
2728
+ categories[category].push(ext);
2729
+ }
2730
+ });
2731
+ return categories;
2732
+ })();
2733
+ var extensionToMime = new Map(
2734
+ Object.entries(ALLOWED_FILE_TYPES).map(([ext, mime]) => [`.${ext}`, mime])
2735
+ );
2736
+ var mimeTypes = new Set(extensionToMime.values());
2737
+ /* @__PURE__ */ new Set([
2738
+ ...mimeTypes.values(),
2739
+ ...MIME_CATEGORIES.map((category) => `${category}/*`)
2740
+ ]);
2741
+ var IMAGE_EXTENSIONS = FILE_EXTENSIONS_BY_CATEGORY.image;
2742
+ IMAGE_EXTENSIONS.map(
2743
+ (ext) => ALLOWED_FILE_TYPES[ext]
2744
+ );
2745
+ var VIDEO_EXTENSIONS = FILE_EXTENSIONS_BY_CATEGORY.video;
2746
+ VIDEO_EXTENSIONS.map(
2747
+ (ext) => ALLOWED_FILE_TYPES[ext]
2748
+ );
2749
+ var FONT_EXTENSIONS = FILE_EXTENSIONS_BY_CATEGORY.font;
2750
+ var detectAssetType = (fileName) => {
2751
+ var _a2;
2752
+ const ext = (_a2 = fileName.split(".").pop()) == null ? void 0 : _a2.toLowerCase();
2753
+ if (!ext) {
2754
+ return "file";
2755
+ }
2756
+ if (IMAGE_EXTENSIONS.includes(ext)) {
2757
+ return "image";
2758
+ }
2759
+ if (FONT_EXTENSIONS.includes(ext)) {
2760
+ return "font";
2761
+ }
2762
+ if (VIDEO_EXTENSIONS.includes(ext)) {
2763
+ return "video";
2764
+ }
2765
+ return "file";
2766
+ };
2767
+ var getAssetUrl = (asset, origin) => {
2768
+ let path;
2769
+ const assetType = detectAssetType(asset.name);
2770
+ if (assetType === "image") {
2771
+ path = `/cgi/image/${asset.name}?format=raw`;
2772
+ } else {
2773
+ path = `/cgi/asset/${asset.name}?format=raw`;
2774
+ }
2775
+ return new URL(path, origin);
2776
+ };
2777
+ var extractImageMetadata = (asset) => {
2778
+ if (asset.type !== "image") {
2779
+ return;
2780
+ }
2781
+ if (asset.meta.width && asset.meta.height) {
2782
+ return {
2783
+ width: asset.meta.width,
2784
+ height: asset.meta.height
2785
+ };
2786
+ }
2787
+ };
2788
+ var extractFontMetadata = (asset) => {
2789
+ if (asset.type !== "font") {
2790
+ return;
2791
+ }
2792
+ const metadata = {
2793
+ family: asset.meta.family
2794
+ };
2795
+ if ("style" in asset.meta) {
2796
+ metadata.style = asset.meta.style;
2797
+ metadata.weight = asset.meta.weight;
2798
+ }
2799
+ return metadata;
2800
+ };
2801
+ var extractFileMetadata = (_asset) => {
2802
+ return;
2803
+ };
2804
+ var metadataExtractors = {
2805
+ image: extractImageMetadata,
2806
+ font: extractFontMetadata,
2807
+ file: extractFileMetadata
2808
+ };
2809
+ var toRuntimeAsset = (asset, origin) => {
2810
+ const extractor = metadataExtractors[asset.type];
2811
+ const metadata = extractor(asset);
2812
+ const url = getAssetUrl(asset, origin);
2813
+ const relativeUrl = url.pathname + url.search;
2814
+ return {
2815
+ url: relativeUrl,
2816
+ ...metadata
2817
+ };
2818
+ };
2620
2819
  var normalize_css_exports = {};
2621
2820
  __export(normalize_css_exports, {
2622
2821
  a: () => a$7,
@@ -3171,6 +3370,16 @@ var collectionMeta = {
3171
3370
  required: true,
3172
3371
  control: "json",
3173
3372
  type: "json"
3373
+ },
3374
+ item: {
3375
+ required: false,
3376
+ control: "text",
3377
+ type: "string"
3378
+ },
3379
+ itemKey: {
3380
+ required: false,
3381
+ control: "text",
3382
+ type: "string"
3174
3383
  }
3175
3384
  }
3176
3385
  };
@@ -4199,6 +4408,18 @@ var isAttributeNameSafe = (attributeName) => {
4199
4408
  illegalAttributeNameCache.set(attributeName, true);
4200
4409
  return false;
4201
4410
  };
4411
+ var generateCollectionIterationCode = ({
4412
+ dataExpression,
4413
+ keyVariable,
4414
+ itemVariable
4415
+ }) => {
4416
+ return `Object.entries(
4417
+ // @ts-ignore
4418
+ ${dataExpression} ?? {}
4419
+ ).map(([_key, ${itemVariable}]: any) => {
4420
+ const ${keyVariable} = Array.isArray(${dataExpression}) ? Number(_key) : _key;
4421
+ return`;
4422
+ };
4202
4423
  var standardAttributesToReactProps = {
4203
4424
  "accept-charset": "acceptCharset",
4204
4425
  accesskey: "accessKey",
@@ -4427,6 +4648,7 @@ ${tagProperty}=${JSON.stringify(instance.tag)}`;
4427
4648
  let conditionValue;
4428
4649
  let collectionDataValue;
4429
4650
  let collectionItemValue;
4651
+ let collectionItemKeyValue;
4430
4652
  let classNameValue;
4431
4653
  for (const prop of props.values()) {
4432
4654
  if (prop.instanceId !== instance.id) {
@@ -4462,6 +4684,9 @@ ${tagProperty}=${JSON.stringify(instance.tag)}`;
4462
4684
  if (prop.name === "item") {
4463
4685
  collectionItemValue = propValue;
4464
4686
  }
4687
+ if (prop.name === "itemKey") {
4688
+ collectionItemKeyValue = propValue;
4689
+ }
4465
4690
  continue;
4466
4691
  }
4467
4692
  if (name2 === "className" && propValue !== void 0) {
@@ -4493,14 +4718,23 @@ ${name2}={${propValue}}`;
4493
4718
  return "";
4494
4719
  }
4495
4720
  const indexVariable = scope.getName(`${instance.id}-index`, "index");
4496
- generatedElement += `{${collectionDataValue}?.map?.((${collectionItemValue}: any, ${indexVariable}: number) =>
4721
+ const keyVariable = collectionItemKeyValue ?? indexVariable;
4722
+ generatedElement += `{${generateCollectionIterationCode({
4723
+ dataExpression: collectionDataValue,
4724
+ keyVariable,
4725
+ itemVariable: collectionItemValue
4726
+ })} (
4497
4727
  `;
4498
- generatedElement += `<Fragment key={${indexVariable}}>
4728
+ generatedElement += `<Fragment key={${keyVariable}}>
4499
4729
  `;
4500
4730
  generatedElement += children;
4501
4731
  generatedElement += `</Fragment>
4502
4732
  `;
4503
- generatedElement += `)}
4733
+ generatedElement += `)
4734
+ `;
4735
+ generatedElement += `})
4736
+ `;
4737
+ generatedElement += `}
4504
4738
  `;
4505
4739
  } else if (instance.component === blockComponent) {
4506
4740
  generatedElement += children;
@@ -5437,7 +5671,8 @@ const o$v = {
5437
5671
  type: "string",
5438
5672
  control: "file",
5439
5673
  label: "Source",
5440
- required: false
5674
+ required: false,
5675
+ accept: "image/*"
5441
5676
  }
5442
5677
  }
5443
5678
  };
@@ -8358,29 +8593,19 @@ Please check webstudio --help for more details`
8358
8593
  const assetsToDownload = [];
8359
8594
  if (options.assets === true) {
8360
8595
  const assetOrigin = siteData.origin;
8596
+ if (!assetOrigin) {
8597
+ console.warn("Warning: Asset origin is not defined in project data.");
8598
+ }
8361
8599
  for (const asset of siteData.assets) {
8362
- if (asset.type === "image") {
8363
- assetsToDownload.push(
8364
- limit(
8365
- () => downloadAsset(
8366
- `${assetOrigin}/cgi/image/${asset.name}?format=raw`,
8367
- asset.name,
8368
- assetBaseUrl
8369
- )
8370
- )
8371
- );
8372
- }
8373
- if (asset.type === "font") {
8374
- assetsToDownload.push(
8375
- limit(
8376
- () => downloadAsset(
8377
- `${assetOrigin}/cgi/asset/${asset.name}`,
8378
- asset.name,
8379
- assetBaseUrl
8380
- )
8600
+ assetsToDownload.push(
8601
+ limit(
8602
+ () => downloadAsset(
8603
+ getAssetUrl(asset, assetOrigin || "").href,
8604
+ asset.name,
8605
+ assetBaseUrl
8381
8606
  )
8382
- );
8383
- }
8607
+ )
8608
+ );
8384
8609
  }
8385
8610
  }
8386
8611
  const assets = new Map(siteData.assets.map((asset) => [asset.id, asset]));
@@ -8590,6 +8815,9 @@ Please check webstudio --help for more details`
8590
8815
  const content = template.replaceAll("__CONSTANTS__", importFrom("./app/constants.mjs", file)).replaceAll(
8591
8816
  "__SITEMAP__",
8592
8817
  importFrom(`./app/__generated__/$resources.sitemap.xml`, file)
8818
+ ).replaceAll(
8819
+ "__ASSETS__",
8820
+ importFrom(`./app/__generated__/$resources.assets`, file)
8593
8821
  ).replaceAll(
8594
8822
  "__CLIENT__",
8595
8823
  importFrom(`./app/__generated__/${generatedBasename}`, file)
@@ -8620,6 +8848,18 @@ Please check webstudio --help for more details`
8620
8848
  )};
8621
8849
  `
8622
8850
  );
8851
+ const assetsById = Object.fromEntries(
8852
+ siteData.assets.map((asset) => [
8853
+ asset.id,
8854
+ toRuntimeAsset(asset, "https://placeholder.local")
8855
+ ])
8856
+ );
8857
+ await createFileIfNotExists(
8858
+ join(generatedDir, "$resources.assets.ts"),
8859
+ `
8860
+ export const assets = ${JSON.stringify(assetsById, null, 2)};
8861
+ `
8862
+ );
8623
8863
  const redirects = (_k = siteData.build.pages) == null ? void 0 : _k.redirects;
8624
8864
  if (redirects !== void 0 && redirects.length > 0) {
8625
8865
  for (const redirect of redirects) {
@@ -8795,7 +9035,7 @@ const getDeploymentInstructions = (deployTarget) => {
8795
9035
  }
8796
9036
  };
8797
9037
  const name = "webstudio";
8798
- const version = "0.238.0";
9038
+ const version = "0.252.2";
8799
9039
  const description = "Webstudio CLI";
8800
9040
  const author = "Webstudio <github@webstudio.is>";
8801
9041
  const homepage = "https://webstudio.is";
@@ -8803,10 +9043,10 @@ const type = "module";
8803
9043
  const bin = { "webstudio-cli": "./bin.js", "webstudio": "./bin.js" };
8804
9044
  const imports = { "#cli": { "webstudio": "./src/cli.ts", "default": "./lib/cli.js" } };
8805
9045
  const files = ["lib/*", "templates/*", "bin.js", "!*.{test,stories}.*"];
8806
- const scripts = { "typecheck": "tsc", "build": "rm -rf lib && vite build", "test": "vitest run" };
9046
+ const scripts = { "typecheck": "tsgo --noEmit", "build": "rm -rf lib && vite build", "test": "vitest run" };
8807
9047
  const license = "AGPL-3.0-or-later";
8808
- const engines = { "node": ">=20.12" };
8809
- const dependencies = { "@clack/prompts": "^0.10.0", "@emotion/hash": "^0.9.2", "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", "yargs": "^17.7.2", "zod": "^3.24.2" };
9048
+ const engines = { "node": ">=22" };
9049
+ const dependencies = { "@clack/prompts": "^0.10.0", "@emotion/hash": "^0.9.2", "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" };
8810
9050
  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" };
8811
9051
  const packageJson = {
8812
9052
  name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.238.0",
3
+ "version": "0.252.2",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "license": "AGPL-3.0-or-later",
25
25
  "engines": {
26
- "node": ">=20.12"
26
+ "node": ">=22"
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^0.10.0",
@@ -39,6 +39,7 @@
39
39
  "picocolors": "^1.1.1",
40
40
  "reserved-identifiers": "^1.0.0",
41
41
  "tinyexec": "^0.3.2",
42
+ "warn-once": "^0.1.1",
42
43
  "yargs": "^17.7.2",
43
44
  "zod": "^3.24.2"
44
45
  },
@@ -70,20 +71,20 @@
70
71
  "vite": "^6.3.4",
71
72
  "vitest": "^3.1.2",
72
73
  "wrangler": "^3.63.2",
73
- "@webstudio-is/css-engine": "0.238.0",
74
- "@webstudio-is/http-client": "0.238.0",
75
- "@webstudio-is/sdk": "0.238.0",
76
- "@webstudio-is/react-sdk": "0.238.0",
77
- "@webstudio-is/image": "0.238.0",
78
- "@webstudio-is/sdk-components-animation": "0.238.0",
79
- "@webstudio-is/sdk-components-react": "0.238.0",
80
- "@webstudio-is/sdk-components-react-radix": "0.238.0",
81
- "@webstudio-is/sdk-components-react-router": "0.238.0",
82
- "@webstudio-is/sdk-components-react-remix": "0.238.0",
74
+ "@webstudio-is/css-engine": "0.252.2",
75
+ "@webstudio-is/http-client": "0.252.2",
76
+ "@webstudio-is/image": "0.252.2",
77
+ "@webstudio-is/sdk": "0.252.2",
78
+ "@webstudio-is/react-sdk": "0.252.2",
79
+ "@webstudio-is/sdk-components-animation": "0.252.2",
80
+ "@webstudio-is/sdk-components-react-radix": "0.252.2",
81
+ "@webstudio-is/sdk-components-react-remix": "0.252.2",
82
+ "@webstudio-is/sdk-components-react": "0.252.2",
83
+ "@webstudio-is/sdk-components-react-router": "0.252.2",
83
84
  "@webstudio-is/tsconfig": "1.0.7"
84
85
  },
85
86
  "scripts": {
86
- "typecheck": "tsc",
87
+ "typecheck": "tsgo --noEmit",
87
88
  "build": "rm -rf lib && vite build",
88
89
  "test": "vitest run"
89
90
  }
@@ -41,6 +41,7 @@ import {
41
41
  import * as constants from "__CONSTANTS__";
42
42
  import css from "__CSS__?url";
43
43
  import { sitemap } from "__SITEMAP__";
44
+ import { assets } from "__ASSETS__";
44
45
 
45
46
  const customFetch: typeof fetch = (input, init) => {
46
47
  if (typeof input !== "string") {
@@ -72,6 +73,12 @@ const customFetch: typeof fetch = (input, init) => {
72
73
  return Promise.resolve(response);
73
74
  }
74
75
 
76
+ if (isLocalResource(input, "assets")) {
77
+ const response = new Response(JSON.stringify(assets));
78
+ response.headers.set("content-type", "application/json; charset=utf-8");
79
+ return Promise.resolve(response);
80
+ }
81
+
75
82
  return cachedFetch(projectId, input, init);
76
83
  };
77
84
 
@@ -238,16 +245,21 @@ export const action = async ({
238
245
  throw new Error("Form bot field not found");
239
246
  }
240
247
 
241
- const submitTime = parseInt(formBotValue, 16);
242
- // Assumes that the difference between the server time and the form submission time,
243
- // including any client-server time drift, is within a 5-minute range.
244
- // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
245
- // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
246
- if (
247
- Number.isNaN(submitTime) ||
248
- Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
249
- ) {
250
- throw new Error(`Form bot value invalid ${formBotValue}`);
248
+ // Skip timestamp validation for Brave browser
249
+ // Brave Shields blocks matchMedia fingerprinting detection used in bot protection
250
+ // See: https://github.com/brave/brave-browser/issues/46541
251
+ if (formBotValue !== "brave") {
252
+ const submitTime = parseInt(formBotValue, 16);
253
+ // Assumes that the difference between the server time and the form submission time,
254
+ // including any client-server time drift, is within a 5-minute range.
255
+ // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
256
+ // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
257
+ if (
258
+ Number.isNaN(submitTime) ||
259
+ Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
260
+ ) {
261
+ throw new Error(`Form bot value invalid ${formBotValue}`);
262
+ }
251
263
  }
252
264
 
253
265
  formData.delete(formIdFieldName);
@@ -9,6 +9,7 @@ import { Page, breakpoints } from "__CLIENT__";
9
9
  import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
10
10
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
11
11
  import { sitemap } from "__SITEMAP__";
12
+ import { assets } from "__ASSETS__";
12
13
 
13
14
  const customFetch: typeof fetch = (input, init) => {
14
15
  if (typeof input !== "string") {
@@ -40,6 +41,12 @@ const customFetch: typeof fetch = (input, init) => {
40
41
  return Promise.resolve(response);
41
42
  }
42
43
 
44
+ if (isLocalResource(input, "assets")) {
45
+ const response = new Response(JSON.stringify(assets));
46
+ response.headers.set("content-type", "application/json; charset=utf-8");
47
+ return Promise.resolve(response);
48
+ }
49
+
43
50
  return fetch(input, init);
44
51
  };
45
52
 
@@ -5,19 +5,19 @@
5
5
  "scripts": {
6
6
  "build": "remix vite:build",
7
7
  "dev": "remix vite:dev",
8
- "typecheck": "tsc"
8
+ "typecheck": "tsgo --noEmit"
9
9
  },
10
10
  "dependencies": {
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.238.0",
15
- "@webstudio-is/react-sdk": "0.238.0",
16
- "@webstudio-is/sdk": "0.238.0",
17
- "@webstudio-is/sdk-components-react": "0.238.0",
18
- "@webstudio-is/sdk-components-animation": "0.238.0",
19
- "@webstudio-is/sdk-components-react-radix": "0.238.0",
20
- "@webstudio-is/sdk-components-react-remix": "0.238.0",
14
+ "@webstudio-is/image": "0.252.2",
15
+ "@webstudio-is/react-sdk": "0.252.2",
16
+ "@webstudio-is/sdk": "0.252.2",
17
+ "@webstudio-is/sdk-components-react": "0.252.2",
18
+ "@webstudio-is/sdk-components-animation": "0.252.2",
19
+ "@webstudio-is/sdk-components-react-radix": "0.252.2",
20
+ "@webstudio-is/sdk-components-react-remix": "0.252.2",
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"
@@ -30,6 +30,6 @@
30
30
  "vite": "^6.3.4"
31
31
  },
32
32
  "engines": {
33
- "node": ">=20.0.0"
33
+ "node": ">=22"
34
34
  }
35
35
  }
@@ -40,6 +40,7 @@ import {
40
40
  import * as constants from "__CONSTANTS__";
41
41
  import css from "__CSS__?url";
42
42
  import { sitemap } from "__SITEMAP__";
43
+ import { assets } from "__ASSETS__";
43
44
 
44
45
  const customFetch: typeof fetch = (input, init) => {
45
46
  if (typeof input !== "string") {
@@ -71,6 +72,12 @@ const customFetch: typeof fetch = (input, init) => {
71
72
  return Promise.resolve(response);
72
73
  }
73
74
 
75
+ if (isLocalResource(input, "assets")) {
76
+ const response = new Response(JSON.stringify(assets));
77
+ response.headers.set("content-type", "application/json; charset=utf-8");
78
+ return Promise.resolve(response);
79
+ }
80
+
74
81
  return cachedFetch(projectId, input, init);
75
82
  };
76
83
 
@@ -237,16 +244,21 @@ export const action = async ({
237
244
  throw new Error("Form bot field not found");
238
245
  }
239
246
 
240
- const submitTime = parseInt(formBotValue, 16);
241
- // Assumes that the difference between the server time and the form submission time,
242
- // including any client-server time drift, is within a 5-minute range.
243
- // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
244
- // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
245
- if (
246
- Number.isNaN(submitTime) ||
247
- Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
248
- ) {
249
- throw new Error(`Form bot value invalid ${formBotValue}`);
247
+ // Skip timestamp validation for Brave browser
248
+ // Brave Shields blocks matchMedia fingerprinting detection used in bot protection
249
+ // See: https://github.com/brave/brave-browser/issues/46541
250
+ if (formBotValue !== "brave") {
251
+ const submitTime = parseInt(formBotValue, 16);
252
+ // Assumes that the difference between the server time and the form submission time,
253
+ // including any client-server time drift, is within a 5-minute range.
254
+ // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
255
+ // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
256
+ if (
257
+ Number.isNaN(submitTime) ||
258
+ Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
259
+ ) {
260
+ throw new Error(`Form bot value invalid ${formBotValue}`);
261
+ }
250
262
  }
251
263
 
252
264
  formData.delete(formIdFieldName);
@@ -9,6 +9,7 @@ import { Page, breakpoints } from "__CLIENT__";
9
9
  import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
10
10
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
11
11
  import { sitemap } from "__SITEMAP__";
12
+ import { assets } from "__ASSETS__";
12
13
 
13
14
  const customFetch: typeof fetch = (input, init) => {
14
15
  if (typeof input !== "string") {
@@ -40,6 +41,12 @@ const customFetch: typeof fetch = (input, init) => {
40
41
  return Promise.resolve(response);
41
42
  }
42
43
 
44
+ if (isLocalResource(input, "assets")) {
45
+ const response = new Response(JSON.stringify(assets));
46
+ response.headers.set("content-type", "application/json; charset=utf-8");
47
+ return Promise.resolve(response);
48
+ }
49
+
43
50
  return fetch(input, init);
44
51
  };
45
52
 
@@ -5,18 +5,18 @@
5
5
  "scripts": {
6
6
  "build": "react-router build",
7
7
  "dev": "react-router dev",
8
- "typecheck": "tsc"
8
+ "typecheck": "tsgo --noEmit"
9
9
  },
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.238.0",
14
- "@webstudio-is/react-sdk": "0.238.0",
15
- "@webstudio-is/sdk": "0.238.0",
16
- "@webstudio-is/sdk-components-animation": "0.238.0",
17
- "@webstudio-is/sdk-components-react-radix": "0.238.0",
18
- "@webstudio-is/sdk-components-react-router": "0.238.0",
19
- "@webstudio-is/sdk-components-react": "0.238.0",
13
+ "@webstudio-is/image": "0.252.2",
14
+ "@webstudio-is/react-sdk": "0.252.2",
15
+ "@webstudio-is/sdk": "0.252.2",
16
+ "@webstudio-is/sdk-components-animation": "0.252.2",
17
+ "@webstudio-is/sdk-components-react-radix": "0.252.2",
18
+ "@webstudio-is/sdk-components-react-router": "0.252.2",
19
+ "@webstudio-is/sdk-components-react": "0.252.2",
20
20
  "isbot": "^5.1.25",
21
21
  "react": "18.3.0-canary-14898b6a9-20240318",
22
22
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
@@ -29,6 +29,6 @@
29
29
  "typescript": "5.8.2"
30
30
  },
31
31
  "engines": {
32
- "node": ">=20.0.0"
32
+ "node": ">=22"
33
33
  }
34
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "scripts": {
3
- "typecheck": "wrangler types && tsc",
3
+ "typecheck": "wrangler types && tsgo",
4
4
  "typegen": "wrangler types",
5
5
  "preview": "react-router build && vite preview",
6
6
  "deploy": "react-router build && wrangler deploy"
@@ -8,42 +8,14 @@ import {
8
8
  } from "__CLIENT__";
9
9
  import "__CSS__";
10
10
 
11
- export const Head = ({ data }: { data: PageContext["data"] }) => {
12
- const { pageMeta } = data;
13
- const { origin } = new URL(data.url);
11
+ export const Head = ({}: { data: PageContext["data"] }) => {
14
12
  const ldJson = {
15
13
  "@context": "https://schema.org",
16
14
  "@type": "WebSite",
17
15
  name: siteName,
18
- url: origin,
19
16
  };
20
- let socialImageUrl = pageMeta.socialImageUrl;
21
- if (pageMeta.socialImageAssetName) {
22
- socialImageUrl = `${origin}${imageLoader({
23
- src: `${assetBaseUrl}/${pageMeta.socialImageAssetName}`,
24
- // Do not transform social image (not enough information do we need to do this)
25
- format: "raw",
26
- })}`;
27
- }
28
- const isTwitterCardSizeDefined = pageMeta.custom.some(
29
- (meta) => meta.property === "twitter:card"
30
- );
31
17
  return (
32
18
  <>
33
- {data.url && <meta property="og:url" content={data.url} />}
34
- <title>{pageMeta.title}</title>
35
- <meta property="og:title" content={pageMeta.title} />
36
- {pageMeta.description && (
37
- <>
38
- <meta name="description" content={pageMeta.description} />
39
- <meta property="og:description" content={pageMeta.description} />
40
- </>
41
- )}
42
- <meta property="og:type" content="website" />
43
- {siteName && <meta property="og:site_name" content={siteName} />}
44
- {socialImageUrl && (
45
- <meta property="og:image" content={pageMeta.socialImageUrl} />
46
- )}
47
19
  {siteName && (
48
20
  <script
49
21
  type="application/ld+json"
@@ -52,18 +24,6 @@ export const Head = ({ data }: { data: PageContext["data"] }) => {
52
24
  }}
53
25
  ></script>
54
26
  )}
55
- {pageMeta.excludePageFromSearch && (
56
- <meta name="robots" content="noindex, nofollow" />
57
- )}
58
- {pageMeta.custom.map(({ property, content }) => (
59
- <meta key={property} property={property} content={content} />
60
- ))}
61
- {(pageMeta.socialImageAssetName !== undefined ||
62
- pageMeta.socialImageUrl !== undefined) &&
63
- isTwitterCardSizeDefined === false && (
64
- <meta property="twitter:card" content="summary_large_image" />
65
- )}
66
-
67
27
  {favIconAsset && (
68
28
  <link
69
29
  rel="icon"
@@ -1,10 +1,14 @@
1
1
  import type { PageContext } from "vike/types";
2
- import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime";
2
+ import {
3
+ PageSettingsMeta,
4
+ PageSettingsTitle,
5
+ ReactSdkContext,
6
+ } from "@webstudio-is/react-sdk/runtime";
3
7
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
4
- import { Page, breakpoints } from "__CLIENT__";
8
+ import { Page, breakpoints, siteName } from "__CLIENT__";
5
9
 
6
10
  const PageComponent = ({ data }: { data: PageContext["data"] }) => {
7
- const { system, resources, url } = data;
11
+ const { system, resources, url, pageMeta } = data;
8
12
  return (
9
13
  <ReactSdkContext.Provider
10
14
  value={{
@@ -17,6 +21,14 @@ const PageComponent = ({ data }: { data: PageContext["data"] }) => {
17
21
  >
18
22
  {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
19
23
  <Page key={url} system={system} />
24
+ <PageSettingsMeta
25
+ url={url}
26
+ pageMeta={pageMeta}
27
+ siteName={siteName}
28
+ imageLoader={imageLoader}
29
+ assetBaseUrl={assetBaseUrl}
30
+ />
31
+ <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
20
32
  </ReactSdkContext.Provider>
21
33
  );
22
34
  };
@@ -1,7 +1,7 @@
1
1
  import type { PageContextServer } from "vike/types";
2
- import { redirect } from "vike/abort";
3
2
  import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
4
3
  import { getPageMeta, getResources } from "__SERVER__";
4
+ import { assets } from "__ASSETS__";
5
5
 
6
6
  const customFetch: typeof fetch = (input, init) => {
7
7
  if (typeof input !== "string") {
@@ -26,6 +26,12 @@ const customFetch: typeof fetch = (input, init) => {
26
26
  return Promise.resolve(response);
27
27
  }
28
28
 
29
+ if (isLocalResource(input, "assets")) {
30
+ const response = new Response(JSON.stringify(assets));
31
+ response.headers.set("content-type", "application/json; charset=utf-8");
32
+ return Promise.resolve(response);
33
+ }
34
+
29
35
  return fetch(input, init);
30
36
  };
31
37
 
@@ -50,14 +56,6 @@ export const data = async (pageContext: PageContextServer) => {
50
56
  );
51
57
  const pageMeta = getPageMeta({ system, resources });
52
58
 
53
- if (pageMeta.redirect) {
54
- const status =
55
- pageMeta.status === 301 || pageMeta.status === 302
56
- ? pageMeta.status
57
- : 302;
58
- throw redirect(pageMeta.redirect, status);
59
- }
60
-
61
59
  return {
62
60
  url: url.href,
63
61
  system,
@@ -5,15 +5,15 @@
5
5
  "scripts": {
6
6
  "build": "vite build",
7
7
  "dev": "vite dev",
8
- "typecheck": "tsc"
8
+ "typecheck": "tsgo --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@webstudio-is/image": "0.238.0",
12
- "@webstudio-is/react-sdk": "0.238.0",
13
- "@webstudio-is/sdk": "0.238.0",
14
- "@webstudio-is/sdk-components-react": "0.238.0",
15
- "@webstudio-is/sdk-components-animation": "0.238.0",
16
- "@webstudio-is/sdk-components-react-radix": "0.238.0",
11
+ "@webstudio-is/image": "0.252.2",
12
+ "@webstudio-is/react-sdk": "0.252.2",
13
+ "@webstudio-is/sdk": "0.252.2",
14
+ "@webstudio-is/sdk-components-react": "0.252.2",
15
+ "@webstudio-is/sdk-components-animation": "0.252.2",
16
+ "@webstudio-is/sdk-components-react-radix": "0.252.2",
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"
@@ -26,6 +26,6 @@
26
26
  "vite": "^6.3.4"
27
27
  },
28
28
  "engines": {
29
- "node": ">=20.0.0"
29
+ "node": ">=22"
30
30
  }
31
31
  }
@@ -1,9 +1,5 @@
1
1
  import { type Root, createRoot } from "react-dom/client";
2
2
  import type { OnRenderClientSync } from "vike/types";
3
- // @todo think about how to make __generated__ typeable
4
- /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
5
- // @ts-ignore
6
- import { CustomCode } from "../app/__generated__/_index";
7
3
 
8
4
  let root: Root;
9
5
 
@@ -17,7 +13,7 @@ export const onRenderClient: OnRenderClientSync = (pageContext) => {
17
13
  <meta charSet="UTF-8" />
18
14
  <meta name="viewport" content="width=device-width,initial-scale=1" />
19
15
  <Head data={pageContext.data} />
20
- <CustomCode />
16
+ {/* avoid hydrating custom code on client, it will duplicate all scripts */}
21
17
  </head>
22
18
  <Page data={pageContext.data} />
23
19
  </>