diff --git a/packages/cli/src/framework-remix.ts b/packages/cli/src/framework-remix.ts new file mode 100644 index 000000000000..ba449fbb49f6 --- /dev/null +++ b/packages/cli/src/framework-remix.ts @@ -0,0 +1,90 @@ +import { join } from "node:path"; +import { readFile, rm } from "node:fs/promises"; +import { + generateRemixRoute, + namespaceMeta, + WsComponentMeta, +} from "@webstudio-is/react-sdk"; +import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas"; +import * as remixComponentMetas from "@webstudio-is/sdk-components-react-remix/metas"; +import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas"; +import type { Framework } from "./framework"; + +export const createFramework = async (): Promise => { + const routeTemplatesDir = join("app", "route-templates"); + + const htmlTemplate = await readFile( + join(routeTemplatesDir, "html.tsx"), + "utf8" + ); + const xmlTemplate = await readFile( + join(routeTemplatesDir, "xml.tsx"), + "utf8" + ); + const defaultSitemapTemplate = await readFile( + join(routeTemplatesDir, "default-sitemap.tsx"), + "utf8" + ); + const redirectTemplate = await readFile( + join(routeTemplatesDir, "redirect.tsx"), + "utf8" + ); + + // cleanup route templates after reading to not bloat generated code + await rm(routeTemplatesDir, { recursive: true, force: true }); + + const radixComponentNamespacedMetas: Record = {}; + for (const [name, meta] of Object.entries(radixComponentMetas)) { + const namespace = "@webstudio-is/sdk-components-react-radix"; + radixComponentNamespacedMetas[`${namespace}:${name}`] = namespaceMeta( + meta, + namespace, + new Set(Object.keys(radixComponentMetas)) + ); + } + + return { + components: [ + { + source: "@webstudio-is/sdk-components-react", + metas: baseComponentMetas, + }, + { + source: "@webstudio-is/sdk-components-react-radix", + metas: radixComponentNamespacedMetas, + }, + { + source: "@webstudio-is/sdk-components-react-remix", + metas: remixComponentMetas, + }, + ], + html: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`), + template: htmlTemplate, + }, + ], + xml: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.tsx`), + template: xmlTemplate, + }, + ], + redirect: ({ pagePath }: { pagePath: string }) => [ + { + file: join("app", "routes", `${generateRemixRoute(pagePath)}.ts`), + template: redirectTemplate, + }, + ], + defaultSitemap: () => [ + { + file: join( + "app", + "routes", + `${generateRemixRoute("/sitemap.xml")}.tsx` + ), + template: defaultSitemapTemplate, + }, + ], + }; +}; diff --git a/packages/cli/src/framework.ts b/packages/cli/src/framework.ts new file mode 100644 index 000000000000..48d710ef7712 --- /dev/null +++ b/packages/cli/src/framework.ts @@ -0,0 +1,19 @@ +import type { WsComponentMeta } from "@webstudio-is/react-sdk"; + +type FrameworkComponentEntry = { + source: string; + metas: Record; +}; + +type FrameworkTemplateEntry = { + file: string; + template: string; +}; + +export type Framework = { + components: FrameworkComponentEntry[]; + html: (params: { pagePath: string }) => FrameworkTemplateEntry[]; + xml: (params: { pagePath: string }) => FrameworkTemplateEntry[]; + redirect: (params: { pagePath: string }) => FrameworkTemplateEntry[]; + defaultSitemap: () => FrameworkTemplateEntry[]; +}; diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index db4b6e80a2c4..cf01fed772dc 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -1,4 +1,4 @@ -import { basename, dirname, join, normalize } from "node:path"; +import { basename, dirname, join, normalize, relative } from "node:path"; import { createWriteStream } from "node:fs"; import { rm, @@ -19,7 +19,6 @@ import { generateCss, generateWebstudioComponent, getIndexesWithinAncestors, - namespaceMeta, type Params, type WsComponentMeta, normalizeProps, @@ -50,9 +49,6 @@ import { } from "@webstudio-is/sdk"; import type { Data } from "@webstudio-is/http-client"; import { createImageLoader } from "@webstudio-is/image"; -import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas"; -import * as remixComponentMetas from "@webstudio-is/sdk-components-react-remix/metas"; -import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas"; import { LOCAL_DATA_FILE } from "./config"; import { createFileIfNotExists, @@ -62,6 +58,7 @@ import { } from "./fs-utils"; import type * as sharedConstants from "../templates/defaults/app/constants.mjs"; import { htmlToJsx } from "./html-to-jsx"; +import { createFramework } from "./framework-remix"; const limit = pLimit(10); @@ -263,6 +260,8 @@ export const prebuild = async (options: { await copyTemplates(template); } + const framework = await createFramework(); + const constants: typeof sharedConstants = await import( pathToFileURL(join(cwd(), "app/constants.mjs")).href ); @@ -284,28 +283,15 @@ export const prebuild = async (options: { throw new Error(`Project domain is missing from the project data`); } - const radixComponentNamespacedMetas = Object.entries( - radixComponentMetas - ).reduce( - (r, [name, meta]) => { - const namespace = "@webstudio-is/sdk-components-react-radix"; - r[`${namespace}:${name}`] = namespaceMeta( - meta, - namespace, - new Set(Object.keys(radixComponentMetas)) - ); - return r; - }, - {} as Record - ); - - const metas = new Map( - Object.entries({ - ...baseComponentMetas, - ...radixComponentNamespacedMetas, - ...remixComponentMetas, - }) - ); + // collect all possible component metas + const metas = new Map(); + const componentSources = new Map(); + for (const entry of framework.components) { + for (const [componentName, meta] of Object.entries(entry.metas)) { + metas.set(componentName, meta); + componentSources.set(componentName, entry.source); + } + } const projectMetas = new Map(); const componentsByPage: ComponentsByPage = {}; @@ -494,22 +480,6 @@ export const prebuild = async (options: { await createFileIfNotExists(join(generatedDir, "index.css"), cssText); - // MARK: - Route templates read - const routeTemplatesDir = join(cwd(), "app/route-templates"); - - const routeTemplatePath = normalize(join(routeTemplatesDir, "html.tsx")); - const routeXmlTemplatePath = normalize(join(routeTemplatesDir, "xml.tsx")); - const defaultSiteMapXmlPath = normalize( - join(routeTemplatesDir, "default-sitemap.tsx") - ); - const redirectPath = normalize(join(routeTemplatesDir, "redirect.tsx")); - - const routeFileTemplate = await readFile(routeTemplatePath, "utf8"); - const routeXmlFileTemplate = await readFile(routeXmlTemplatePath, "utf8"); - const defaultSiteMapTemplate = await readFile(defaultSiteMapXmlPath, "utf8"); - const redirectTemplate = await readFile(redirectPath, "utf8"); - await rm(routeTemplatesDir, { recursive: true, force: true }); - for (const [pageId, pageComponents] of Object.entries(componentsByPage)) { const scope = createScope([ // manually maintained list of occupied identifiers @@ -524,30 +494,18 @@ export const prebuild = async (options: { string, Set<[shortName: string, componentName: string]> >(); - - const BASE_NAMESPACE = "@webstudio-is/sdk-components-react"; - const REMIX_NAMESPACE = "@webstudio-is/sdk-components-react-remix"; - for (const component of pageComponents) { - const parsed = parseComponentName(component); - let [namespace] = parsed; - const [_namespace, shortName] = parsed; - + const namespace = componentSources.get(component); if (namespace === undefined) { - // use base as fallback namespace and consider remix overrides - if (shortName in remixComponentMetas) { - namespace = REMIX_NAMESPACE; - } else { - namespace = BASE_NAMESPACE; - } + continue; } - if (namespaces.has(namespace) === false) { namespaces.set( namespace, new Set<[shortName: string, componentName: string]>() ); } + const [_namespace, shortName] = parseComponentName(component); namespaces.get(namespace)?.add([shortName, component]); } @@ -728,37 +686,33 @@ export const prebuild = async (options: { const generatedBasename = generateRemixRoute(pagePath); - const routeFileContent = ( - documentType === "html" ? routeFileTemplate : routeXmlFileTemplate - ) - .replaceAll("__CLIENT__", `../__generated__/${generatedBasename}`) - .replaceAll("__SERVER__", `../__generated__/${generatedBasename}.server`) - .replaceAll("__CSS__", `../__generated__/index.css`); - - await createFileIfNotExists( - join(routesDir, `${generateRemixRoute(pagePath)}.tsx`), - routeFileContent - ); - - await createFileIfNotExists( - join(generatedDir, `${generatedBasename}.tsx`), - pageExports - ); - - await createFileIfNotExists( - join(generatedDir, `${generatedBasename}.server.tsx`), - serverExports - ); + const clientFile = join(generatedDir, `${generatedBasename}.tsx`); + await createFileIfNotExists(clientFile, pageExports); + + const serverFile = join(generatedDir, `${generatedBasename}.server.tsx`); + await createFileIfNotExists(serverFile, serverExports); + + const getTemplates = + documentType === "html" ? framework.html : framework.xml; + for (const { file, template } of getTemplates({ pagePath })) { + const base = relative(dirname(file), generatedDir); + const content = template + .replaceAll("__CLIENT__", `${base}/${generatedBasename}`) + .replaceAll("__SERVER__", `${base}/${generatedBasename}.server`) + .replaceAll("__CSS__", `${base}/index.css`); + await createFileIfNotExists(file, content); + } } // MARK: - Default sitemap.xml - await createFileIfNotExists( - join(routesDir, `${generateRemixRoute("/sitemap.xml")}.tsx`), - defaultSiteMapTemplate.replaceAll( + for (const { file, template } of framework.defaultSitemap()) { + const base = relative(dirname(file), generatedDir); + const content = template.replaceAll( "__SITEMAP__", - `../__generated__/$resources.sitemap.xml` - ) - ); + `${base}/$resources.sitemap.xml` + ); + await createFileIfNotExists(file, content); + } await createFileIfNotExists( join(generatedDir, "$resources.sitemap.xml.ts"), @@ -783,13 +737,16 @@ export const prebuild = async (options: { ` ); - await createFileIfNotExists( - join(routesDir, `${generateRemixRoute(redirect.old)}.ts`), - redirectTemplate.replaceAll( + for (const { file, template } of framework.redirect({ + pagePath: redirect.old, + })) { + const base = relative(dirname(file), generatedDir); + const content = template.replaceAll( "__REDIRECT__", - `../__generated__/${generatedBasename}` - ) - ); + `${base}/${generatedBasename}` + ); + await createFileIfNotExists(file, content); + } } }