mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
176
packages/live-previews/src/utils/check-package.ts
Normal file
176
packages/live-previews/src/utils/check-package.ts
Normal 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;
|
||||
};
|
||||
12
packages/live-previews/src/utils/development-cookie.ts
Normal file
12
packages/live-previews/src/utils/development-cookie.ts
Normal 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";
|
||||
};
|
||||
26
packages/live-previews/src/utils/pretty-spaces.test.ts
Normal file
26
packages/live-previews/src/utils/pretty-spaces.test.ts
Normal 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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
3
packages/live-previews/src/utils/pretty-spaces.ts
Normal file
3
packages/live-previews/src/utils/pretty-spaces.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const prettySpaces = (content: string): string => {
|
||||
return content.replace(/ {2,}/g, " ").replace(/\n{2,}/g, "\n");
|
||||
};
|
||||
46
packages/live-previews/src/utils/replace-imports.test.ts
Normal file
46
packages/live-previews/src/utils/replace-imports.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
127
packages/live-previews/src/utils/replace-imports.ts
Normal file
127
packages/live-previews/src/utils/replace-imports.ts
Normal 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;
|
||||
};
|
||||
100
packages/live-previews/src/utils/use-code.ts
Normal file
100
packages/live-previews/src/utils/use-code.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user