diff --git a/lib/plugins/mdx/deno.jsonc b/lib/plugins/mdx/deno.jsonc new file mode 100644 index 000000000..77c16ec29 --- /dev/null +++ b/lib/plugins/mdx/deno.jsonc @@ -0,0 +1,15 @@ +{ + "lock": false, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "tasks": { + "check:types": "deno check **/*.ts && deno check **/*.tsx", + "coverage": "rm -rf coverage && deno test -A --parallel --no-check --coverage && deno coverage --html", + "fixture": "deno run -A --watch=static/,routes/ tests/fixture/dev.ts", + "ok": "deno fmt --check && deno lint && deno task check:types && deno task test", + "test": "deno test -A --parallel --no-check" + }, + "importMap": "./.vscode/import_map.json" +} diff --git a/lib/plugins/mdx/plugin.ts b/lib/plugins/mdx/mod.ts similarity index 58% rename from lib/plugins/mdx/plugin.ts rename to lib/plugins/mdx/mod.ts index 585022233..189aa860f 100644 --- a/lib/plugins/mdx/plugin.ts +++ b/lib/plugins/mdx/mod.ts @@ -2,8 +2,9 @@ import type { Plugin, PluginRoute } from "../../deps/$fresh/server.ts"; import type { NetzoState } from "../../mod.ts"; import { mdxPathsToRoutes, scanForMDXFiles } from "./utils.ts"; -// deno-lint-ignore ban-types -export type MdxConfig = {}; +export type MdxConfig = { + configLocation: string; +}; // deno-lint-ignore ban-types export type MdxState = {}; @@ -14,14 +15,13 @@ export type MdxState = {}; * @param {MdxConfig} - configuration options for the plugin * @returns {Plugin} - a Plugin for Deno Fresh */ -export const mdx = async (_config: MdxConfig): Promise< - Plugin -> => { - const mdxFiles = await scanForMDXFiles("routes"); - const routes: PluginRoute[] = await mdxPathsToRoutes(mdxFiles); +export async function mdx(config: MdxConfig): Promise> { + const routesDir = new URL("./routes", config.configLocation).pathname; + const mdxFiles = await scanForMDXFiles(routesDir); + const routes: PluginRoute[] = await mdxPathsToRoutes(mdxFiles, routesDir); return { name: "mdx", routes, }; -}; +} diff --git a/lib/plugins/mdx/tests/fixture/components/Bar.tsx b/lib/plugins/mdx/tests/fixture/components/Bar.tsx new file mode 100644 index 000000000..9f79462ab --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/components/Bar.tsx @@ -0,0 +1,3 @@ +export default function Bar() { + return
Bar
; +} diff --git a/lib/plugins/mdx/tests/fixture/deno.json b/lib/plugins/mdx/tests/fixture/deno.json new file mode 100644 index 000000000..6939e41cd --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/deno.json @@ -0,0 +1,39 @@ +{ + "lock": false, + "tasks": { + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", + "manifest": "deno task cli manifest $(pwd)", + "start": "deno run -A --watch=static/,routes/ dev.ts", + "build": "deno run -A dev.ts build", + "preview": "deno run -A main.ts", + "update": "deno run -A -r https://fresh.deno.dev/update ." + }, + "lint": { + "rules": { + "tags": [ + "fresh", + "recommended" + ] + } + }, + "exclude": [ + "**/_fresh/*" + ], + "imports": { + "$fresh/": "https://deno.land/x/fresh@1.6.5/", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", + "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", + "tailwindcss": "npm:tailwindcss@3.4.1", + "tailwindcss/": "npm:/tailwindcss@3.4.1/", + "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js", + "$std/": "https://deno.land/std@0.211.0/" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "nodeModulesDir": true +} diff --git a/lib/plugins/mdx/tests/fixture/dev.ts b/lib/plugins/mdx/tests/fixture/dev.ts new file mode 100755 index 000000000..ae73946d7 --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/dev.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; +import config from "./fresh.config.ts"; + +import "$std/dotenv/load.ts"; + +await dev(import.meta.url, "./main.ts", config); diff --git a/lib/plugins/mdx/tests/fixture/fresh.config.ts b/lib/plugins/mdx/tests/fixture/fresh.config.ts new file mode 100644 index 000000000..ec01377ab --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/fresh.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "$fresh/server.ts"; +import tailwind from "$fresh/plugins/tailwind.ts"; +import { mdx } from "../../mod.ts"; + +export default defineConfig({ + plugins: [await mdx({ configLocation: import.meta.url }), tailwind()], +}); diff --git a/lib/plugins/mdx/tests/fixture/fresh.gen.ts b/lib/plugins/mdx/tests/fixture/fresh.gen.ts new file mode 100644 index 000000000..473ddeb4e --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/fresh.gen.ts @@ -0,0 +1,19 @@ +// DO NOT EDIT. This file is generated by Fresh. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $_app from "./routes/_app.tsx"; +import * as $Counter from "./islands/Counter.tsx"; +import { type Manifest } from "$fresh/server.ts"; + +const manifest = { + routes: { + "./routes/_app.tsx": $_app, + }, + islands: { + "./islands/Counter.tsx": $Counter, + }, + baseUrl: import.meta.url, +} satisfies Manifest; + +export default manifest; diff --git a/lib/plugins/mdx/tests/fixture/islands/Counter.tsx b/lib/plugins/mdx/tests/fixture/islands/Counter.tsx new file mode 100644 index 000000000..9a325f396 --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/islands/Counter.tsx @@ -0,0 +1,20 @@ +import { Signal } from "@preact/signals"; +import { IS_BROWSER } from "$fresh/runtime.ts"; + +interface CounterProps { + count: Signal; +} + +export default function Counter({ count }: CounterProps) { + return ( +
+

{count}

+ + +
+ ); +} diff --git a/lib/plugins/mdx/tests/fixture/main.ts b/lib/plugins/mdx/tests/fixture/main.ts new file mode 100644 index 000000000..675f529bb --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/main.ts @@ -0,0 +1,13 @@ +/// +/// +/// +/// +/// + +import "$std/dotenv/load.ts"; + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; +import config from "./fresh.config.ts"; + +await start(manifest, config); diff --git a/lib/plugins/mdx/tests/fixture/routes/_app.tsx b/lib/plugins/mdx/tests/fixture/routes/_app.tsx new file mode 100644 index 000000000..d34729ecc --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/routes/_app.tsx @@ -0,0 +1,16 @@ +import { type PageProps } from "$fresh/server.ts"; +export default function App({ Component }: PageProps) { + return ( + + + + + fixture + {/* */} + + + + + + ); +} diff --git a/lib/plugins/mdx/tests/fixture/routes/foo.mdx b/lib/plugins/mdx/tests/fixture/routes/foo.mdx new file mode 100644 index 000000000..44fd5e4bf --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/routes/foo.mdx @@ -0,0 +1,18 @@ +--- +title: foobar +--- + +import Foo from "../components/Bar.tsx" +import { useSignal } from "https://esm.sh/*@preact/signals@1.2.2"; +import Counter from "../islands/Counter.tsx"; + +# Hello + +This is rendered from mdx! + +* one +* two +* three + + + \ No newline at end of file diff --git a/lib/plugins/mdx/tests/fixture/static/favicon.ico b/lib/plugins/mdx/tests/fixture/static/favicon.ico new file mode 100644 index 000000000..1cfaaa219 Binary files /dev/null and b/lib/plugins/mdx/tests/fixture/static/favicon.ico differ diff --git a/lib/plugins/mdx/tests/fixture/static/styles.css b/lib/plugins/mdx/tests/fixture/static/styles.css new file mode 100644 index 000000000..bd6213e1d --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/static/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/lib/plugins/mdx/tests/fixture/tailwind.config.ts b/lib/plugins/mdx/tests/fixture/tailwind.config.ts new file mode 100644 index 000000000..99dba8f84 --- /dev/null +++ b/lib/plugins/mdx/tests/fixture/tailwind.config.ts @@ -0,0 +1,7 @@ +import { type Config } from "tailwindcss"; + +export default { + content: [ + "{routes,islands,components}/**/*.{ts,tsx}", + ], +} satisfies Config; diff --git a/lib/plugins/mdx/utils.ts b/lib/plugins/mdx/utils.ts index 6d5a780a6..cc6ed1951 100644 --- a/lib/plugins/mdx/utils.ts +++ b/lib/plugins/mdx/utils.ts @@ -1,36 +1,105 @@ import { compile } from "npm:@mdx-js/mdx"; +import { type MdxjsEsm } from "npm:mdast-util-mdxjs-esm@2.0.1"; import { default as remarkFrontmatter } from "npm:remark-frontmatter@5.0.0"; import { default as remarkGfm } from "npm:remark-gfm@4.0.0"; import { type PluginRoute } from "../../deps/$fresh/server.ts"; import { walk } from "../../deps/std/fs/walk.ts"; +import { visit } from "npm:unist-util-visit@4.1.2"; +import { Root } from "npm:@types/hast@3.0.3"; +import { Project } from "npm:ts-morph@21.0.1"; +import { join, toFileUrl } from "../../deps/std/path/mod.ts"; +import { ImportDeclaration } from "npm:@types/estree-jsx@1.0.5"; -export async function scanForMDXFiles(_directory: string): Promise { +export async function scanForMDXFiles(directory: string): Promise { const files: string[] = []; for await ( - const entry of walk( - "/home/mrk/repos/netzo/templates/minimal/routes", - { includeDirs: false, exts: [".mdx"] }, - ) + const entry of walk(directory, { includeDirs: false, exts: [".mdx"] }) ) { files.push(entry.path); } return files; } -export function mdxPathsToRoutes(mdxPaths: string[]): Promise { +export function mdxPathsToRoutes( + mdxPaths: string[], + routesDir: string, +): Promise { return Promise.all(mdxPaths.map(async (path: string) => { const routePath = path.split("/routes/").pop()!.replace(".mdx", ""); const file = Deno.readTextFileSync(path); const compiled = await compile(file, { - remarkPlugins: [remarkGfm, remarkFrontmatter], + rehypePlugins: [rehypeLogger], + remarkPlugins: [remarkGfm, remarkFrontmatter, [ + remarkAbsoluteImportPaths, + { basePath: routesDir }, + ], remarkLogger], jsxImportSource: "preact", }); const result = compiled.toString(); + // console.log({ file, result }); const code = await import("data:text/javascript," + result); const comp = code.default; return { path: routePath, component: comp } satisfies PluginRoute; })); } + +type RemarkAbsoluteImportPathsOptions = { + basePath?: string; +}; + +function remarkAbsoluteImportPaths( + options: Readonly, +): (tree: Root) => void { + const basePath = options.basePath || ""; + + return function (tree: Root) { + visit(tree, "mdxjsEsm", (node: MdxjsEsm) => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile("tempFile.ts", node.value); + const imports = sourceFile.getImportDeclarations(); + + imports.forEach((importDeclaration) => { + const originalSpecifier = importDeclaration.getModuleSpecifierValue(); + if (originalSpecifier.startsWith("http")) return; + const updatedPath = join(basePath, originalSpecifier); + + const newSpecifier = toFileUrl(updatedPath).href; + + if (node.data?.estree) { + const estreeBody = node.data.estree.body as ImportDeclaration[]; + const importNode = estreeBody.find((n) => + n.type === "ImportDeclaration" && + n.source.type === "Literal" && + n.source.value === originalSpecifier + ); + if (importNode) { + importNode.source.value = newSpecifier; + importNode.source.raw = `"${newSpecifier}"`; + } + } + }); + }); + }; +} + +function remarkLogger(): (tree: Root) => undefined { + return function (tree: Root) { + visit(tree, "root", (node: Root) => { + // console.log(node); + }); + }; +} + +function rehypeLogger(): (tree: Root) => undefined { + return function (tree: Root) { + visit(tree, "root", (node: Element) => { + // console.log(node); + }); + }; +}