mirror of
https://github.com/webstudio-is/webstudio.git
synced 2025-03-15 09:45:09 +00:00
experimental: Animation publishing (#4976)
## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
.github/workflows
.gitignoreeslint.config.mjsfixtures
webstudio-cloudflare-template
webstudio-features
packages
cli
css-engine
sdk-components-animation
patches
pnpm-lock.yamlvite.sdk-components.config.ts
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -135,7 +135,7 @@ jobs:
|
||||
const results = [
|
||||
await assertSize('./fixtures/ssg/dist/client', 352),
|
||||
await assertSize('./fixtures/react-router-netlify/build/client', 368),
|
||||
await assertSize('./fixtures/webstudio-features/build/client', 936),
|
||||
await assertSize('./fixtures/webstudio-features/build/client', 1024),
|
||||
]
|
||||
for (const result of results) {
|
||||
if (result.passed) {
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -62,4 +62,5 @@ dist
|
||||
# should be here otherwise if placed inside prisma-client pnpm deploy doesn't copy it
|
||||
packages/prisma-client/src/__generated__
|
||||
|
||||
.temp
|
||||
.temp
|
||||
*.timestamp-*.mjs
|
||||
|
@ -15,6 +15,8 @@ export default tseslint.config({
|
||||
"packages/prisma-client/prisma/migrations/**",
|
||||
"packages/cli/templates/**",
|
||||
"fixtures/**",
|
||||
"packages/sdk-components-animation/private-src/polyfill/**",
|
||||
"packages/sdk-components-animation/private-src/perf/**",
|
||||
],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
|
@ -53,6 +53,7 @@
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "^5.4.11",
|
||||
"wrangler": "^3.63.2"
|
||||
"wrangler": "^3.63.2",
|
||||
"fast-glob": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,34 @@ import {
|
||||
} from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
// @ts-ignore
|
||||
import path from "node:path";
|
||||
// @ts-ignore
|
||||
import fg from "fast-glob";
|
||||
|
||||
const rootDir = ["..", "../..", "../../.."]
|
||||
.map((dir) => path.join(__dirname, dir))
|
||||
.find((dir) => existsSync(path.join(dir, ".git")));
|
||||
|
||||
const hasPrivateFolders =
|
||||
fg.sync([path.join(rootDir ?? "", "packages/*/private-src/*")], {
|
||||
ignore: ["**/node_modules/**"],
|
||||
}).length > 0;
|
||||
|
||||
const conditions = hasPrivateFolders
|
||||
? ["webstudio-private", "webstudio"]
|
||||
: ["webstudio"];
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
resolve: {
|
||||
conditions,
|
||||
},
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)
|
||||
mode === "production" ? undefined : remixCloudflareDevProxy(),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,26 +1,26 @@
|
||||
export const sitemap = [
|
||||
{
|
||||
path: "/",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
{
|
||||
path: "/_route_with_symbols_",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
{
|
||||
path: "/form",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
{
|
||||
path: "/heading-with-id",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
{
|
||||
path: "/resources",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
{
|
||||
path: "/nested/nested-page",
|
||||
lastModified: "2025-03-04",
|
||||
lastModified: "2025-03-10",
|
||||
},
|
||||
];
|
||||
|
39
fixtures/webstudio-features/app/__generated__/[animations]._index.server.tsx
generated
Normal file
39
fixtures/webstudio-features/app/__generated__/[animations]._index.server.tsx
generated
Normal file
@ -0,0 +1,39 @@
|
||||
/* eslint-disable */
|
||||
/* This is a auto generated file for building the project */
|
||||
|
||||
import type { PageMeta } from "@webstudio-is/sdk";
|
||||
import type { System, ResourceRequest } from "@webstudio-is/sdk";
|
||||
export const getResources = (_props: { system: System }) => {
|
||||
const _data = new Map<string, ResourceRequest>([]);
|
||||
const _action = new Map<string, ResourceRequest>([]);
|
||||
return { data: _data, action: _action };
|
||||
};
|
||||
|
||||
export const getPageMeta = ({
|
||||
system,
|
||||
resources,
|
||||
}: {
|
||||
system: System;
|
||||
resources: Record<string, any>;
|
||||
}): PageMeta => {
|
||||
return {
|
||||
title: "Untitled",
|
||||
description: "",
|
||||
excludePageFromSearch: true,
|
||||
language: "",
|
||||
socialImageAssetName: undefined,
|
||||
socialImageUrl: "",
|
||||
status: 200,
|
||||
redirect: "",
|
||||
custom: [],
|
||||
};
|
||||
};
|
||||
|
||||
type Params = Record<string, string | undefined>;
|
||||
export const getRemixParams = ({ ...params }: Params): Params => {
|
||||
return params;
|
||||
};
|
||||
|
||||
export const projectId = "cddc1d44-af37-4cb6-a430-d300cf6f932d";
|
||||
|
||||
export const contactEmail = "hello@webstudio.is";
|
294
fixtures/webstudio-features/app/__generated__/[animations]._index.tsx
generated
Normal file
294
fixtures/webstudio-features/app/__generated__/[animations]._index.tsx
generated
Normal file
@ -0,0 +1,294 @@
|
||||
/* eslint-disable */
|
||||
/* This is a auto generated file for building the project */
|
||||
|
||||
import { Fragment, useState } from "react";
|
||||
import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
|
||||
import { Body as Body } from "@webstudio-is/sdk-components-react-router";
|
||||
import {
|
||||
Box as Box,
|
||||
Heading as Heading,
|
||||
} from "@webstudio-is/sdk-components-react";
|
||||
import {
|
||||
AnimateChildren as AnimateChildren,
|
||||
AnimateText as AnimateText,
|
||||
} from "@webstudio-is/sdk-components-animation";
|
||||
|
||||
export const siteName = "KittyGuardedZone";
|
||||
|
||||
export const favIconAsset: string | undefined =
|
||||
"DALL_E_2023-10-30_12.39.46_-_Photo_logo_with_a_bold_cat_silhouette_centered_on_a_contrasting_background_designed_for_clarity_at_small_32x32_favicon_resolution_00h6cEA8u2pJRvVJv7hRe.png";
|
||||
|
||||
// Font assets on current page (can be preloaded)
|
||||
export const pageFontAssets: string[] = [];
|
||||
|
||||
export const pageBackgroundImageAssets: string[] = [];
|
||||
|
||||
const Page = (_props: { system: any }) => {
|
||||
return (
|
||||
<Body className={`w-body c1jgcte3 cncutr5 c1ywt30e cqvurle`}>
|
||||
<Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>
|
||||
<Heading className={`w-heading`}>{"ANIMATIONS"}</Heading>
|
||||
</Box>
|
||||
<Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc c16asro7 c1pt69cw`}>
|
||||
<Box className={`w-box`}>
|
||||
<AnimateChildren
|
||||
action={{
|
||||
type: "view",
|
||||
animations: [
|
||||
{
|
||||
name: "Fade In",
|
||||
description: "Fade in the element as it scrolls into view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 0,
|
||||
styles: {
|
||||
opacity: { type: "unit", unit: "number", value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "backwards",
|
||||
rangeStart: [
|
||||
"entry",
|
||||
{ type: "unit", value: 0, unit: "%" },
|
||||
],
|
||||
rangeEnd: [
|
||||
"entry",
|
||||
{ type: "unit", value: 100, unit: "%" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Fade Out",
|
||||
description:
|
||||
"Fade out the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 1,
|
||||
styles: {
|
||||
opacity: { type: "unit", unit: "number", value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "forwards",
|
||||
rangeStart: ["exit", { type: "unit", value: 0, unit: "%" }],
|
||||
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parallax In",
|
||||
description:
|
||||
"Parallax the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 0,
|
||||
styles: {
|
||||
translate: {
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "unit", unit: "number", value: 0 },
|
||||
{ type: "unit", unit: "px", value: 100 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "backwards",
|
||||
rangeStart: [
|
||||
"cover",
|
||||
{ type: "unit", value: 0, unit: "%" },
|
||||
],
|
||||
rangeEnd: ["cover", { type: "unit", value: 50, unit: "%" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parallax Out",
|
||||
description:
|
||||
"Parallax the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 1,
|
||||
styles: {
|
||||
translate: {
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "unit", unit: "number", value: 0 },
|
||||
{ type: "unit", unit: "px", value: -100 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "forwards",
|
||||
rangeStart: [
|
||||
"cover",
|
||||
{ type: "unit", value: 50, unit: "%" },
|
||||
],
|
||||
rangeEnd: [
|
||||
"cover",
|
||||
{ type: "unit", value: 100, unit: "%" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
isPinned: false,
|
||||
}}
|
||||
>
|
||||
<Heading className={`w-heading c1pdroxx cudat22`}>
|
||||
{"ANIMATED CHILD 0"}
|
||||
</Heading>
|
||||
<Heading className={`w-heading`}>{"ANIMATED CHILD 1"}</Heading>
|
||||
</AnimateChildren>
|
||||
</Box>
|
||||
<Box className={`w-box`}>
|
||||
{
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={`w-box cq3mp4w`} />
|
||||
<Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc`}>
|
||||
<Box className={`w-box`}>
|
||||
<AnimateChildren
|
||||
action={{
|
||||
type: "view",
|
||||
animations: [
|
||||
{
|
||||
name: "Fade In",
|
||||
description: "Fade in the element as it scrolls into view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 0,
|
||||
styles: {
|
||||
opacity: { type: "unit", unit: "number", value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "backwards",
|
||||
rangeStart: [
|
||||
"entry",
|
||||
{ type: "unit", value: 0, unit: "%" },
|
||||
],
|
||||
rangeEnd: [
|
||||
"entry",
|
||||
{ type: "unit", value: 100, unit: "%" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Fade Out",
|
||||
description:
|
||||
"Fade out the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 1,
|
||||
styles: {
|
||||
opacity: { type: "unit", unit: "number", value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "forwards",
|
||||
rangeStart: ["exit", { type: "unit", value: 0, unit: "%" }],
|
||||
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parallax In",
|
||||
description:
|
||||
"Parallax the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 0,
|
||||
styles: {
|
||||
translate: {
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "unit", unit: "number", value: 0 },
|
||||
{ type: "unit", unit: "px", value: 100 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "backwards",
|
||||
rangeStart: [
|
||||
"cover",
|
||||
{ type: "unit", value: 0, unit: "%" },
|
||||
],
|
||||
rangeEnd: ["cover", { type: "unit", value: 50, unit: "%" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parallax Out",
|
||||
description:
|
||||
"Parallax the element as it scrolls out of view.",
|
||||
keyframes: [
|
||||
{
|
||||
offset: 1,
|
||||
styles: {
|
||||
translate: {
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "unit", unit: "number", value: 0 },
|
||||
{ type: "unit", unit: "px", value: -100 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "linear",
|
||||
fill: "forwards",
|
||||
rangeStart: [
|
||||
"cover",
|
||||
{ type: "unit", value: 50, unit: "%" },
|
||||
],
|
||||
rangeEnd: [
|
||||
"cover",
|
||||
{ type: "unit", value: 100, unit: "%" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
isPinned: false,
|
||||
}}
|
||||
>
|
||||
<AnimateText charWindow={20}>
|
||||
<Heading className={`w-heading c1pdroxx`}>
|
||||
{"ANIMATED CHILD 1"}
|
||||
</Heading>
|
||||
<Box className={`w-box`}>
|
||||
{
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
|
||||
}
|
||||
</Box>
|
||||
</AnimateText>
|
||||
</AnimateChildren>
|
||||
</Box>
|
||||
<Box className={`w-box`}>
|
||||
{
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>
|
||||
<Heading className={`w-heading`}>{"THE END"}</Heading>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
};
|
||||
|
||||
export { Page };
|
@ -411,4 +411,45 @@
|
||||
.c1a2hnxl {
|
||||
background-color: rgba(2, 139, 250, 1);
|
||||
}
|
||||
.c1m04i8w {
|
||||
height: 100dvh;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 472px) {
|
||||
.c1jgcte3 {
|
||||
max-width: 900px;
|
||||
}
|
||||
.cncutr5 {
|
||||
width: 100%;
|
||||
}
|
||||
.c1ywt30e {
|
||||
min-width: 0px;
|
||||
}
|
||||
.cqvurle {
|
||||
justify-self: center;
|
||||
}
|
||||
.c1y8ynw5 {
|
||||
display: grid;
|
||||
}
|
||||
.c2gdfrb {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
.c1yk3skc {
|
||||
grid-auto-columns: 1fr;
|
||||
}
|
||||
.c16asro7 {
|
||||
align-items: start;
|
||||
}
|
||||
.c1pt69cw {
|
||||
align-content: start;
|
||||
}
|
||||
.cq3mp4w {
|
||||
height: 40dvh;
|
||||
}
|
||||
.c1pdroxx {
|
||||
margin-top: 0em;
|
||||
}
|
||||
.cudat22 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
}
|
||||
|
295
fixtures/webstudio-features/app/routes/[animations]._index.tsx
Normal file
295
fixtures/webstudio-features/app/routes/[animations]._index.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
import {
|
||||
type MetaFunction,
|
||||
type LinksFunction,
|
||||
type LinkDescriptor,
|
||||
type ActionFunctionArgs,
|
||||
type LoaderFunctionArgs,
|
||||
type HeadersFunction,
|
||||
data,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from "react-router";
|
||||
import {
|
||||
isLocalResource,
|
||||
loadResource,
|
||||
loadResources,
|
||||
formIdFieldName,
|
||||
formBotFieldName,
|
||||
} from "@webstudio-is/sdk/runtime";
|
||||
import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
siteName,
|
||||
favIconAsset,
|
||||
pageFontAssets,
|
||||
pageBackgroundImageAssets,
|
||||
} from "../__generated__/[animations]._index";
|
||||
import {
|
||||
getResources,
|
||||
getPageMeta,
|
||||
getRemixParams,
|
||||
projectId,
|
||||
contactEmail,
|
||||
} from "../__generated__/[animations]._index.server";
|
||||
import { assetBaseUrl, imageLoader } from "../constants.mjs";
|
||||
import css from "../__generated__/index.css?url";
|
||||
import { sitemap } from "../__generated__/$resources.sitemap.xml";
|
||||
|
||||
const customFetch: typeof fetch = (input, init) => {
|
||||
if (typeof input !== "string") {
|
||||
return fetch(input, init);
|
||||
}
|
||||
|
||||
if (isLocalResource(input, "sitemap.xml")) {
|
||||
// @todo: dynamic import sitemap ???
|
||||
const response = new Response(JSON.stringify(sitemap));
|
||||
response.headers.set("content-type", "application/json; charset=utf-8");
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
return fetch(input, init);
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
const url = new URL(arg.request.url);
|
||||
const host =
|
||||
arg.request.headers.get("x-forwarded-host") ||
|
||||
arg.request.headers.get("host") ||
|
||||
"";
|
||||
url.host = host;
|
||||
url.protocol = "https";
|
||||
|
||||
const params = getRemixParams(arg.params);
|
||||
const system = {
|
||||
params,
|
||||
search: Object.fromEntries(url.searchParams),
|
||||
origin: url.origin,
|
||||
};
|
||||
|
||||
const resources = await loadResources(
|
||||
customFetch,
|
||||
getResources({ system }).data
|
||||
);
|
||||
const pageMeta = getPageMeta({ system, resources });
|
||||
|
||||
if (pageMeta.redirect) {
|
||||
const status =
|
||||
pageMeta.status === 301 || pageMeta.status === 302
|
||||
? pageMeta.status
|
||||
: 302;
|
||||
throw redirect(pageMeta.redirect, status);
|
||||
}
|
||||
|
||||
// typecheck
|
||||
arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;
|
||||
|
||||
if (arg.context.EXCLUDE_FROM_SEARCH) {
|
||||
pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;
|
||||
}
|
||||
|
||||
return data(
|
||||
{
|
||||
host,
|
||||
url: url.href,
|
||||
system,
|
||||
resources,
|
||||
pageMeta,
|
||||
},
|
||||
// No way for current information to change, so add cache for 10 minutes
|
||||
// In case of CRM Data, this should be set to 0
|
||||
{
|
||||
status: pageMeta.status,
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=600",
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const headers: HeadersFunction = () => {
|
||||
return {
|
||||
"Cache-Control": "public, max-age=0, must-revalidate",
|
||||
};
|
||||
};
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
const metas: ReturnType<MetaFunction> = [];
|
||||
if (data === undefined) {
|
||||
return metas;
|
||||
}
|
||||
|
||||
const origin = `https://${data.host}`;
|
||||
|
||||
if (siteName) {
|
||||
metas.push({
|
||||
"script:ld+json": {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: siteName,
|
||||
url: origin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return metas;
|
||||
};
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
const result: LinkDescriptor[] = [];
|
||||
|
||||
result.push({
|
||||
rel: "stylesheet",
|
||||
href: css,
|
||||
});
|
||||
|
||||
if (favIconAsset) {
|
||||
result.push({
|
||||
rel: "icon",
|
||||
href: imageLoader({
|
||||
src: `${assetBaseUrl}${favIconAsset}`,
|
||||
// width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search
|
||||
width: 144,
|
||||
height: 144,
|
||||
fit: "pad",
|
||||
quality: 100,
|
||||
format: "auto",
|
||||
}),
|
||||
type: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
for (const asset of pageFontAssets) {
|
||||
result.push({
|
||||
rel: "preload",
|
||||
href: `${assetBaseUrl}${asset}`,
|
||||
as: "font",
|
||||
crossOrigin: "anonymous",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backgroundImageAsset of pageBackgroundImageAssets) {
|
||||
result.push({
|
||||
rel: "preload",
|
||||
href: `${assetBaseUrl}${backgroundImageAsset}`,
|
||||
as: "image",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getRequestHost = (request: Request): string =>
|
||||
request.headers.get("x-forwarded-host") || request.headers.get("host") || "";
|
||||
|
||||
export const action = async ({
|
||||
request,
|
||||
context,
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] }
|
||||
> => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url.host = getRequestHost(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const system = {
|
||||
params: {},
|
||||
search: {},
|
||||
origin: url.origin,
|
||||
};
|
||||
|
||||
const resourceName = formData.get(formIdFieldName);
|
||||
let resource =
|
||||
typeof resourceName === "string"
|
||||
? getResources({ system }).action.get(resourceName)
|
||||
: undefined;
|
||||
|
||||
const formBotValue = formData.get(formBotFieldName);
|
||||
|
||||
if (formBotValue == null || typeof formBotValue !== "string") {
|
||||
throw new Error("Form bot field not found");
|
||||
}
|
||||
|
||||
const submitTime = parseInt(formBotValue, 16);
|
||||
// Assumes that the difference between the server time and the form submission time,
|
||||
// including any client-server time drift, is within a 5-minute range.
|
||||
// Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
|
||||
// Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
|
||||
if (
|
||||
Number.isNaN(submitTime) ||
|
||||
Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
|
||||
) {
|
||||
throw new Error(`Form bot value invalid ${formBotValue}`);
|
||||
}
|
||||
|
||||
formData.delete(formIdFieldName);
|
||||
formData.delete(formBotFieldName);
|
||||
|
||||
if (resource) {
|
||||
resource.headers.push({
|
||||
name: "Content-Type",
|
||||
value: "application/json",
|
||||
});
|
||||
resource.body = Object.fromEntries(formData);
|
||||
} else {
|
||||
if (contactEmail === undefined) {
|
||||
throw new Error("Contact email not found");
|
||||
}
|
||||
|
||||
resource = context.getDefaultActionResource?.({
|
||||
url,
|
||||
projectId,
|
||||
contactEmail,
|
||||
formData,
|
||||
});
|
||||
}
|
||||
|
||||
if (resource === undefined) {
|
||||
throw Error("Resource not found");
|
||||
}
|
||||
const { ok, statusText } = await loadResource(fetch, resource);
|
||||
if (ok) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, errors: [statusText] };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors: [error instanceof Error ? error.message : "Unknown error"],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Outlet = () => {
|
||||
const { system, resources, url, pageMeta, host } =
|
||||
useLoaderData<typeof loader>();
|
||||
return (
|
||||
<ReactSdkContext.Provider
|
||||
value={{
|
||||
imageLoader,
|
||||
assetBaseUrl,
|
||||
resources,
|
||||
}}
|
||||
>
|
||||
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
||||
<Page key={url} system={system} />
|
||||
<PageSettingsMeta
|
||||
url={url}
|
||||
pageMeta={pageMeta}
|
||||
host={host}
|
||||
siteName={siteName}
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Outlet;
|
@ -6,7 +6,7 @@
|
||||
"dev": "react-router dev",
|
||||
"cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio",
|
||||
"fixtures:link": "pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'",
|
||||
"fixtures:sync": "pnpm cli sync --buildId 6f14cdae-073c-4f2c-b535-da3f404f3e36 && pnpm prettier --write ./.webstudio/",
|
||||
"fixtures:sync": "pnpm cli sync --buildId 342a7c73-ed72-441d-8a69-8b2659639e93 && pnpm prettier --write ./.webstudio/",
|
||||
"fixtures:build": "pnpm cli build --template react-router --template ./.template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json"
|
||||
},
|
||||
"private": true,
|
||||
|
@ -89,8 +89,7 @@
|
||||
"css-tree@2.3.1": "patches/css-tree@2.3.1.patch",
|
||||
"@types/css-tree@2.3.1": "patches/@types__css-tree@2.3.1.patch",
|
||||
"@radix-ui/react-scroll-area@1.0.5": "patches/@radix-ui__react-scroll-area@1.0.5.patch",
|
||||
"@remix-run/dev": "patches/@remix-run__dev.patch",
|
||||
"scroll-timeline-polyfill@1.1.0": "patches/scroll-timeline-polyfill@1.1.0.patch"
|
||||
"@remix-run/dev": "patches/@remix-run__dev.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { readFile, rm } from "node:fs/promises";
|
||||
import type { WsComponentMeta } from "@webstudio-is/sdk";
|
||||
import { generateRemixRoute, namespaceMeta } from "@webstudio-is/react-sdk";
|
||||
import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
|
||||
import * as animationComponentMetas from "@webstudio-is/sdk-components-animation/metas";
|
||||
import * as reactRouterComponentMetas from "@webstudio-is/sdk-components-react-router/metas";
|
||||
import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas";
|
||||
import type { Framework } from "./framework";
|
||||
@ -40,12 +41,26 @@ export const createFramework = async (): Promise<Framework> => {
|
||||
);
|
||||
}
|
||||
|
||||
const animationComponentNamespacedMetas: Record<string, WsComponentMeta> = {};
|
||||
for (const [name, meta] of Object.entries(animationComponentMetas)) {
|
||||
const namespace = "@webstudio-is/sdk-components-animation";
|
||||
animationComponentNamespacedMetas[`${namespace}:${name}`] = namespaceMeta(
|
||||
meta as WsComponentMeta,
|
||||
namespace,
|
||||
new Set(Object.keys(animationComponentMetas))
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
source: "@webstudio-is/sdk-components-react",
|
||||
metas: baseComponentMetas,
|
||||
},
|
||||
{
|
||||
source: "@webstudio-is/sdk-components-animation",
|
||||
metas: animationComponentNamespacedMetas,
|
||||
},
|
||||
{
|
||||
source: "@webstudio-is/sdk-components-react-radix",
|
||||
metas: radixComponentNamespacedMetas,
|
||||
|
@ -2,5 +2,8 @@
|
||||
"dependencies": {
|
||||
"worktop": "0.8.0-next.18",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fast-glob": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
48
packages/cli/templates/saas-helpers/vite.config.ts
Normal file
48
packages/cli/templates/saas-helpers/vite.config.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
vitePlugin as remix,
|
||||
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
|
||||
} from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
// @ts-ignore
|
||||
import path from "node:path";
|
||||
// @ts-ignore
|
||||
import fg from "fast-glob";
|
||||
|
||||
const rootDir = ["..", "../..", "../../.."]
|
||||
.map((dir) => path.join(__dirname, dir))
|
||||
.find((dir) => existsSync(path.join(dir, ".git")));
|
||||
|
||||
const hasPrivateFolders =
|
||||
fg.sync([path.join(rootDir ?? "", "packages/*/private-src/*")], {
|
||||
ignore: ["**/node_modules/**"],
|
||||
}).length > 0;
|
||||
|
||||
const conditions = hasPrivateFolders
|
||||
? ["webstudio-private", "webstudio"]
|
||||
: ["webstudio"];
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
resolve: {
|
||||
conditions,
|
||||
},
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)
|
||||
mode === "production" ? undefined : remixCloudflareDevProxy(),
|
||||
remix({
|
||||
future: {
|
||||
v3_lazyRouteDiscovery: false,
|
||||
v3_relativeSplatPath: false,
|
||||
v3_singleFetch: false,
|
||||
v3_fetcherPersist: false,
|
||||
v3_throwAbortReason: false,
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
}));
|
@ -31,9 +31,6 @@ const isExternal = (id: string, importer: string | undefined) => {
|
||||
|
||||
export default defineConfig({
|
||||
// resolve only webstudio condition in tests
|
||||
resolve: {
|
||||
conditions: ["webstudio"],
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
lib: {
|
||||
|
@ -7,7 +7,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc",
|
||||
"build": "rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external",
|
||||
"build": "rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external && esbuild src/runtime.ts --outdir=lib --bundle --format=esm --packages=external",
|
||||
"dts": "tsc --project tsconfig.dts.json",
|
||||
"test": "vitest run"
|
||||
},
|
||||
@ -25,9 +25,16 @@
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"exports": {
|
||||
"webstudio": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"import": "./lib/index.js"
|
||||
".": {
|
||||
"webstudio": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"import": "./lib/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"webstudio": "./src/runtime.ts",
|
||||
"types": "./lib/types/runtime.d.ts",
|
||||
"import": "./lib/runtime.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"lib/*",
|
||||
|
1
packages/css-engine/src/runtime.ts
Normal file
1
packages/css-engine/src/runtime.ts
Normal file
@ -0,0 +1 @@
|
||||
export { toValue } from "./core/to-value";
|
@ -61,7 +61,6 @@
|
||||
"change-case": "^5.4.4",
|
||||
"nanostores": "^0.11.3",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"scroll-timeline-polyfill": "^1.1.0",
|
||||
"shallow-equal": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
Submodule packages/sdk-components-animation/private-src updated: 45acad5a6c...9a8c382dd3
@ -1,11 +1,17 @@
|
||||
import type { PropMeta } from "@webstudio-is/sdk";
|
||||
|
||||
export const props: Record<string, PropMeta> = {
|
||||
charWindow: { required: true, control: "number", type: "number" },
|
||||
charWindow: {
|
||||
required: false,
|
||||
control: "number",
|
||||
type: "number",
|
||||
defaultValue: 5,
|
||||
},
|
||||
easing: {
|
||||
required: true,
|
||||
required: false,
|
||||
control: "select",
|
||||
type: "string",
|
||||
defaultValue: "linear",
|
||||
options: [
|
||||
"linear",
|
||||
"easeIn",
|
||||
|
@ -14,13 +14,13 @@ const easings = {
|
||||
};
|
||||
|
||||
type AnimateChildrenProps = {
|
||||
charWindow: number;
|
||||
easing: keyof typeof easings;
|
||||
charWindow?: number;
|
||||
easing?: keyof typeof easings;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AnimateText = forwardRef<ElementRef<"div">, AnimateChildrenProps>(
|
||||
({ charWindow: _, easing: __, ...props }, ref) => {
|
||||
({ charWindow = 5, easing = "linear", ...props }, ref) => {
|
||||
return <div ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
|
@ -6,6 +6,7 @@
|
||||
"private-src",
|
||||
"../sdk/src/schema/animation-schema.ts"
|
||||
],
|
||||
"exclude": ["private-src/perf/**/*"],
|
||||
"compilerOptions": {
|
||||
"types": ["react/experimental", "react-dom/experimental", "@types/node"]
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js
|
||||
index 2c854928701c5dae23c985eb04f73a6ca2b661c3..abf1a7629568f411dd83dd509640850706d59528 100644
|
||||
--- a/src/scroll-timeline-base.js
|
||||
+++ b/src/scroll-timeline-base.js
|
||||
@@ -162,13 +162,19 @@ function isValidAxis(axis) {
|
||||
*/
|
||||
export function measureSource (source) {
|
||||
const style = getComputedStyle(source);
|
||||
+ const clientHeight = source.clientHeight;
|
||||
return {
|
||||
scrollLeft: source.scrollLeft,
|
||||
scrollTop: source.scrollTop,
|
||||
scrollWidth: source.scrollWidth,
|
||||
scrollHeight: source.scrollHeight,
|
||||
clientWidth: source.clientWidth,
|
||||
- clientHeight: source.clientHeight,
|
||||
+ get clientHeight() {
|
||||
+ if (document.scrollingElement === source) {
|
||||
+ return window.innerHeight;
|
||||
+ }
|
||||
+ return clientHeight;
|
||||
+ },
|
||||
writingMode: style.writingMode,
|
||||
direction: style.direction,
|
||||
scrollPaddingTop: style.scrollPaddingTop,
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -26,9 +26,6 @@ patchedDependencies:
|
||||
css-tree@2.3.1:
|
||||
hash: epgcmebti7rfrc2ej4odb3t4jy
|
||||
path: patches/css-tree@2.3.1.patch
|
||||
scroll-timeline-polyfill@1.1.0:
|
||||
hash: i4g3vdpump4efgy2hri5l5rsfm
|
||||
path: patches/scroll-timeline-polyfill@1.1.0.patch
|
||||
|
||||
importers:
|
||||
|
||||
@ -854,6 +851,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.2.25
|
||||
version: 18.2.25
|
||||
fast-glob:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
typescript:
|
||||
specifier: 5.7.3
|
||||
version: 5.7.3
|
||||
@ -1871,9 +1871,6 @@ importers:
|
||||
react-error-boundary:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(react@18.3.0-canary-14898b6a9-20240318)
|
||||
scroll-timeline-polyfill:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(patch_hash=i4g3vdpump4efgy2hri5l5rsfm)
|
||||
shallow-equal:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
@ -8497,9 +8494,6 @@ packages:
|
||||
scheduler@0.24.0-canary-14898b6a9-20240318:
|
||||
resolution: {integrity: sha512-ifDO3bUdooS4OlxvGxMyoDEC/aq14MvJLDd0thjrUSZGeLJA7WBc+sr9NZxIxrXfVqMl1GTGGPwXqRJZDNW76w==}
|
||||
|
||||
scroll-timeline-polyfill@1.1.0:
|
||||
resolution: {integrity: sha512-BpL3gk3Ynt/5VYaDFUNUP/FTkDldwKQnWcA07g/mDHkMVS9pQUyUXBpsy4RZYAgsfFeI1tWcnPNrEFtCpQoO9Q==}
|
||||
|
||||
selfsigned@2.1.1:
|
||||
resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -16559,8 +16553,6 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
scroll-timeline-polyfill@1.1.0(patch_hash=i4g3vdpump4efgy2hri5l5rsfm): {}
|
||||
|
||||
selfsigned@2.1.1:
|
||||
dependencies:
|
||||
node-forge: 1.3.1
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const hasPrivateFolders = existsSync(
|
||||
path.join(process.cwd(), "private-src", "README.md")
|
||||
);
|
||||
|
||||
const isBareImport = (id: string) =>
|
||||
id.startsWith("@") || id.includes(".") === false;
|
||||
|
||||
export default defineConfig({
|
||||
// resolve only webstudio condition in tests
|
||||
resolve: {
|
||||
conditions: ["webstudio"],
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: [
|
||||
"src/components.ts",
|
||||
hasPrivateFolders ? "private-src/components.ts" : "src/components.ts",
|
||||
"src/metas.ts",
|
||||
"src/props.ts",
|
||||
"src/hooks.ts",
|
||||
@ -21,11 +23,20 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
external: isBareImport,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
dir: "lib",
|
||||
},
|
||||
output: [
|
||||
{
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
dir: "lib",
|
||||
},
|
||||
hasPrivateFolders
|
||||
? {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "private-src",
|
||||
dir: "lib",
|
||||
}
|
||||
: undefined,
|
||||
].filter((output) => output !== undefined),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user