experimental: Allow json in template literals (#3886)

## Description
closes #3872


- [x] - Builder support
- [x] - Publish support

## Steps for reproduction

Example

https://add-json.staging.webstudio.is/builder/8160e16d-5ab4-4249-86de-0c98a64526a3

https://test-json-ki9yj.wstd.work/

## 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:
5de6)
- [ ] 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:
Ivan Starkov authored and GitHub committed 2024-08-11 11:50:18 +03:00
1 parent 403d7b2328
commit 13da170ef4
38 files changed
+888 -173

No files matched your search

+2 -1
View File
@@ -496,6 +496,7 @@ export const prebuild = async (options: {
"useState",
"Fragment",
"useResource",
"useVariableState",
"Page",
"_props",
]);
@@ -615,7 +616,7 @@ export const prebuild = async (options: {
import { Fragment, useState } from "react";
import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
import { useResource } from "@webstudio-is/react-sdk/runtime";
import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
${componentImports}
export const siteName = ${JSON.stringify(projectMeta?.siteName)};
@@ -472,7 +472,7 @@ test("generate component with variables and actions", () => {
).toEqual(
clear(`
const Page = () => {
let [variableName, set$variableName] = useState<any>("initial")
let [variableName, set$variableName] = useVariableState<any>("initial")
return <Body>
<Input
value={variableName}
@@ -545,7 +545,7 @@ test("avoid generating collection parameter variable as state", () => {
).toEqual(
clear(`
const Page = () => {
let [data, set$data] = useState<any>(["apple","orange","mango"])
let [data, set$data] = useVariableState<any>(["apple","orange","mango"])
return <Body>
{data?.map((element: any, index: number) =>
<Fragment key={index}>
@@ -637,7 +637,7 @@ test("generate resources loading", () => {
).toEqual(
clear(`
const Page = () => {
let [data, set$data] = useState<any>("data")
let [data, set$data] = useVariableState<any>("data")
let data_1 = useResource("data_2")
return <Body
data-data={data}
@@ -702,7 +702,7 @@ test("avoid generating unused variables", () => {
})
).toMatchInlineSnapshot(`
"const Page = ({ }: { system: any; }) => {
let [UsedVariableName, set$UsedVariableName] = useState<any>("initial")
let [UsedVariableName, set$UsedVariableName] = useVariableState<any>("initial")
return <Body
data-data={UsedVariableName} />
}
@@ -771,21 +771,21 @@ test("generate conditional collection", () => {
),
})
).toMatchInlineSnapshot(`
"const Page = () => {
let [conditionName, set$conditionName] = useState<any>(false)
return <Body>
{(conditionName) &&
<>
{[]?.map((collectionItemName: any, index: number) =>
<Fragment key={index}>
</Fragment>
)}
</>
}
</Body>
}
"
`);
"const Page = () => {
let [conditionName, set$conditionName] = useVariableState<any>(false)
return <Body>
{(conditionName) &&
<>
{[]?.map((collectionItemName: any, index: number) =>
<Fragment key={index}>
</Fragment>
)}
</>
}
</Body>
}
"
`);
});
test("generate resource prop", () => {
@@ -424,7 +424,7 @@ export const generateWebstudioComponent = ({
);
const initialValue = dataSource.value.value;
const initialValueString = JSON.stringify(initialValue);
generatedDataSources += `let [${valueName}, ${setterName}] = useState<any>(${initialValueString})\n`;
generatedDataSources += `let [${valueName}, ${setterName}] = useVariableState<any>(${initialValueString})\n`;
}
if (dataSource.type === "resource") {
const valueName = scope.getName(dataSource.id, dataSource.name);
+11 -2
View File
@@ -1,5 +1,6 @@
import { createContext, useContext } from "react";
import { createContext, useContext, useMemo } from "react";
import type { ImageLoader } from "@webstudio-is/image";
import { createJsonStringifyProxy, isPlainObject } from "@webstudio-is/sdk";
export type Params = {
/**
@@ -49,5 +50,13 @@ export const ReactSdkContext = createContext<
export const useResource = (name: string) => {
const { resources } = useContext(ReactSdkContext);
return resources[name];
const resource = resources[name];
const resourceMemozied = useMemo(
() =>
isPlainObject(resource) ? createJsonStringifyProxy(resource) : resource,
[resource]
);
return resourceMemozied;
};
+1
View File
@@ -1,5 +1,6 @@
export * from "./context";
export * from "./hook";
export * from "./variable-state";
export const getIndexWithinAncestorFromComponentProps = (
props: Record<string, unknown>
+15
View File
@@ -0,0 +1,15 @@
import { createJsonStringifyProxy, isPlainObject } from "@webstudio-is/sdk";
import { useState, useMemo, type Dispatch, type SetStateAction } from "react";
export const useVariableState = <S,>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>] => {
const [state, setState] = useState(initialState);
const value = useMemo(
() => (isPlainObject(state) ? createJsonStringifyProxy(state) : state),
[state]
);
return [value, setState];
};
+1
View File
@@ -20,3 +20,4 @@ export * from "./resources-generator";
export * from "./page-meta-generator";
export * from "./url-pattern";
export * from "./form-fields";
export * from "./to-string";
+27
View File
@@ -0,0 +1,27 @@
export const createJsonStringifyProxy = <T extends object>(target: T): T => {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop === "toString") {
return function () {
return JSON.stringify(target);
};
}
const value = Reflect.get(target, prop, receiver);
if (typeof value === "object" && value !== null) {
return createJsonStringifyProxy(value);
}
return value;
},
});
};
export const isPlainObject = (value: unknown): value is object => {
return (
Object.prototype.toString.call(value) === "[object Object]" &&
(Object.getPrototypeOf(value) === null ||
Object.getPrototypeOf(value) === Object.prototype)
);
};