diff --git a/framework/css.ts b/framework/css.ts index 682f6c7..c7063c5 100644 --- a/framework/css.ts +++ b/framework/css.ts @@ -26,7 +26,10 @@ export function preprocess(css: string, theme: Theme): string { return css.replace( regex, (_, line) => - line.replace(regex2, (_: string, varName: string) => theme[varName]) + + line.replace( + regex2, + (_: string, varName: string) => theme[varName as keyof Theme], + ) + ";" + line.slice(1), ); } diff --git a/framework/engine/jsx-runtime.ts b/framework/engine/jsx-runtime.ts index fe84691..999d500 100644 --- a/framework/engine/jsx-runtime.ts +++ b/framework/engine/jsx-runtime.ts @@ -29,4 +29,18 @@ export function jsxDEV( // jsxs export { jsx as jsxs }; +declare global { + namespace JSX { + interface IntrinsicElements { + [name: string]: Record; + } + interface ElementChildrenAttribute { + children: {}; + } + type Element = engine.Node; + type ElementType = keyof IntrinsicElements | engine.Component; + type ElementClass = ReturnType; + } +} + import * as engine from "./ssr.ts"; diff --git a/framework/engine/ssr.ts b/framework/engine/ssr.ts index 242250d..fbcaeba 100644 --- a/framework/engine/ssr.ts +++ b/framework/engine/ssr.ts @@ -198,7 +198,7 @@ function renderElement(element: ResolvedElement) { case "className": // Legacy React Compat case "class": - attr = `class=${quoteIfNeeded(escapeHTML(clsx(value)))}`; + attr = `class=${quoteIfNeeded(escapeHTML(clsx(value as ClsxInput)))}`; break; case "htmlFor": throw new Error("Do not use the `htmlFor` attribute. Use `for`"); @@ -232,7 +232,7 @@ export function renderStyleAttribute(style: Record) { } return "style=" + quoteIfNeeded(out); } -export function quoteIfNeeded(text) { +export function quoteIfNeeded(text: string) { if (text.includes(" ")) return '"' + text + '"'; return text; } @@ -270,11 +270,10 @@ export function inspect(object: unknown) { export type ClsxInput = string | Record | ClsxInput[]; export function clsx(mix: ClsxInput) { - var k, y, str; + var k, y, str = ""; if (typeof mix === "string") { return mix; } else if (typeof mix === "object") { - str = ""; if (Array.isArray(mix)) { for (k = 0; k < mix.length; k++) { if (mix[k] && (y = clsx(mix[k]))) { diff --git a/framework/meta/index.ts b/framework/meta/index.ts deleted file mode 100644 index ffe86b7..0000000 --- a/framework/meta/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { resolveMetadata } from "./merge"; -import { renderMetadata } from "./render"; -import { Metadata } from "./types"; - -export * from "./types"; -export * from "./merge"; -export * from "./render"; - -export function resolveAndRenderMetadata( - ...metadata: [Metadata, ...Metadata[]] -) { - return renderMetadata(resolveMetadata(...metadata)); -} diff --git a/framework/meta/merge.ts b/framework/meta/merge.ts deleted file mode 100644 index 0cbfc75..0000000 --- a/framework/meta/merge.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { createDefaultMetadata } from "./nextjs/default-metadata"; -import { resolveAsArrayOrUndefined } from "./nextjs/generate/utils"; -import { - resolveAlternates, - resolveAppleWebApp, - resolveAppLinks, - resolveRobots, - resolveThemeColor, - resolveVerification, - resolveViewport, -} from "./nextjs/resolvers/resolve-basics"; -import { resolveIcons } from "./nextjs/resolvers/resolve-icons"; -import { - resolveOpenGraph, - resolveTwitter, -} from "./nextjs/resolvers/resolve-opengraph"; -import { resolveTitle } from "./nextjs/resolvers/resolve-title"; -import type { - Metadata, - ResolvedMetadata, -} from "./nextjs/types/metadata-interface"; - -type MetadataAccumulationOptions = { - pathname: string; -}; - -// Merge the source metadata into the resolved target metadata. -function merge( - target: ResolvedMetadata, - source: Metadata | null, - titleTemplates: { - title?: string | null; - twitter?: string | null; - openGraph?: string | null; - } = {}, -) { - const metadataBase = source?.metadataBase || target.metadataBase; - for (const key_ in source) { - const key = key_ as keyof Metadata; - - switch (key) { - case "title": { - target.title = resolveTitle(source.title, titleTemplates.title); - break; - } - case "alternates": { - target.alternates = resolveAlternates(source.alternates, metadataBase, { - pathname: (source as any)._pathname ?? "/", - }); - break; - } - case "openGraph": { - target.openGraph = resolveOpenGraph(source.openGraph, metadataBase); - if (target.openGraph) { - target.openGraph.title = resolveTitle( - target.openGraph.title, - titleTemplates.openGraph, - ); - } - break; - } - case "twitter": { - target.twitter = resolveTwitter(source.twitter, metadataBase); - if (target.twitter) { - target.twitter.title = resolveTitle( - target.twitter.title, - titleTemplates.twitter, - ); - } - break; - } - case "verification": - target.verification = resolveVerification(source.verification); - break; - case "viewport": { - target.viewport = resolveViewport(source.viewport); - break; - } - case "icons": { - target.icons = resolveIcons(source.icons); - break; - } - case "appleWebApp": - target.appleWebApp = resolveAppleWebApp(source.appleWebApp); - break; - case "appLinks": - target.appLinks = resolveAppLinks(source.appLinks); - break; - case "robots": { - target.robots = resolveRobots(source.robots); - break; - } - case "themeColor": { - target.themeColor = resolveThemeColor(source.themeColor); - break; - } - case "archives": - case "assets": - case "bookmarks": - case "keywords": - case "authors": { - // FIXME: type inferring - // @ts-ignore - target[key] = resolveAsArrayOrUndefined(source[key]) || null; - break; - } - // directly assign fields that fallback to null - case "applicationName": - case "description": - case "generator": - case "creator": - case "publisher": - case "category": - case "classification": - case "referrer": - case "colorScheme": - case "itunes": - case "formatDetection": - case "manifest": - // @ts-ignore TODO: support inferring - target[key] = source[key] || null; - break; - case "other": - target.other = Object.assign({}, target.other, source.other); - break; - case "metadataBase": - target.metadataBase = metadataBase; - break; - default: - break; - } - } - - return target; -} - -export interface MetadataWithPathname extends Metadata { - /** Set by framework author to the pathname of the page defining this metadata. */ - _pathname?: string; -} - -export function resolveMetadata( - ...metadata: [MetadataWithPathname, ...MetadataWithPathname[]] -) { - const base = createDefaultMetadata(); - for (const item of metadata) { - merge(base, item, { - title: base.title?.template, - twitter: base.twitter?.title?.template, - openGraph: base.openGraph?.title?.template, - }); - } - return base; -} diff --git a/framework/meta/nextjs/constants.ts b/framework/meta/nextjs/constants.ts deleted file mode 100644 index 5f9b883..0000000 --- a/framework/meta/nextjs/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Viewport } from "./types/extra-types"; -import type { Icons } from "./types/metadata-types"; - -export const ViewPortKeys: { [k in keyof Viewport]: string } = { - width: "width", - height: "height", - initialScale: "initial-scale", - minimumScale: "minimum-scale", - maximumScale: "maximum-scale", - viewportFit: "viewport-fit", - userScalable: "user-scalable", - interactiveWidget: "interactive-widget", -} as const; - -export const IconKeys: (keyof Icons)[] = ["icon", "shortcut", "apple", "other"]; diff --git a/framework/meta/nextjs/default-metadata.ts b/framework/meta/nextjs/default-metadata.ts deleted file mode 100644 index 9b5a8ae..0000000 --- a/framework/meta/nextjs/default-metadata.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ResolvedMetadata } from "./types/metadata-interface"; -import process from "node:process"; - -export function createDefaultMetadata(): ResolvedMetadata { - const defaultMetadataBase = - process.env.NODE_ENV === "production" && process.env.VERCEL_URL - ? new URL(`https://${process.env.VERCEL_URL}`) - : null; - - return { - viewport: "width=device-width, initial-scale=1", - metadataBase: defaultMetadataBase, - - // Other values are all null - title: null, - description: null, - applicationName: null, - authors: null, - generator: null, - keywords: null, - referrer: null, - themeColor: null, - colorScheme: null, - creator: null, - publisher: null, - robots: null, - manifest: null, - alternates: { - canonical: null, - languages: null, - media: null, - types: null, - }, - icons: null, - openGraph: null, - twitter: null, - verification: {}, - appleWebApp: null, - formatDetection: null, - itunes: null, - abstract: null, - appLinks: null, - archives: null, - assets: null, - bookmarks: null, - category: null, - classification: null, - other: {}, - }; -} diff --git a/framework/meta/nextjs/generate/alternate.tsx b/framework/meta/nextjs/generate/alternate.tsx deleted file mode 100644 index 26f694d..0000000 --- a/framework/meta/nextjs/generate/alternate.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { ResolvedMetadata } from "../types/metadata-interface"; - -import React from "react"; -import { AlternateLinkDescriptor } from "../types/alternative-urls-types"; - -function AlternateLink({ - descriptor, - ...props -}: { - descriptor: AlternateLinkDescriptor; -} & React.LinkHTMLAttributes) { - if (!descriptor.url) return null; - return ( - - ); -} - -export function AlternatesMetadata({ - alternates, -}: { - alternates: ResolvedMetadata["alternates"]; -}) { - if (!alternates) return null; - const { canonical, languages, media, types } = alternates; - return ( - <> - {canonical - ? - : null} - {languages - ? Object.entries(languages).map(([locale, descriptors]) => { - return descriptors?.map((descriptor, index) => ( - - )); - }) - : null} - {media - ? Object.entries(media).map(([mediaName, descriptors]) => - descriptors?.map((descriptor, index) => ( - - )) - ) - : null} - {types - ? Object.entries(types).map(([type, descriptors]) => - descriptors?.map((descriptor, index) => ( - - )) - ) - : null} - - ); -} diff --git a/framework/meta/nextjs/generate/basic.tsx b/framework/meta/nextjs/generate/basic.tsx deleted file mode 100644 index 9585d1b..0000000 --- a/framework/meta/nextjs/generate/basic.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import type { ResolvedMetadata } from "../types/metadata-interface"; - -import React from "react"; -import { Meta, MultiMeta } from "./meta"; - -export function BasicMetadata({ metadata }: { metadata: ResolvedMetadata }) { - return ( - <> - - {metadata.title !== null && metadata.title.absolute - ? {metadata.title.absolute} - : null} - - - {metadata.authors - ? metadata.authors.map((author, index) => ( - - {author.url && } - - - )) - : null} - {metadata.manifest - ? - : null} - - - - {metadata.themeColor - ? metadata.themeColor.map((themeColor, index) => ( - - )) - : null} - - - - - - - - {metadata.archives - ? metadata.archives.map((archive) => ( - - )) - : null} - {metadata.assets - ? metadata.assets.map((asset) => ( - - )) - : null} - {metadata.bookmarks - ? metadata.bookmarks.map((bookmark) => ( - - )) - : null} - - - {metadata.other - ? Object.entries(metadata.other).map(([name, content]) => ( - - )) - : null} - - ); -} - -export function ItunesMeta({ itunes }: { itunes: ResolvedMetadata["itunes"] }) { - if (!itunes) return null; - const { appId, appArgument } = itunes; - let content = `app-id=${appId}`; - if (appArgument) { - content += `, app-argument=${appArgument}`; - } - return ; -} - -const formatDetectionKeys = [ - "telephone", - "date", - "address", - "email", - "url", -] as const; -export function FormatDetectionMeta({ - formatDetection, -}: { - formatDetection: ResolvedMetadata["formatDetection"]; -}) { - if (!formatDetection) return null; - let content = ""; - for (const key of formatDetectionKeys) { - if (key in formatDetection) { - if (content) content += ", "; - content += `${key}=no`; - } - } - return ; -} - -export function AppleWebAppMeta({ - appleWebApp, -}: { - appleWebApp: ResolvedMetadata["appleWebApp"]; -}) { - if (!appleWebApp) return null; - const { capable, title, startupImage, statusBarStyle } = appleWebApp; - - return ( - <> - {capable - ? - : null} - - {startupImage - ? startupImage.map((image, index) => ( - - )) - : null} - {statusBarStyle - ? ( - - ) - : null} - - ); -} - -export function VerificationMeta({ - verification, -}: { - verification: ResolvedMetadata["verification"]; -}) { - if (!verification) return null; - - return ( - <> - - - - - {verification.other - ? Object.entries(verification.other).map(([key, value], index) => ( - - )) - : null} - - ); -} diff --git a/framework/meta/nextjs/generate/icons.tsx b/framework/meta/nextjs/generate/icons.tsx deleted file mode 100644 index af54eb1..0000000 --- a/framework/meta/nextjs/generate/icons.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { ResolvedMetadata } from "../types/metadata-interface"; -import type { Icon, IconDescriptor } from "../types/metadata-types"; - -import React from "react"; - -function IconDescriptorLink({ icon }: { icon: IconDescriptor }) { - const { url, rel = "icon", ...props } = icon; - - return ; -} - -function IconLink({ rel, icon }: { rel?: string; icon: Icon }) { - if (typeof icon === "object" && !(icon instanceof URL)) { - if (rel) icon.rel = rel; - return ; - } else { - const href = icon.toString(); - return ; - } -} - -export function IconsMetadata({ icons }: { icons: ResolvedMetadata["icons"] }) { - if (!icons) return null; - - const shortcutList = icons.shortcut; - const iconList = icons.icon; - const appleList = icons.apple; - const otherList = icons.other; - - return ( - <> - {shortcutList - ? shortcutList.map((icon, index) => ( - - )) - : null} - {iconList - ? iconList.map((icon, index) => ( -