This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
export const checkPackage = (code = "") => {
const set = new Set<string>();
const hasAntd =
code.includes("@refinedev/antd") ||
code.includes("RefineAntd") ||
code.includes(`from "antd"`) ||
code.includes("@ant-design/icons") ||
code.includes("@uiw/react-md-editor");
const hasMui =
code.includes("@refinedev/mui") ||
code.includes("RefineMui") ||
code.includes("@emotion/react") ||
code.includes("@emotion/styled") ||
code.includes("@mui/lab") ||
code.includes("@mui/material/styles") ||
code.includes("@mui/icons-material") ||
code.includes("@mui/material") ||
code.includes("@mui/x-data-grid") ||
code.includes(`from "react-hook-form"`);
const hasMantine =
code.includes("@refinedev/mantine") ||
code.includes("RefineMantine") ||
code.includes("@mantine/core") ||
code.includes("@mantine/hooks") ||
code.includes("@mantine/form") ||
code.includes("@mantine/notifications");
const hasChakra =
code.includes("@refinedev/chakra-ui") ||
code.includes("RefineChakra") ||
code.includes("@chakra-ui/react") ||
code.includes(`from "react-hook-form"`);
const hasAntdInferencer =
code.includes("@refinedev/inferencer/antd") ||
code.includes("RefineAntdInferencer");
const hasMuiInferencer =
code.includes("@refinedev/inferencer/mui") ||
code.includes("RefineMuiInferencer");
const hasMantineInferencer =
code.includes("@refinedev/inferencer/mantine") ||
code.includes("RefineMantineInferencer");
const hasChakraInferencer =
code.includes("@refinedev/inferencer/chakra-ui") ||
code.includes("RefineChakraInferencer");
const hasHeadlessInferencer =
code.includes("@refinedev/inferencer/headless") ||
code.includes("RefineHeadlessInferencer");
const hasCasbin = code.includes("casbin");
const hasI18n = code.includes("react-i18next") || code.includes("i18next");
const hasTablerIcons =
code.includes("@tabler/icons") || code.includes("@tabler/icons-react");
const hasKbar = code.includes("@refinedev/kbar");
const hasAirtable = code.includes("@refinedev/airtable");
const hasAppwrite = code.includes("@refinedev/appwrite");
const hasHasura = code.includes("@refinedev/hasura");
const hasNestjsxCrud = code.includes("@refinedev/nestjsx-crud");
const hasNestjsQuery = code.includes("@refinedev/nestjs-query");
const hasStrapiV4 = code.includes("@refinedev/strapi-v4");
const hasStrapiGraphql = code.includes("@refinedev/strapi-graphql");
const hasSupabase = code.includes("@refinedev/supabase");
const hasDevtools = code.includes("@refinedev/devtools");
const hasAxios = code.includes("axios");
const hasAuth0 = code.includes("@auth0/auth0-react");
const hasKeycloak =
code.includes("@react-keycloak/web") || code.includes("keycloak-js");
const hasReactDom = code.includes("react-dom/client");
const hasWebVitals = code.includes("./reportWebVitals");
if (hasReactDom) {
set.add("react-dom");
}
if (hasWebVitals) {
set.add("web-vitals");
}
if (hasI18n) {
set.add("i18n");
}
if (hasTablerIcons) {
set.add("tabler-icons");
}
if (hasKbar) {
set.add("kbar");
}
if (hasAirtable) {
set.add("airtable");
}
if (hasAppwrite) {
set.add("appwrite");
}
if (hasHasura) {
set.add("hasura");
}
if (hasNestjsxCrud) {
set.add("nestjsx-crud");
}
if (hasNestjsQuery) {
set.add("nestjs-query");
}
if (hasStrapiV4) {
set.add("strapi-v4");
}
if (hasStrapiGraphql) {
set.add("strapi-graphql");
}
if (hasSupabase) {
set.add("supabase");
}
if (hasDevtools) {
set.add("devtools");
}
if (hasAxios) {
set.add("axios");
}
if (hasAuth0) {
set.add("auth0");
}
if (hasKeycloak) {
set.add("keycloak");
}
if (hasAntd) {
set.add("antd");
}
if (hasAntdInferencer) {
set.add("antd-inferencer");
}
if (hasMui) {
set.add("mui");
}
if (hasMuiInferencer) {
set.add("mui-inferencer");
}
if (hasMantine) {
set.add("mantine");
}
if (hasMantineInferencer) {
set.add("mantine-inferencer");
}
if (hasChakra) {
set.add("chakra");
}
if (hasChakraInferencer) {
set.add("chakra-inferencer");
}
if (hasHeadlessInferencer) {
set.add("headless-inferencer");
}
if (hasCasbin) {
set.add("casbin");
}
return set;
};

View File

@@ -0,0 +1,12 @@
const DEVELOPMENT_MODE_COOKIE = "refine-live-preview-development-mode";
export const isDevelopmentModeByCookie = (): boolean => {
if (typeof document === "undefined") return false;
const cookie = document.cookie;
const parts = cookie.split(";");
const dev = parts.find((part) => part.includes(DEVELOPMENT_MODE_COOKIE));
if (!dev) return false;
const [, value] = dev.split("=");
return value === "true";
};

View File

@@ -0,0 +1,26 @@
import { prettySpaces } from "./pretty-spaces";
describe("Pretty Spaces Helper", () => {
it("should remove extra spaces", () => {
const content = "const a = 1; const b = 2;";
const result = prettySpaces(content);
expect(result).toBe("const a = 1; const b = 2;");
});
it("should remove extra new lines", () => {
const content = ["const a = 1;", "", "", "const b = 2;"].join("\n");
const result = prettySpaces(content);
expect(result).toEqual(["const a = 1;", "const b = 2;"].join("\n"));
});
it("should remove extra spaces and new lines", () => {
const content = [
"const a = 1; const b = 2;",
"",
"",
"const c = 3; const d = 4;",
].join("\n");
const result = prettySpaces(content);
expect(result).toEqual(
["const a = 1; const b = 2;", "const c = 3; const d = 4;"].join("\n"),
);
});
});

View File

@@ -0,0 +1,3 @@
export const prettySpaces = (content: string): string => {
return content.replace(/ {2,}/g, " ").replace(/\n{2,}/g, "\n");
};

View File

@@ -0,0 +1,46 @@
import { replaceImports } from "./replace-imports";
describe("Replace Imports Helper", () => {
it("works with default import", () => {
const content = `import { default as RefineCore } from "@refinedev/core";`;
const result = replaceImports(content);
expect(result).toContain("const { default: RefineCore } = RefineCore;");
expect(result).not.toContain(content);
});
it("works with named import", () => {
const content = `import { useForm, useStepsForm } from "@refinedev/core";`;
const result = replaceImports(content);
expect(result).toContain("const { useForm, useStepsForm } = RefineCore;");
expect(result).not.toContain(content);
});
it("works with namespace import", () => {
const content = `import * as RefineCore from "@refinedev/core";`;
const result = replaceImports(content);
expect(result).toContain("const RefineCore = RefineCore;");
expect(result).not.toContain(content);
});
it("removes side effect imports", () => {
const content = `import "@refinedev/core";`;
const result = replaceImports(content);
expect(result).not.toContain(content);
});
it("works with name change", () => {
const content = `import { useForm as useRefineForm } from "@refinedev/core";`;
const result = replaceImports(content);
expect(result).toContain("const { useForm: useRefineForm } = RefineCore;");
expect(result).not.toContain(content);
});
it("should work with multiline code", () => {
const content = `import { useForm, useStepsForm } from "@refinedev/core";
import { default as RefineCore } from "@refinedev/core";
import * as RefineCore from "@refinedev/core";
import "@refinedev/core";
//this should still be there`;
const result = replaceImports(content);
expect(result).toContain("const { useForm, useStepsForm } = RefineCore;");
expect(result).toContain("const { default: RefineCore } = RefineCore;");
expect(result).toContain("const RefineCore = RefineCore;");
expect(result).not.toContain(content);
expect(result).toContain("//this should still be there");
});
});

View File

@@ -0,0 +1,127 @@
import { packageMap, packageScopeMap } from "@/src/scope/map";
import { isDevelopmentModeByCookie } from "./development-cookie";
import { prettySpaces } from "./pretty-spaces";
const packageRegex =
/import(?:(?:(?:[ \n\t]+([^ *\n\t\{\},]+)[ \n\t]*(?:,|[ \n\t]+))?([ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+([^ \n\t\{\}]+)[ \n\t]+)from[ \n\t]*(?:['"])([^'"\n]+)(?:['"])(?:;?)/g;
//
const sideEffectRegex = /import[ \n\t](?:['"])([^'"\n]+)(?:['"])(?:;?)/g;
const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g;
export const replaceImports = (content: string): string => {
const matches = content.matchAll(packageRegex);
const imports = new Set();
for (const match of matches) {
const [
,
baseDefaultImport,
baseNamedImports,
namespaceImport,
packageName,
] = match;
let defaultImport: string | undefined = baseDefaultImport;
let namedImports: string | undefined = baseNamedImports;
const regexMatch = Object.entries(packageScopeMap).find(([key, value]) =>
// value is regexp, key is package name
value.test(packageName),
);
const regexPackage = regexMatch ? regexMatch[0] : null;
if (regexPackage && !(packageName in packageMap)) {
const importName = packageMap[regexPackage];
if (regexMatch) {
// If `regexMatch` is present, it means we have a scoped import
// In this case if there's a default import, we should use the scope name instead
// Because the default import name may not match the root export name
// eg: import Button$1 from "@mui/material/Button";
// We'll convert it to a named export: import { /scopedImportName/ as /defaultImport/ } from "@mui/material";
if (defaultImport) {
const scopedImportName = packageName.match(regexMatch[1])?.[1];
if (scopedImportName) {
if (namedImports) {
namedImports = `{ ${scopedImportName} as ${defaultImport}, ${namedImports.slice(
1,
-1,
)} }`;
} else {
namedImports = ` { ${scopedImportName} as ${defaultImport} }`;
}
defaultImport = undefined;
}
}
if (namedImports) {
// If the import is made like this: import { default as Button$1 } from "@mui/material/Button";
// We should convert it to: import { Button as Button$1 } from "@mui/material";
if (namedImports.includes("default as")) {
const scopedImportName = packageName.match(regexMatch[1])?.[1];
namedImports = namedImports.replace(
"default as",
`${scopedImportName} as`,
);
}
}
}
if (defaultImport) {
imports.add(`const { ${defaultImport} } = ${importName};`);
}
if (namedImports) {
imports.add(
`const${namedImports.replace(
nameChangeRegex,
"$1: $3$4",
)} = ${importName};`,
);
}
}
if (packageName in packageMap) {
const importName = packageMap[packageName];
if (defaultImport) {
imports.add(`const { default: ${defaultImport} } = ${importName};`);
}
if (namedImports) {
imports.add(
`const${namedImports.replace(
nameChangeRegex,
"$1: $3$4",
)} = ${importName};`,
);
}
if (namespaceImport) {
imports.add(`const ${namespaceImport} = ${importName};`);
}
}
}
const pretty = prettySpaces(`
${Array.from(imports).join("\n")}
${content
.replace(packageRegex, "")
.replace(sideEffectRegex, "")
.replace(
"React.createElement(GoogleButton,",
"React.createElement(MockGoogleButton,",
)}
`);
if (isDevelopmentModeByCookie()) {
console.log("== Incoming Code ==");
console.log(content);
console.log("== ==");
console.log("== Transformed Code ==");
console.log(pretty);
console.log("== ==");
}
return pretty;
};

View File

@@ -0,0 +1,100 @@
import React from "react";
import { useRouter } from "next/router";
import { decompressFromEncodedURIComponent } from "lz-string";
import base64url from "base64url";
type UseCodeReturn = {
code: string | null;
css: string | null;
isReady: boolean;
hasQuery: boolean;
disableScroll: boolean;
useTailwind: boolean;
isLoading: boolean;
};
export const useCode = (): UseCodeReturn => {
const { query, isReady } = useRouter();
const {
code: encoded,
hash,
disableScroll,
tailwind,
css: cssCompressed,
} = query ?? {};
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [compressed, setCompressed] = React.useState<string | undefined>();
React.useEffect(() => {
if (!isReady) return;
if (compressed) return;
if (encoded) {
setCompressed(encoded as string);
setIsLoading(false);
return;
}
if (hash) {
fetch(
`https://${process.env.NEXT_PUBLIC_PREVIEWS_BUCKET_NAME}.fra1.cdn.digitaloceanspaces.com/preview-strings/${hash}`,
)
.then((body) =>
body.text().then((data) => {
setCompressed(data);
setIsLoading(false);
}),
)
.catch((e) => {
setIsLoading(false);
});
}
}, [isReady, compressed, encoded, hash]);
const code = React.useMemo(() => {
if (!isReady) return "";
if (!compressed) return "";
const decompressed = decompressFromEncodedURIComponent(
compressed as string,
);
const fixed = decompressed?.replace(/React\$1/g, "React");
const shouldChangeEntrypoint =
fixed.match(/render\(<App \/>\);?/) &&
fixed.match(/createRoot\(container\);?/);
let content = fixed;
if (shouldChangeEntrypoint) {
content = fixed.replace(/render\(<App \/>\);?/, "");
content = content.replace(
/createRoot\(container\);?/,
"{ render: (children) => render(<RefineCore.ExternalNavigationProvider>{children}</RefineCore.ExternalNavigationProvider>) };",
);
}
return content;
}, [compressed, isReady]);
const css = React.useMemo(() => {
if (!isReady) return "";
if (!cssCompressed) return "";
try {
return base64url.decode(cssCompressed as string);
} catch (err) {
console.log("CSS Decode Error", { err });
return "";
}
}, [cssCompressed, isReady]);
return {
code,
css,
isReady,
hasQuery: !!encoded || !!hash,
disableScroll: !!disableScroll,
useTailwind: !!tailwind,
isLoading,
};
};