From c6dfcffde87556eb5fcf258ed12f9626f45de15c Mon Sep 17 00:00:00 2001 From: Brock Donahue Date: Mon, 19 Jun 2023 15:43:20 -0400 Subject: [PATCH 1/6] familiarized myself with the repo --- dev.ts | 2 +- examples/react-app/main.ts | 2 + examples/react-app/routes/todos.tsx | 234 ++++++----- examples/react-app/server.ts | 10 +- framework/react/markup.ts | 23 + init.ts | 622 +++++++++++++++------------- server/deps.ts | 27 +- server/dev.ts | 362 +++++++++------- 8 files changed, 712 insertions(+), 570 deletions(-) create mode 100644 framework/react/markup.ts diff --git a/dev.ts b/dev.ts index 8216797a1..f0f52e19d 100644 --- a/dev.ts +++ b/dev.ts @@ -1,5 +1,5 @@ import dev from "./server/dev.ts"; if (import.meta.main) { - dev(Deno.args[0]); + dev(Deno.args); } diff --git a/examples/react-app/main.ts b/examples/react-app/main.ts index 81010ad7e..96199776a 100644 --- a/examples/react-app/main.ts +++ b/examples/react-app/main.ts @@ -1,3 +1,5 @@ +/** @format */ + import { bootstrap } from "aleph/react"; bootstrap(); diff --git a/examples/react-app/routes/todos.tsx b/examples/react-app/routes/todos.tsx index c43e2b5ed..fa224e4b3 100644 --- a/examples/react-app/routes/todos.tsx +++ b/examples/react-app/routes/todos.tsx @@ -1,119 +1,149 @@ +/** @format */ + import type { FormEvent } from "react"; import { Head, useData } from "aleph/react"; type Todo = { - id: number; - message: string; - completed: boolean; + id: number; + message: string; + completed: boolean; }; const store = { - todos: JSON.parse(window.localStorage?.getItem("todos") || "[]") as Todo[], - save() { - localStorage?.setItem("todos", JSON.stringify(this.todos)); - }, + todos: JSON.parse(window.localStorage?.getItem("todos") || "[]") as Todo[], + save() { + localStorage?.setItem("todos", JSON.stringify(this.todos)); + }, }; -export function data() { - return Response.json(store); -} +export const data = { + defer: false, + fetch() { + return Response.json(store); + }, +}; export async function mutation(req: Request): Promise { - const { id, message, completed } = await req.json(); - switch (req.method) { - case "PUT": { - store.todos.push({ id: Date.now(), message, completed: false }); - store.save(); - break; - } - case "PATCH": { - const todo = store.todos.find((todo) => todo.id === id); - if (todo) { - todo.completed = completed; - store.save(); - } - break; - } - case "DELETE": { - store.todos = store.todos.filter((todo) => todo.id !== id); - store.save(); - } - } - return Response.json(store); + const { id, message, completed } = await req.json(); + switch (req.method) { + case "PUT": { + store.todos.push({ id: Date.now(), message, completed: false }); + store.save(); + break; + } + case "PATCH": { + const todo = store.todos.find(todo => todo.id === id); + if (todo) { + todo.completed = completed; + store.save(); + } + break; + } + case "DELETE": { + store.todos = store.todos.filter(todo => todo.id !== id); + store.save(); + } + } + return Response.json(store); } export default function Todos() { - const { data: { todos }, isMutating, mutation } = useData<{ todos: Todo[] }>(); + const { + data: { todos }, + isMutating, + mutation, + } = useData<{ todos: Todo[] }>(); - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const fd = new FormData(form); - const message = fd.get("message")?.toString().trim(); - if (message) { - await mutation.put({ message }, { - // optimistic update data without waiting for the server response - optimisticUpdate: (data) => { - return { - todos: [...data.todos, { id: 0, message, completed: false }], - }; - }, - // replace the data with the new data that is from the server response - replace: true, - }); - setTimeout(() => form.querySelector("input")?.focus(), 0); - form.reset(); - } - }; + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const fd = new FormData(form); + const message = fd.get("message")?.toString().trim(); + if (message) { + await mutation.put( + { message }, + { + // optimistic update data without waiting for the server response + optimisticUpdate: data => { + return { + todos: [ + ...data.todos, + { id: 0, message, completed: false }, + ], + }; + }, + // replace the data with the new data that is from the server response + replace: true, + } + ); + setTimeout(() => form.querySelector("input")?.focus(), 0); + form.reset(); + } + }; - return ( -
- - Todos - - -

- Todos - {todos.length > 0 && {todos.filter((todo) => todo.completed).length}/{todos.length}} -

-
    - {todos.map((todo) => ( -
  • - mutation.patch({ id: todo.id, completed: !todo.completed }, "replace")} - /> - - {todo.id > 0 && ( - - )} -
  • - ))} -
-
- -
-
- ); + return ( +
+ + Todos + + +

+ Todos + {todos.length > 0 && ( + + {todos.filter(todo => todo.completed).length}/ + {todos.length} + + )} +

+
    + {todos.map(todo => ( +
  • + + mutation.patch( + { id: todo.id, completed: !todo.completed }, + "replace" + ) + } + /> + + {todo.id > 0 && ( + + )} +
  • + ))} +
+
+ +
+
+ ); } diff --git a/examples/react-app/server.ts b/examples/react-app/server.ts index daf5e7313..815aa7b86 100644 --- a/examples/react-app/server.ts +++ b/examples/react-app/server.ts @@ -1,11 +1,13 @@ +/** @format */ + import { serve } from "aleph/server"; import react from "aleph/plugins/react"; import denoDeploy from "aleph/plugins/deploy"; import modules from "./routes/_export.ts"; serve({ - plugins: [ - denoDeploy({ moduleMain: import.meta.url, modules }), - react({ ssr: true }), - ], + plugins: [ + denoDeploy({ moduleMain: import.meta.url, modules }), + react({ ssr: true }), + ], }); diff --git a/framework/react/markup.ts b/framework/react/markup.ts new file mode 100644 index 000000000..4e91a3855 --- /dev/null +++ b/framework/react/markup.ts @@ -0,0 +1,23 @@ +/** @format */ + +const ESCAPE_LOOKUP: { [match: string]: string } = { + "&": "\\u0026", + ">": "\\u003e", + "<": "\\u003c", + "\u2028": "\\u2028", + "\u2029": "\\u2029", +}; + +const ESCAPE_REGEX = /[&><\u2028\u2029]/g; + +export function escapeHtml(html: string) { + return html.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); +} + +export interface SafeHtml { + __html: string; +} + +export function createHtml(html: string): SafeHtml { + return { __html: html }; +} diff --git a/init.ts b/init.ts index b7f552a24..79477d3d9 100644 --- a/init.ts +++ b/init.ts @@ -1,343 +1,379 @@ -import { Untar } from "https://deno.land/std@0.180.0/archive/untar.ts"; -import { parse } from "https://deno.land/std@0.180.0/flags/mod.ts"; -import { blue, bold, cyan, dim, green, red } from "https://deno.land/std@0.180.0/fmt/colors.ts"; -import { copy as copyDir } from "https://deno.land/std@0.180.0/fs/copy.ts"; -import { copy } from "https://deno.land/std@0.180.0/streams/copy.ts"; -import { readerFromStreamReader } from "https://deno.land/std@0.180.0/streams/reader_from_stream_reader.ts"; -import { ensureDir } from "https://deno.land/std@0.180.0/fs/ensure_dir.ts"; -import { basename, join } from "https://deno.land/std@0.180.0/path/mod.ts"; +/** @format */ -const templates = [ - "react", - "react-mdx", - "yew", - "leptos", - "api", -]; +import { Untar } from "https://deno.land/std@0.192.0/archive/untar.ts"; +import { parse } from "https://deno.land/std@0.192.0/flags/mod.ts"; +import { + blue, + bold, + cyan, + dim, + green, + red, +} from "https://deno.land/std@0.192.0/fmt/colors.ts"; +import { copy as copyDir } from "https://deno.land/std@0.192.0/fs/copy.ts"; +import { copy } from "https://deno.land/std@0.192.0/streams/copy.ts"; +import { readerFromStreamReader } from "https://deno.land/std@0.192.0/streams/reader_from_stream_reader.ts"; +import { ensureDir } from "https://deno.land/std@0.192.0/fs/ensure_dir.ts"; +import { basename, join } from "https://deno.land/std@0.192.0/path/mod.ts"; -const rsApps = [ - "yew", - "leptos", -]; +const templates = ["react", "react-mdx", "yew", "leptos", "api"]; -const unocssApps = [ - "react", - "yew", - "leptos", -]; +const rsApps = ["yew", "leptos"]; + +const unocssApps = ["react", "yew", "leptos"]; const versions = { - react: "18.2.0", + react: "18.2.0", }; type Options = { - template?: string; + template?: string; }; export default async function init(nameArg?: string, options?: Options) { - let { template } = options || {}; + let { template } = options || {}; - // get and check the project name - const name = nameArg ?? await ask("Project Name:"); - if (!name) { - console.error(`${red("!")} Please entry project name.`); - Deno.exit(1); - } + // get and check the project name + const name = nameArg ?? (await ask("Project Name:")); + if (!name) { + console.error(`${red("!")} Please entry project name.`); + Deno.exit(1); + } - if (template && !(templates.includes(template))) { - console.error( - `${red("!")} Invalid template name ${red(template)}, must be one of [${blue(templates.join(","))}]`, - ); - Deno.exit(1); - } + if (template && !templates.includes(template)) { + console.error( + `${red("!")} Invalid template name ${red( + template + )}, must be one of [${blue(templates.join(","))}]` + ); + Deno.exit(1); + } - // check the dir is clean - if ( - !(await isFolderEmpty(Deno.cwd(), name)) && - !(await confirm(`Folder ${blue(name)} already exists, continue?`)) - ) { - Deno.exit(1); - } + // check the dir is clean + if ( + !(await isFolderEmpty(Deno.cwd(), name)) && + !(await confirm(`Folder ${blue(name)} already exists, continue?`)) + ) { + Deno.exit(1); + } - if (!template) { - const answer = await ask([ - "Select a framework:", - ...templates.map((name, i) => ` ${bold((i + 1).toString())}. ${getTemplateDisplayName(name)}`), - dim(`[1-${templates.length}]`), - ].join("\n")); - const n = parseInt(answer); - if (!isNaN(n) && n > 0 && n <= templates.length) { - template = templates[n - 1]; - } else { - console.error(`${red("!")} Please entry ${cyan(`[1-${templates.length}]`)}.`); - Deno.exit(1); - } - } + if (!template) { + const answer = await ask( + [ + "Select a framework:", + ...templates.map( + (name, i) => + ` ${bold( + (i + 1).toString() + )}. ${getTemplateDisplayName(name)}` + ), + dim(`[1-${templates.length}]`), + ].join("\n") + ); + const n = parseInt(answer); + if (!isNaN(n) && n > 0 && n <= templates.length) { + template = templates[n - 1]; + } else { + console.error( + `${red("!")} Please entry ${cyan(`[1-${templates.length}]`)}.` + ); + Deno.exit(1); + } + } - const appDir = join(Deno.cwd(), name); - const withUnocss = unocssApps.includes(template!) && await confirm("Use Atomic CSS (powered by Unocss)?"); - const withVscode = await confirm("Initialize VS Code workspace configuration?"); - const deploy = !rsApps.includes(template) ? await confirm("Deploy to Deno Deploy?") : false; - const isRsApp = rsApps.includes(template); + const appDir = join(Deno.cwd(), name); + const withUnocss = + unocssApps.includes(template!) && + (await confirm("Use Atomic CSS (powered by Unocss)?")); + const withVscode = await confirm( + "Initialize VS Code workspace configuration?" + ); + const deploy = !rsApps.includes(template) + ? await confirm("Deploy to Deno Deploy?") + : false; + const isRsApp = rsApps.includes(template); - let alephPkgUri: string; - if (import.meta.url.startsWith("file://")) { - const src = `examples/${withUnocss ? "with-unocss/" : ""}${template}-app/`; - await copyDir(src, name); - alephPkgUri = ".."; - } else { - console.log(`${dim("↓")} Downloading template(${blue(template!)}), this might take a moment...`); - const res = await fetch("https://cdn.deno.land/aleph/meta/versions.json"); - if (res.status !== 200) { - console.error(await res.text()); - Deno.exit(1); - } - const { latest: VERSION } = await res.json(); - const repo = "alephjs/aleph.js"; - const resp = await fetch( - `https://codeload.github.com/${repo}/tar.gz/refs/tags/${VERSION}`, - ); - if (resp.status !== 200) { - console.error(await resp.text()); - Deno.exit(1); - } - // deno-lint-ignore ban-ts-comment - // @ts-ignore - const gz = new DecompressionStream("gzip"); - const entryList = new Untar( - readerFromStreamReader(resp.body!.pipeThrough(gz).getReader()), - ); - const prefix = `${basename(repo)}-${VERSION}/examples/${withUnocss ? "with-unocss/" : ""}${template}-app/`; - for await (const entry of entryList) { - if (entry.fileName.startsWith(prefix) && !entry.fileName.endsWith("/README.md")) { - const fp = join(appDir, trimPrefix(entry.fileName, prefix)); - if (entry.type === "directory") { - await ensureDir(fp); - continue; - } - const file = await Deno.open(fp, { write: true, create: true }); - await copy(entry, file); - } - } - alephPkgUri = `https://deno.land/x/aleph@${VERSION}`; - } + let alephPkgUri: string; + if (import.meta.url.startsWith("file://")) { + const src = `examples/${ + withUnocss ? "with-unocss/" : "" + }${template}-app/`; + await copyDir(src, name); + alephPkgUri = ".."; + } else { + console.log( + `${dim("↓")} Downloading template(${blue( + template! + )}), this might take a moment...` + ); + const res = await fetch( + "https://cdn.deno.land/aleph/meta/versions.json" + ); + if (res.status !== 200) { + console.error(await res.text()); + Deno.exit(1); + } + const { latest: VERSION } = await res.json(); + const repo = "alephjs/aleph.js"; + const resp = await fetch( + `https://codeload.github.com/${repo}/tar.gz/refs/tags/${VERSION}` + ); + if (resp.status !== 200) { + console.error(await resp.text()); + Deno.exit(1); + } + // deno-lint-ignore ban-ts-comment + // @ts-ignore + const gz = new DecompressionStream("gzip"); + const entryList = new Untar( + readerFromStreamReader( + resp.body!.pipeThrough(gz).getReader() + ) + ); + const prefix = `${basename(repo)}-${VERSION}/examples/${ + withUnocss ? "with-unocss/" : "" + }${template}-app/`; + for await (const entry of entryList) { + if ( + entry.fileName.startsWith(prefix) && + !entry.fileName.endsWith("/README.md") + ) { + const fp = join(appDir, trimPrefix(entry.fileName, prefix)); + if (entry.type === "directory") { + await ensureDir(fp); + continue; + } + const file = await Deno.open(fp, { write: true, create: true }); + await copy(entry, file); + } + } + alephPkgUri = `https://deno.land/x/aleph@${VERSION}`; + } - const serverCode = await Deno.readTextFile(join(appDir, "server.ts")); - if (!isRsApp && !deploy) { - await Deno.writeTextFile( - join(appDir, "server.ts"), - serverCode - .replace('import modules from "./routes/_export.ts";\n', "") - .replace('import denoDeploy from "aleph/plugins/deploy";\n', "") - .replace(" denoDeploy({ moduleMain: import.meta.url, modules }),\n", "") - .replace(" denoDeploy({ modules }),\n", ""), - ); - await Deno.remove(join(appDir, "routes/_export.ts")); - } else { - await Deno.writeTextFile( - join(appDir, "server.ts"), - serverCode.replace( - "denoDeploy({ moduleMain: import.meta.url, modules })", - "denoDeploy({ modules })", - ), - ); - } + const serverCode = await Deno.readTextFile(join(appDir, "server.ts")); + if (!isRsApp && !deploy) { + await Deno.writeTextFile( + join(appDir, "server.ts"), + serverCode + .replace('import modules from "./routes/_export.ts";\n', "") + .replace('import denoDeploy from "aleph/plugins/deploy";\n', "") + .replace( + " denoDeploy({ moduleMain: import.meta.url, modules }),\n", + "" + ) + .replace(" denoDeploy({ modules }),\n", "") + ); + await Deno.remove(join(appDir, "routes/_export.ts")); + } else { + await Deno.writeTextFile( + join(appDir, "server.ts"), + serverCode.replace( + "denoDeploy({ moduleMain: import.meta.url, modules })", + "denoDeploy({ modules })" + ) + ); + } - const res = await fetch("https://esm.sh/status.json"); - if (res.status !== 200) { - console.error(await res.text()); - Deno.exit(1); - } - const { version: ESM_VERSION } = await res.json(); - const denoConfig = { - "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "dom.extras", - "deno.ns", - ], - "types": [ - `${alephPkgUri}/types.d.ts`, - ], - }, - "importMap": "import_map.json", - "tasks": { - "dev": await existsFile(join(appDir, "dev.ts")) ? "deno run -A dev.ts" : `deno run -A ${alephPkgUri}/dev.ts`, - "start": "deno run -A server.ts", - "build": "deno run -A server.ts --build", - "esm:add": `deno run -A https://esm.sh/v${ESM_VERSION} add`, - "esm:update": `deno run -A https://esm.sh/v${ESM_VERSION} update`, - "esm:remove": `deno run -A https://esm.sh/v${ESM_VERSION} remove`, - }, - }; - const importMap = { - imports: { - "~/": "./", - "std/": "https://deno.land/std@0.180.0/", - "aleph/": `${alephPkgUri}/`, - "aleph/server": `${alephPkgUri}/server/mod.ts`, - "aleph/dev": `${alephPkgUri}/server/dev.ts`, - } as Record, - scopes: {}, - }; - if (deploy) { - Object.assign(importMap.imports, { - "aleph/plugins/deploy": `${alephPkgUri}/plugins/deploy.ts`, - }); - } - if (withUnocss) { - Object.assign(importMap.imports, { - "aleph/plugins/unocss": `${alephPkgUri}/plugins/unocss.ts`, - "@unocss/core": `https://esm.sh/v${ESM_VERSION}/@unocss/core@0.50.6`, - "@unocss/preset-uno": `https://esm.sh/v${ESM_VERSION}/@unocss/preset-uno@0.50.6`, - }); - } - switch (template) { - case "react-mdx": - Object.assign(importMap.imports, { - "aleph/plugins/mdx": `${alephPkgUri}/plugins/mdx.ts`, - "@mdx-js/react": `https://esm.sh/v${ESM_VERSION}/@mdx-js/react@2.3.0`, - }); - /* falls through */ - case "react": { - Object.assign(denoConfig.compilerOptions, { - "jsx": "react-jsx", - "jsxImportSource": `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, - }); - Object.assign(importMap.imports, { - "aleph/react": `${alephPkgUri}/framework/react/mod.ts`, - "aleph/plugins/react": `${alephPkgUri}/framework/react/plugin.ts`, - "react": `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, - "react-dom": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}`, - "react-dom/": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}/`, - }); - break; - } - } + const res = await fetch("https://esm.sh/status.json"); + if (res.status !== 200) { + console.error(await res.text()); + Deno.exit(1); + } + const { version: ESM_VERSION } = await res.json(); + const denoConfig = { + compilerOptions: { + lib: ["dom", "dom.iterable", "dom.extras", "deno.ns"], + types: [`${alephPkgUri}/types.d.ts`], + }, + importMap: "import_map.json", + tasks: { + dev: (await existsFile(join(appDir, "dev.ts"))) + ? "deno run -A dev.ts" + : `deno run -A ${alephPkgUri}/dev.ts`, + start: "deno run -A server.ts", + build: "deno run -A server.ts --build", + "esm:add": `deno run -A https://esm.sh/v${ESM_VERSION} add`, + "esm:update": `deno run -A https://esm.sh/v${ESM_VERSION} update`, + "esm:remove": `deno run -A https://esm.sh/v${ESM_VERSION} remove`, + }, + }; + const importMap = { + imports: { + "~/": "./", + "std/": "https://deno.land/std@0.180.0/", + "aleph/": `${alephPkgUri}/`, + "aleph/server": `${alephPkgUri}/server/mod.ts`, + "aleph/dev": `${alephPkgUri}/server/dev.ts`, + } as Record, + scopes: {}, + }; + if (deploy) { + Object.assign(importMap.imports, { + "aleph/plugins/deploy": `${alephPkgUri}/plugins/deploy.ts`, + }); + } + if (withUnocss) { + Object.assign(importMap.imports, { + "aleph/plugins/unocss": `${alephPkgUri}/plugins/unocss.ts`, + "@unocss/core": `https://esm.sh/v${ESM_VERSION}/@unocss/core@0.50.6`, + "@unocss/preset-uno": `https://esm.sh/v${ESM_VERSION}/@unocss/preset-uno@0.50.6`, + }); + } + switch (template) { + case "react-mdx": + Object.assign(importMap.imports, { + "aleph/plugins/mdx": `${alephPkgUri}/plugins/mdx.ts`, + "@mdx-js/react": `https://esm.sh/v${ESM_VERSION}/@mdx-js/react@2.3.0`, + }); + /* falls through */ + case "react": { + Object.assign(denoConfig.compilerOptions, { + jsx: "react-jsx", + jsxImportSource: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, + }); + Object.assign(importMap.imports, { + "aleph/react": `${alephPkgUri}/framework/react/mod.ts`, + "aleph/plugins/react": `${alephPkgUri}/framework/react/plugin.ts`, + react: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, + "react-dom": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}`, + "react-dom/": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}/`, + }); + break; + } + } - await ensureDir(appDir); - await Promise.all([ - Deno.writeTextFile( - join(appDir, "deno.json"), - JSON.stringify(denoConfig, undefined, 2), - ), - Deno.writeTextFile( - join(appDir, "import_map.json"), - JSON.stringify(importMap, undefined, 2), - ), - ]); + await ensureDir(appDir); + await Promise.all([ + Deno.writeTextFile( + join(appDir, "deno.json"), + JSON.stringify(denoConfig, undefined, 2) + ), + Deno.writeTextFile( + join(appDir, "import_map.json"), + JSON.stringify(importMap, undefined, 2) + ), + ]); - if (withVscode) { - const settings = { - "deno.enable": true, - "deno.lint": true, - "deno.config": "./deno.json", - }; - await ensureDir(join(appDir, ".vscode")); - await Deno.writeTextFile( - join(appDir, ".vscode", "settings.json"), - JSON.stringify(settings, undefined, 2), - ); - } + if (withVscode) { + const settings = { + "deno.enable": true, + "deno.lint": true, + "deno.config": "./deno.json", + }; + await ensureDir(join(appDir, ".vscode")); + await Deno.writeTextFile( + join(appDir, ".vscode", "settings.json"), + JSON.stringify(settings, undefined, 2) + ); + } - await Deno.run({ - cmd: [Deno.execPath(), "cache", "--no-lock", "server.ts"], - cwd: appDir, - stderr: "inherit", - stdout: "inherit", - }).status(); + await Deno.run({ + cmd: [Deno.execPath(), "cache", "--no-lock", "server.ts"], + cwd: appDir, + stderr: "inherit", + stdout: "inherit", + }).status(); - console.log([ - "", - green("▲ Aleph.js is ready to go!"), - "", - `${dim("$")} cd ${name}`, - `${dim("$")} deno task dev ${dim("# Start the server in `development` mode")}`, - `${dim("$")} deno task start ${dim("# Start the server in `production` mode")}`, - `${dim("$")} deno task build ${dim("# Build & Optimize the app (bundling, SSG, etc.)")}`, - "", - `Docs: ${cyan("https://alephjs.org/docs")}`, - `Bugs: ${cyan("https://github.com/alephjs/aleph.js/issues")}`, - "", - ].join("\n")); - Deno.exit(0); + console.log( + [ + "", + green("▲ Aleph.js is ready to go!"), + "", + `${dim("$")} cd ${name}`, + `${dim("$")} deno task dev ${dim( + "# Start the server in `development` mode" + )}`, + `${dim("$")} deno task start ${dim( + "# Start the server in `production` mode" + )}`, + `${dim("$")} deno task build ${dim( + "# Build & Optimize the app (bundling, SSG, etc.)" + )}`, + "", + `Docs: ${cyan("https://alephjs.org/docs")}`, + `Bugs: ${cyan("https://github.com/alephjs/aleph.js/issues")}`, + "", + ].join("\n") + ); + Deno.exit(0); } async function isFolderEmpty(root: string, name: string): Promise { - const dir = join(root, name); - if (await existsFile(dir)) { - throw new Error(`Folder ${name} already exists as a file.`); - } - if (await existsDir(dir)) { - for await (const file of Deno.readDir(dir)) { - if (file.name !== ".DS_Store") { - return false; - } - } - } - return true; + const dir = join(root, name); + if (await existsFile(dir)) { + throw new Error(`Folder ${name} already exists as a file.`); + } + if (await existsDir(dir)) { + for await (const file of Deno.readDir(dir)) { + if (file.name !== ".DS_Store") { + return false; + } + } + } + return true; } async function ask(question = ":") { - await Deno.stdout.write(new TextEncoder().encode(cyan("? ") + question + " ")); - const buf = new Uint8Array(1024); - const n = await Deno.stdin.read(buf); - const answer = new TextDecoder().decode(buf.subarray(0, n)); - return answer.trim(); + await Deno.stdout.write( + new TextEncoder().encode(cyan("? ") + question + " ") + ); + const buf = new Uint8Array(1024); + const n = await Deno.stdin.read(buf); + const answer = new TextDecoder().decode(buf.subarray(0, n)); + return answer.trim(); } async function confirm(question = "are you sure?") { - let a: string; - // deno-lint-ignore no-empty - while (!/^(y|n|)$/i.test(a = await ask(question + dim(" [y/N]")))) {} - return a.toLowerCase() === "y"; + let a: string; + // deno-lint-ignore no-empty + while (!/^(y|n|)$/i.test((a = await ask(question + dim(" [y/N]"))))) {} + return a.toLowerCase() === "y"; } function trimPrefix(s: string, prefix: string): string { - if (prefix !== "" && s.startsWith(prefix)) { - return s.slice(prefix.length); - } - return s; + if (prefix !== "" && s.startsWith(prefix)) { + return s.slice(prefix.length); + } + return s; } function getTemplateDisplayName(name: string) { - if (name === "api") { - return "REST API"; - } - if (name === "react-mdx") { - return "React with MDX"; - } - return name.at(0)?.toUpperCase() + name.slice(1); + if (name === "api") { + return "REST API"; + } + if (name === "react-mdx") { + return "React with MDX"; + } + return name.at(0)?.toUpperCase() + name.slice(1); } /** Check whether or not the given path exists as a directory. */ export async function existsDir(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isDirectory; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isDirectory; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } /** Check whether or not the given path exists as regular file. */ export async function existsFile(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isFile; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isFile; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } if (import.meta.main) { - const { _: args, ...options } = parse(Deno.args); - await init(args[0] as string | undefined, options); + const { _: args, ...options } = parse(Deno.args); + await init(args[0] as string | undefined, options); } diff --git a/server/deps.ts b/server/deps.ts index f72a3dd06..630de9559 100644 --- a/server/deps.ts +++ b/server/deps.ts @@ -1,19 +1,28 @@ +/** @format */ + // deno std -export { concat as concatBytes } from "https://deno.land/std@0.180.0/bytes/mod.ts"; -export { encode as btoa } from "https://deno.land/std@0.180.0/encoding/base64.ts"; -export * as colors from "https://deno.land/std@0.180.0/fmt/colors.ts"; -export { ensureDir } from "https://deno.land/std@0.180.0/fs/ensure_dir.ts"; -export { serve, serveTls } from "https://deno.land/std@0.180.0/http/server.ts"; -export * as path from "https://deno.land/std@0.180.0/path/mod.ts"; -export * as jsonc from "https://deno.land/std@0.180.0/jsonc/mod.ts"; +export { concat as concatBytes } from "https://deno.land/std@0.192.0/bytes/mod.ts"; +export { encode as btoa } from "https://deno.land/std@0.192.0/encoding/base64.ts"; +export * as colors from "https://deno.land/std@0.192.0/fmt/colors.ts"; +export { ensureDir } from "https://deno.land/std@0.192.0/fs/ensure_dir.ts"; +export { serve, serveTls } from "https://deno.land/std@0.192.0/http/server.ts"; +export * as path from "https://deno.land/std@0.192.0/path/mod.ts"; +export * as jsonc from "https://deno.land/std@0.192.0/jsonc/mod.ts"; +export { parse as parseCliArgs } from "https://deno.land/std@0.192.0/flags/mod.ts"; // third-party // @deno-types="https://deno.land/x/esbuild@v0.17.12/mod.d.ts" export * as esbuild from "https://deno.land/x/esbuild@v0.17.12/mod.js"; export * from "https://deno.land/x/aleph_compiler@0.9.3/mod.ts"; export * from "https://deno.land/x/aleph_compiler@0.9.3/types.ts"; -export { default as initLolHtml, HTMLRewriter } from "https://deno.land/x/lol_html@0.0.6/mod.ts"; +export { + default as initLolHtml, + HTMLRewriter, +} from "https://deno.land/x/lol_html@0.0.6/mod.ts"; export { default as lolHtmlWasm } from "https://deno.land/x/lol_html@0.0.6/wasm.js"; // npm -export { default as mitt, type Emitter } from "https://esm.sh/mitt@3.0.0?pin=v110"; +export { + default as mitt, + type Emitter, +} from "https://esm.sh/mitt@3.0.0?pin=v110"; diff --git a/server/dev.ts b/server/dev.ts index 6df2741ec..269f1e837 100644 --- a/server/dev.ts +++ b/server/dev.ts @@ -1,191 +1,231 @@ +/** @format */ + import { isFilledString } from "../shared/util.ts"; -import { colors, Emitter, ensureDir, mitt, parseDeps, path } from "./deps.ts"; +import { + colors, + Emitter, + ensureDir, + mitt, + parseDeps, + path, + parseCliArgs, +} from "./deps.ts"; import depGraph from "./graph.ts"; -import { builtinModuleExts, findFile, getAlephConfig, getImportMap, watchFs } from "./helpers.ts"; +import { + builtinModuleExts, + findFile, + getAlephConfig, + getImportMap, + watchFs, +} from "./helpers.ts"; import log from "./log.ts"; import { initRouter, toRouterRegExp } from "./router.ts"; import type { AlephConfig } from "./types.ts"; type WatchFsEvents = { - [key in "create" | "remove" | "modify" | `modify:${string}` | `hotUpdate:${string}`]: { - specifier: string; - }; + [key in + | "create" + | "remove" + | "modify" + | `modify:${string}` + | `hotUpdate:${string}`]: { + specifier: string; + }; }; const watchFsEmitters = new Set>(); /** Create a `watchFs` emitter. */ export function createWatchFsEmitter() { - const e = mitt(); - watchFsEmitters.add(e); - return e; + const e = mitt(); + watchFsEmitters.add(e); + return e; } /** Remove the emitter. */ export function removeWatchFsEmitter(e: Emitter) { - e.all.clear(); - watchFsEmitters.delete(e); + e.all.clear(); + watchFsEmitters.delete(e); } /** Watch for file changes. */ export function watch(appDir: string, onRouterChange?: () => void) { - const config = getAlephConfig(); - const emitter = createWatchFsEmitter(); - - emitter.on("*", async (kind, { specifier }) => { - if (kind === "create" || kind === "remove") { - // reload router when fs changess - const reg = toRouterRegExp(config?.router); - if (reg.test(specifier)) { - const router = await initRouter(appDir, config?.router); - Reflect.set(globalThis, "__ALEPH_ROUTER", router); - onRouterChange?.(); - } - } - }); - - if (onRouterChange) { - initRouter(appDir, config?.router).then((router) => { - Reflect.set(globalThis, "__ALEPH_ROUTER", router); - onRouterChange(); - }); - } - - watchFs(appDir, (kind: "create" | "remove" | "modify", pathname: string) => { - const specifier = "./" + path.relative(appDir, pathname).replaceAll("\\", "/"); - // delete global cached index html - if (specifier === "./index.html") { - Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML"); - } - if (kind === "remove") { - depGraph.unmark(specifier); - } else { - depGraph.update(specifier); - } - if (kind === "modify") { - watchFsEmitters.forEach((e) => { - e.emit("modify", { specifier }); - e.emit(`modify:${specifier}`, { specifier }); - if (e.all.has(`hotUpdate:${specifier}`)) { - e.emit(`hotUpdate:${specifier}`, { specifier }); - } else if (specifier !== "./routes/_export.ts") { - depGraph.lookup(specifier, (specifier) => { - if (e.all.has(`hotUpdate:${specifier}`)) { - e.emit(`hotUpdate:${specifier}`, { specifier }); - return false; - } - }); - } - }); - } else { - watchFsEmitters.forEach((e) => e.emit(kind, { specifier })); - } - }); + const config = getAlephConfig(); + const emitter = createWatchFsEmitter(); + + emitter.on("*", async (kind, { specifier }) => { + if (kind === "create" || kind === "remove") { + // reload router when fs changess + const reg = toRouterRegExp(config?.router); + if (reg.test(specifier)) { + const router = await initRouter(appDir, config?.router); + Reflect.set(globalThis, "__ALEPH_ROUTER", router); + onRouterChange?.(); + } + } + }); + + if (onRouterChange) { + initRouter(appDir, config?.router).then(router => { + Reflect.set(globalThis, "__ALEPH_ROUTER", router); + onRouterChange(); + }); + } + + watchFs( + appDir, + (kind: "create" | "remove" | "modify", pathname: string) => { + const specifier = + "./" + path.relative(appDir, pathname).replaceAll("\\", "/"); + // delete global cached index html + if (specifier === "./index.html") { + Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML"); + } + if (kind === "remove") { + depGraph.unmark(specifier); + } else { + depGraph.update(specifier); + } + if (kind === "modify") { + watchFsEmitters.forEach(e => { + e.emit("modify", { specifier }); + e.emit(`modify:${specifier}`, { specifier }); + if (e.all.has(`hotUpdate:${specifier}`)) { + e.emit(`hotUpdate:${specifier}`, { specifier }); + } else if (specifier !== "./routes/_export.ts") { + depGraph.lookup(specifier, specifier => { + if (e.all.has(`hotUpdate:${specifier}`)) { + e.emit(`hotUpdate:${specifier}`, { specifier }); + return false; + } + }); + } + }); + } else { + watchFsEmitters.forEach(e => e.emit(kind, { specifier })); + } + } + ); } let devProcess: Deno.Process | null = null; let watched = false; -export default async function dev(serverEntry?: string) { - serverEntry = serverEntry - ? serverEntry.startsWith("file://") ? path.fromFileUrl(serverEntry) : path.resolve(serverEntry) - : await findFile(builtinModuleExts.map((ext) => `server.${ext}`)); - if (!serverEntry) { - log.fatal("[dev] No server entry found."); - return; - } - - const appDir = path.dirname(serverEntry); - if (!watched) { - log.info(colors.dim("[dev]"), "Watching for file changes..."); - watch(appDir); - watched = true; - } - - const entry = `./${path.basename(serverEntry)}`; - const code = await Deno.readTextFile(serverEntry); - const importMap = await getImportMap(); - const deps = await parseDeps(entry, code, { - importMap: JSON.stringify(importMap), - }); - const exportTs = deps.find((dep) => dep.specifier.startsWith("./") && dep.specifier.endsWith("/_export.ts")); - - // reset the `_export.ts` module - if (exportTs) { - const fp = path.join(appDir, exportTs.specifier); - await ensureDir(path.dirname(fp)); - await Deno.writeTextFile(fp, "export default {}"); - } - - // watch server entry and its deps to restart the dev server - const emitter = createWatchFsEmitter(); - emitter.on("*", (kind, { specifier }) => { - if ( - kind === "modify" && !specifier.endsWith("/_export.ts") && ( - specifier === entry || - deps.some((dep) => dep.specifier === specifier) - ) - ) { - console.clear(); - console.info(colors.dim("[dev] Restarting the server...")); - devProcess?.kill("SIGTERM"); - dev(serverEntry); - } - }); - - const cmd = [Deno.execPath(), "run", "-A", "--no-lock", serverEntry, "--dev"]; - devProcess = Deno.run({ cmd, stderr: "inherit", stdout: "inherit" }); - await devProcess.status(); - removeWatchFsEmitter(emitter); +export default async function dev(args?: string[]) { + const flags = parseCliArgs(args || [], { + boolean: ["unstable", "A"], + }); + const serverEntry = + typeof flags._[0] === "string" + ? flags._[0].startsWith("file://") + ? path.fromFileUrl(flags._[0]) + : path.resolve(flags._[0]) + : await findFile(builtinModuleExts.map(ext => `server.${ext}`)); + + if (!serverEntry) { + log.fatal("[dev] No server entry found."); + return; + } + + const appDir = path.dirname(serverEntry); + if (!watched) { + log.info(colors.dim("[dev]"), "Watching for file changes..."); + watch(appDir); + watched = true; + } + + const entry = `./${path.basename(serverEntry)}`; + const code = await Deno.readTextFile(serverEntry); + const importMap = await getImportMap(); + const deps = await parseDeps(entry, code, { + importMap: JSON.stringify(importMap), + }); + const exportTs = deps.find( + dep => + dep.specifier.startsWith("./") && + dep.specifier.endsWith("/_export.ts") + ); + + // reset the `_export.ts` module + if (exportTs) { + const fp = path.join(appDir, exportTs.specifier); + await ensureDir(path.dirname(fp)); + await Deno.writeTextFile(fp, "export default {}"); + } + + // watch server entry and its deps to restart the dev server + const emitter = createWatchFsEmitter(); + emitter.on("*", (kind, { specifier }) => { + if ( + kind === "modify" && + !specifier.endsWith("/_export.ts") && + (specifier === entry || + deps.some(dep => dep.specifier === specifier)) + ) { + console.clear(); + console.info(colors.dim("[dev] Restarting the server...")); + devProcess?.kill("SIGTERM"); + dev(args); + } + }); + + const cmd = [Deno.execPath(), "run", "-A", "--no-lock"]; + + flags.unstable && cmd.push("--unstable"); + cmd.push(serverEntry), cmd.push("--dev"); + console.log(cmd); + devProcess = Deno.run({ cmd, stderr: "inherit", stdout: "inherit" }); + await devProcess.status(); + removeWatchFsEmitter(emitter); } export function handleHMR(req: Request): Response { - const { socket, response } = Deno.upgradeWebSocket(req); - const emitter = createWatchFsEmitter(); - const send = (message: Record) => { - try { - socket.send(JSON.stringify(message)); - } catch (err) { - log.warn("socket.send:", err.message); - } - }; - socket.addEventListener("close", () => { - removeWatchFsEmitter(emitter); - }); - socket.addEventListener("open", () => { - emitter.on("create", ({ specifier }) => { - const config: AlephConfig | undefined = Reflect.get( - globalThis, - "__ALEPH_CONFIG", - ); - if (config?.router) { - const reg = toRouterRegExp(config.router); - const routePattern = reg.exec(specifier); - if (routePattern) { - send({ type: "create", specifier, routePattern }); - return; - } - } - send({ type: "create", specifier }); - }); - emitter.on("remove", ({ specifier }) => { - emitter.off(`hotUpdate:${specifier}`); - send({ type: "remove", specifier }); - }); - }); - socket.addEventListener("message", (e) => { - if (isFilledString(e.data)) { - try { - const { type, specifier } = JSON.parse(e.data); - if (type === "hotAccept" && isFilledString(specifier)) { - emitter.on(`hotUpdate:${specifier}`, () => { - send({ type: "modify", specifier }); - }); - } - } catch (_e) { - log.error("invlid socket message:", e.data); - } - } - }); - return response; + const { socket, response } = Deno.upgradeWebSocket(req); + const emitter = createWatchFsEmitter(); + const send = (message: Record) => { + try { + socket.send(JSON.stringify(message)); + } catch (err) { + log.warn("socket.send:", err.message); + } + }; + socket.addEventListener("close", () => { + removeWatchFsEmitter(emitter); + }); + socket.addEventListener("open", () => { + emitter.on("create", ({ specifier }) => { + const config: AlephConfig | undefined = Reflect.get( + globalThis, + "__ALEPH_CONFIG" + ); + if (config?.router) { + const reg = toRouterRegExp(config.router); + const routePattern = reg.exec(specifier); + if (routePattern) { + send({ type: "create", specifier, routePattern }); + return; + } + } + send({ type: "create", specifier }); + }); + emitter.on("remove", ({ specifier }) => { + emitter.off(`hotUpdate:${specifier}`); + send({ type: "remove", specifier }); + }); + }); + socket.addEventListener("message", e => { + if (isFilledString(e.data)) { + try { + const { type, specifier } = JSON.parse(e.data); + if (type === "hotAccept" && isFilledString(specifier)) { + emitter.on(`hotUpdate:${specifier}`, () => { + send({ type: "modify", specifier }); + }); + } + } catch (_e) { + log.error("invlid socket message:", e.data); + } + } + }); + return response; } From 3282afd420817ac85aa22440196352fc0d7310fb Mon Sep 17 00:00:00 2001 From: Brock Donahue Date: Tue, 20 Jun 2023 12:00:15 -0400 Subject: [PATCH 2/6] fix format issues, and change data back to function instead for test -A since it failed in CI for some reason... --- dev.ts | 2 +- examples/react-app/routes/todos.tsx | 254 ++++++----- examples/react-app/server.ts | 8 +- framework/react/markup.ts | 16 +- init.ts | 630 ++++++++++++++-------------- server/deps.ts | 10 +- server/dev.ts | 380 ++++++++--------- 7 files changed, 635 insertions(+), 665 deletions(-) diff --git a/dev.ts b/dev.ts index f0f52e19d..dc93f2b6f 100644 --- a/dev.ts +++ b/dev.ts @@ -1,5 +1,5 @@ import dev from "./server/dev.ts"; if (import.meta.main) { - dev(Deno.args); + dev(Deno.args); } diff --git a/examples/react-app/routes/todos.tsx b/examples/react-app/routes/todos.tsx index fa224e4b3..0788008e2 100644 --- a/examples/react-app/routes/todos.tsx +++ b/examples/react-app/routes/todos.tsx @@ -4,146 +4,142 @@ import type { FormEvent } from "react"; import { Head, useData } from "aleph/react"; type Todo = { - id: number; - message: string; - completed: boolean; + id: number; + message: string; + completed: boolean; }; const store = { - todos: JSON.parse(window.localStorage?.getItem("todos") || "[]") as Todo[], - save() { - localStorage?.setItem("todos", JSON.stringify(this.todos)); - }, + todos: JSON.parse(window.localStorage?.getItem("todos") || "[]") as Todo[], + save() { + localStorage?.setItem("todos", JSON.stringify(this.todos)); + }, }; -export const data = { - defer: false, - fetch() { - return Response.json(store); - }, +export const data = () => { + return Response.json(store); }; export async function mutation(req: Request): Promise { - const { id, message, completed } = await req.json(); - switch (req.method) { - case "PUT": { - store.todos.push({ id: Date.now(), message, completed: false }); - store.save(); - break; - } - case "PATCH": { - const todo = store.todos.find(todo => todo.id === id); - if (todo) { - todo.completed = completed; - store.save(); - } - break; - } - case "DELETE": { - store.todos = store.todos.filter(todo => todo.id !== id); - store.save(); - } - } - return Response.json(store); + const { id, message, completed } = await req.json(); + switch (req.method) { + case "PUT": { + store.todos.push({ id: Date.now(), message, completed: false }); + store.save(); + break; + } + case "PATCH": { + const todo = store.todos.find((todo) => todo.id === id); + if (todo) { + todo.completed = completed; + store.save(); + } + break; + } + case "DELETE": { + store.todos = store.todos.filter((todo) => todo.id !== id); + store.save(); + } + } + return Response.json(store); } export default function Todos() { - const { - data: { todos }, - isMutating, - mutation, - } = useData<{ todos: Todo[] }>(); + const { + data: { todos }, + isMutating, + mutation, + } = useData<{ todos: Todo[] }>(); - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const fd = new FormData(form); - const message = fd.get("message")?.toString().trim(); - if (message) { - await mutation.put( - { message }, - { - // optimistic update data without waiting for the server response - optimisticUpdate: data => { - return { - todos: [ - ...data.todos, - { id: 0, message, completed: false }, - ], - }; - }, - // replace the data with the new data that is from the server response - replace: true, - } - ); - setTimeout(() => form.querySelector("input")?.focus(), 0); - form.reset(); - } - }; + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const fd = new FormData(form); + const message = fd.get("message")?.toString().trim(); + if (message) { + await mutation.put( + { message }, + { + // optimistic update data without waiting for the server response + optimisticUpdate: (data) => { + return { + todos: [ + ...data.todos, + { id: 0, message, completed: false }, + ], + }; + }, + // replace the data with the new data that is from the server response + replace: true, + }, + ); + setTimeout(() => form.querySelector("input")?.focus(), 0); + form.reset(); + } + }; - return ( -
- - Todos - - -

- Todos - {todos.length > 0 && ( - - {todos.filter(todo => todo.completed).length}/ - {todos.length} - - )} -

-
    - {todos.map(todo => ( -
  • - - mutation.patch( - { id: todo.id, completed: !todo.completed }, - "replace" - ) - } - /> - - {todo.id > 0 && ( - - )} -
  • - ))} -
-
- -
-
- ); + return ( +
+ + Todos + + +

+ Todos + {todos.length > 0 && ( + + {todos.filter((todo) => todo.completed).length}/ + {todos.length} + + )} +

+
    + {todos.map((todo) => ( +
  • + + mutation.patch( + { id: todo.id, completed: !todo.completed }, + "replace", + )} + /> + + {todo.id > 0 && ( + + )} +
  • + ))} +
+
+ +
+
+ ); } diff --git a/examples/react-app/server.ts b/examples/react-app/server.ts index 815aa7b86..a48ed7495 100644 --- a/examples/react-app/server.ts +++ b/examples/react-app/server.ts @@ -6,8 +6,8 @@ import denoDeploy from "aleph/plugins/deploy"; import modules from "./routes/_export.ts"; serve({ - plugins: [ - denoDeploy({ moduleMain: import.meta.url, modules }), - react({ ssr: true }), - ], + plugins: [ + denoDeploy({ moduleMain: import.meta.url, modules }), + react({ ssr: true }), + ], }); diff --git a/framework/react/markup.ts b/framework/react/markup.ts index 4e91a3855..ef23cc1f2 100644 --- a/framework/react/markup.ts +++ b/framework/react/markup.ts @@ -1,23 +1,23 @@ /** @format */ const ESCAPE_LOOKUP: { [match: string]: string } = { - "&": "\\u0026", - ">": "\\u003e", - "<": "\\u003c", - "\u2028": "\\u2028", - "\u2029": "\\u2029", + "&": "\\u0026", + ">": "\\u003e", + "<": "\\u003c", + "\u2028": "\\u2028", + "\u2029": "\\u2029", }; const ESCAPE_REGEX = /[&><\u2028\u2029]/g; export function escapeHtml(html: string) { - return html.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); + return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); } export interface SafeHtml { - __html: string; + __html: string; } export function createHtml(html: string): SafeHtml { - return { __html: html }; + return { __html: html }; } diff --git a/init.ts b/init.ts index 79477d3d9..e32a7feec 100644 --- a/init.ts +++ b/init.ts @@ -2,14 +2,7 @@ import { Untar } from "https://deno.land/std@0.192.0/archive/untar.ts"; import { parse } from "https://deno.land/std@0.192.0/flags/mod.ts"; -import { - blue, - bold, - cyan, - dim, - green, - red, -} from "https://deno.land/std@0.192.0/fmt/colors.ts"; +import { blue, bold, cyan, dim, green, red } from "https://deno.land/std@0.192.0/fmt/colors.ts"; import { copy as copyDir } from "https://deno.land/std@0.192.0/fs/copy.ts"; import { copy } from "https://deno.land/std@0.192.0/streams/copy.ts"; import { readerFromStreamReader } from "https://deno.land/std@0.192.0/streams/reader_from_stream_reader.ts"; @@ -23,357 +16,360 @@ const rsApps = ["yew", "leptos"]; const unocssApps = ["react", "yew", "leptos"]; const versions = { - react: "18.2.0", + react: "18.2.0", }; type Options = { - template?: string; + template?: string; }; export default async function init(nameArg?: string, options?: Options) { - let { template } = options || {}; + let { template } = options || {}; - // get and check the project name - const name = nameArg ?? (await ask("Project Name:")); - if (!name) { - console.error(`${red("!")} Please entry project name.`); - Deno.exit(1); - } + // get and check the project name + const name = nameArg ?? (await ask("Project Name:")); + if (!name) { + console.error(`${red("!")} Please entry project name.`); + Deno.exit(1); + } - if (template && !templates.includes(template)) { - console.error( - `${red("!")} Invalid template name ${red( - template - )}, must be one of [${blue(templates.join(","))}]` - ); - Deno.exit(1); - } + if (template && !templates.includes(template)) { + console.error( + `${red("!")} Invalid template name ${ + red( + template, + ) + }, must be one of [${blue(templates.join(","))}]`, + ); + Deno.exit(1); + } - // check the dir is clean - if ( - !(await isFolderEmpty(Deno.cwd(), name)) && - !(await confirm(`Folder ${blue(name)} already exists, continue?`)) - ) { - Deno.exit(1); - } + // check the dir is clean + if ( + !(await isFolderEmpty(Deno.cwd(), name)) && + !(await confirm(`Folder ${blue(name)} already exists, continue?`)) + ) { + Deno.exit(1); + } - if (!template) { - const answer = await ask( - [ - "Select a framework:", - ...templates.map( - (name, i) => - ` ${bold( - (i + 1).toString() - )}. ${getTemplateDisplayName(name)}` - ), - dim(`[1-${templates.length}]`), - ].join("\n") - ); - const n = parseInt(answer); - if (!isNaN(n) && n > 0 && n <= templates.length) { - template = templates[n - 1]; - } else { - console.error( - `${red("!")} Please entry ${cyan(`[1-${templates.length}]`)}.` - ); - Deno.exit(1); - } - } + if (!template) { + const answer = await ask( + [ + "Select a framework:", + ...templates.map( + (name, i) => + ` ${ + bold( + (i + 1).toString(), + ) + }. ${getTemplateDisplayName(name)}`, + ), + dim(`[1-${templates.length}]`), + ].join("\n"), + ); + const n = parseInt(answer); + if (!isNaN(n) && n > 0 && n <= templates.length) { + template = templates[n - 1]; + } else { + console.error( + `${red("!")} Please entry ${cyan(`[1-${templates.length}]`)}.`, + ); + Deno.exit(1); + } + } - const appDir = join(Deno.cwd(), name); - const withUnocss = - unocssApps.includes(template!) && - (await confirm("Use Atomic CSS (powered by Unocss)?")); - const withVscode = await confirm( - "Initialize VS Code workspace configuration?" - ); - const deploy = !rsApps.includes(template) - ? await confirm("Deploy to Deno Deploy?") - : false; - const isRsApp = rsApps.includes(template); + const appDir = join(Deno.cwd(), name); + const withUnocss = unocssApps.includes(template!) && + (await confirm("Use Atomic CSS (powered by Unocss)?")); + const withVscode = await confirm( + "Initialize VS Code workspace configuration?", + ); + const deploy = !rsApps.includes(template) ? await confirm("Deploy to Deno Deploy?") : false; + const isRsApp = rsApps.includes(template); - let alephPkgUri: string; - if (import.meta.url.startsWith("file://")) { - const src = `examples/${ - withUnocss ? "with-unocss/" : "" - }${template}-app/`; - await copyDir(src, name); - alephPkgUri = ".."; - } else { - console.log( - `${dim("↓")} Downloading template(${blue( - template! - )}), this might take a moment...` - ); - const res = await fetch( - "https://cdn.deno.land/aleph/meta/versions.json" - ); - if (res.status !== 200) { - console.error(await res.text()); - Deno.exit(1); - } - const { latest: VERSION } = await res.json(); - const repo = "alephjs/aleph.js"; - const resp = await fetch( - `https://codeload.github.com/${repo}/tar.gz/refs/tags/${VERSION}` - ); - if (resp.status !== 200) { - console.error(await resp.text()); - Deno.exit(1); - } - // deno-lint-ignore ban-ts-comment - // @ts-ignore - const gz = new DecompressionStream("gzip"); - const entryList = new Untar( - readerFromStreamReader( - resp.body!.pipeThrough(gz).getReader() - ) - ); - const prefix = `${basename(repo)}-${VERSION}/examples/${ - withUnocss ? "with-unocss/" : "" - }${template}-app/`; - for await (const entry of entryList) { - if ( - entry.fileName.startsWith(prefix) && - !entry.fileName.endsWith("/README.md") - ) { - const fp = join(appDir, trimPrefix(entry.fileName, prefix)); - if (entry.type === "directory") { - await ensureDir(fp); - continue; - } - const file = await Deno.open(fp, { write: true, create: true }); - await copy(entry, file); - } - } - alephPkgUri = `https://deno.land/x/aleph@${VERSION}`; - } + let alephPkgUri: string; + if (import.meta.url.startsWith("file://")) { + const src = `examples/${withUnocss ? "with-unocss/" : ""}${template}-app/`; + await copyDir(src, name); + alephPkgUri = ".."; + } else { + console.log( + `${dim("↓")} Downloading template(${ + blue( + template!, + ) + }), this might take a moment...`, + ); + const res = await fetch( + "https://cdn.deno.land/aleph/meta/versions.json", + ); + if (res.status !== 200) { + console.error(await res.text()); + Deno.exit(1); + } + const { latest: VERSION } = await res.json(); + const repo = "alephjs/aleph.js"; + const resp = await fetch( + `https://codeload.github.com/${repo}/tar.gz/refs/tags/${VERSION}`, + ); + if (resp.status !== 200) { + console.error(await resp.text()); + Deno.exit(1); + } + // deno-lint-ignore ban-ts-comment + // @ts-ignore + const gz = new DecompressionStream("gzip"); + const entryList = new Untar( + readerFromStreamReader( + resp.body!.pipeThrough(gz).getReader(), + ), + ); + const prefix = `${basename(repo)}-${VERSION}/examples/${withUnocss ? "with-unocss/" : ""}${template}-app/`; + for await (const entry of entryList) { + if ( + entry.fileName.startsWith(prefix) && + !entry.fileName.endsWith("/README.md") + ) { + const fp = join(appDir, trimPrefix(entry.fileName, prefix)); + if (entry.type === "directory") { + await ensureDir(fp); + continue; + } + const file = await Deno.open(fp, { write: true, create: true }); + await copy(entry, file); + } + } + alephPkgUri = `https://deno.land/x/aleph@${VERSION}`; + } - const serverCode = await Deno.readTextFile(join(appDir, "server.ts")); - if (!isRsApp && !deploy) { - await Deno.writeTextFile( - join(appDir, "server.ts"), - serverCode - .replace('import modules from "./routes/_export.ts";\n', "") - .replace('import denoDeploy from "aleph/plugins/deploy";\n', "") - .replace( - " denoDeploy({ moduleMain: import.meta.url, modules }),\n", - "" - ) - .replace(" denoDeploy({ modules }),\n", "") - ); - await Deno.remove(join(appDir, "routes/_export.ts")); - } else { - await Deno.writeTextFile( - join(appDir, "server.ts"), - serverCode.replace( - "denoDeploy({ moduleMain: import.meta.url, modules })", - "denoDeploy({ modules })" - ) - ); - } + const serverCode = await Deno.readTextFile(join(appDir, "server.ts")); + if (!isRsApp && !deploy) { + await Deno.writeTextFile( + join(appDir, "server.ts"), + serverCode + .replace('import modules from "./routes/_export.ts";\n', "") + .replace('import denoDeploy from "aleph/plugins/deploy";\n', "") + .replace( + " denoDeploy({ moduleMain: import.meta.url, modules }),\n", + "", + ) + .replace(" denoDeploy({ modules }),\n", ""), + ); + await Deno.remove(join(appDir, "routes/_export.ts")); + } else { + await Deno.writeTextFile( + join(appDir, "server.ts"), + serverCode.replace( + "denoDeploy({ moduleMain: import.meta.url, modules })", + "denoDeploy({ modules })", + ), + ); + } - const res = await fetch("https://esm.sh/status.json"); - if (res.status !== 200) { - console.error(await res.text()); - Deno.exit(1); - } - const { version: ESM_VERSION } = await res.json(); - const denoConfig = { - compilerOptions: { - lib: ["dom", "dom.iterable", "dom.extras", "deno.ns"], - types: [`${alephPkgUri}/types.d.ts`], - }, - importMap: "import_map.json", - tasks: { - dev: (await existsFile(join(appDir, "dev.ts"))) - ? "deno run -A dev.ts" - : `deno run -A ${alephPkgUri}/dev.ts`, - start: "deno run -A server.ts", - build: "deno run -A server.ts --build", - "esm:add": `deno run -A https://esm.sh/v${ESM_VERSION} add`, - "esm:update": `deno run -A https://esm.sh/v${ESM_VERSION} update`, - "esm:remove": `deno run -A https://esm.sh/v${ESM_VERSION} remove`, - }, - }; - const importMap = { - imports: { - "~/": "./", - "std/": "https://deno.land/std@0.180.0/", - "aleph/": `${alephPkgUri}/`, - "aleph/server": `${alephPkgUri}/server/mod.ts`, - "aleph/dev": `${alephPkgUri}/server/dev.ts`, - } as Record, - scopes: {}, - }; - if (deploy) { - Object.assign(importMap.imports, { - "aleph/plugins/deploy": `${alephPkgUri}/plugins/deploy.ts`, - }); - } - if (withUnocss) { - Object.assign(importMap.imports, { - "aleph/plugins/unocss": `${alephPkgUri}/plugins/unocss.ts`, - "@unocss/core": `https://esm.sh/v${ESM_VERSION}/@unocss/core@0.50.6`, - "@unocss/preset-uno": `https://esm.sh/v${ESM_VERSION}/@unocss/preset-uno@0.50.6`, - }); - } - switch (template) { - case "react-mdx": - Object.assign(importMap.imports, { - "aleph/plugins/mdx": `${alephPkgUri}/plugins/mdx.ts`, - "@mdx-js/react": `https://esm.sh/v${ESM_VERSION}/@mdx-js/react@2.3.0`, - }); - /* falls through */ - case "react": { - Object.assign(denoConfig.compilerOptions, { - jsx: "react-jsx", - jsxImportSource: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, - }); - Object.assign(importMap.imports, { - "aleph/react": `${alephPkgUri}/framework/react/mod.ts`, - "aleph/plugins/react": `${alephPkgUri}/framework/react/plugin.ts`, - react: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, - "react-dom": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}`, - "react-dom/": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}/`, - }); - break; - } - } + const res = await fetch("https://esm.sh/status.json"); + if (res.status !== 200) { + console.error(await res.text()); + Deno.exit(1); + } + const { version: ESM_VERSION } = await res.json(); + const denoConfig = { + compilerOptions: { + lib: ["dom", "dom.iterable", "dom.extras", "deno.ns"], + types: [`${alephPkgUri}/types.d.ts`], + }, + importMap: "import_map.json", + tasks: { + dev: (await existsFile(join(appDir, "dev.ts"))) ? "deno run -A dev.ts" : `deno run -A ${alephPkgUri}/dev.ts`, + start: "deno run -A server.ts", + build: "deno run -A server.ts --build", + "esm:add": `deno run -A https://esm.sh/v${ESM_VERSION} add`, + "esm:update": `deno run -A https://esm.sh/v${ESM_VERSION} update`, + "esm:remove": `deno run -A https://esm.sh/v${ESM_VERSION} remove`, + }, + }; + const importMap = { + imports: { + "~/": "./", + "std/": "https://deno.land/std@0.180.0/", + "aleph/": `${alephPkgUri}/`, + "aleph/server": `${alephPkgUri}/server/mod.ts`, + "aleph/dev": `${alephPkgUri}/server/dev.ts`, + } as Record, + scopes: {}, + }; + if (deploy) { + Object.assign(importMap.imports, { + "aleph/plugins/deploy": `${alephPkgUri}/plugins/deploy.ts`, + }); + } + if (withUnocss) { + Object.assign(importMap.imports, { + "aleph/plugins/unocss": `${alephPkgUri}/plugins/unocss.ts`, + "@unocss/core": `https://esm.sh/v${ESM_VERSION}/@unocss/core@0.50.6`, + "@unocss/preset-uno": `https://esm.sh/v${ESM_VERSION}/@unocss/preset-uno@0.50.6`, + }); + } + switch (template) { + case "react-mdx": + Object.assign(importMap.imports, { + "aleph/plugins/mdx": `${alephPkgUri}/plugins/mdx.ts`, + "@mdx-js/react": `https://esm.sh/v${ESM_VERSION}/@mdx-js/react@2.3.0`, + }); + /* falls through */ + case "react": { + Object.assign(denoConfig.compilerOptions, { + jsx: "react-jsx", + jsxImportSource: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, + }); + Object.assign(importMap.imports, { + "aleph/react": `${alephPkgUri}/framework/react/mod.ts`, + "aleph/plugins/react": `${alephPkgUri}/framework/react/plugin.ts`, + react: `https://esm.sh/v${ESM_VERSION}/react@${versions.react}`, + "react-dom": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}`, + "react-dom/": `https://esm.sh/v${ESM_VERSION}/react-dom@${versions.react}/`, + }); + break; + } + } - await ensureDir(appDir); - await Promise.all([ - Deno.writeTextFile( - join(appDir, "deno.json"), - JSON.stringify(denoConfig, undefined, 2) - ), - Deno.writeTextFile( - join(appDir, "import_map.json"), - JSON.stringify(importMap, undefined, 2) - ), - ]); + await ensureDir(appDir); + await Promise.all([ + Deno.writeTextFile( + join(appDir, "deno.json"), + JSON.stringify(denoConfig, undefined, 2), + ), + Deno.writeTextFile( + join(appDir, "import_map.json"), + JSON.stringify(importMap, undefined, 2), + ), + ]); - if (withVscode) { - const settings = { - "deno.enable": true, - "deno.lint": true, - "deno.config": "./deno.json", - }; - await ensureDir(join(appDir, ".vscode")); - await Deno.writeTextFile( - join(appDir, ".vscode", "settings.json"), - JSON.stringify(settings, undefined, 2) - ); - } + if (withVscode) { + const settings = { + "deno.enable": true, + "deno.lint": true, + "deno.config": "./deno.json", + }; + await ensureDir(join(appDir, ".vscode")); + await Deno.writeTextFile( + join(appDir, ".vscode", "settings.json"), + JSON.stringify(settings, undefined, 2), + ); + } - await Deno.run({ - cmd: [Deno.execPath(), "cache", "--no-lock", "server.ts"], - cwd: appDir, - stderr: "inherit", - stdout: "inherit", - }).status(); + await Deno.run({ + cmd: [Deno.execPath(), "cache", "--no-lock", "server.ts"], + cwd: appDir, + stderr: "inherit", + stdout: "inherit", + }).status(); - console.log( - [ - "", - green("▲ Aleph.js is ready to go!"), - "", - `${dim("$")} cd ${name}`, - `${dim("$")} deno task dev ${dim( - "# Start the server in `development` mode" - )}`, - `${dim("$")} deno task start ${dim( - "# Start the server in `production` mode" - )}`, - `${dim("$")} deno task build ${dim( - "# Build & Optimize the app (bundling, SSG, etc.)" - )}`, - "", - `Docs: ${cyan("https://alephjs.org/docs")}`, - `Bugs: ${cyan("https://github.com/alephjs/aleph.js/issues")}`, - "", - ].join("\n") - ); - Deno.exit(0); + console.log( + [ + "", + green("▲ Aleph.js is ready to go!"), + "", + `${dim("$")} cd ${name}`, + `${dim("$")} deno task dev ${ + dim( + "# Start the server in `development` mode", + ) + }`, + `${dim("$")} deno task start ${ + dim( + "# Start the server in `production` mode", + ) + }`, + `${dim("$")} deno task build ${ + dim( + "# Build & Optimize the app (bundling, SSG, etc.)", + ) + }`, + "", + `Docs: ${cyan("https://alephjs.org/docs")}`, + `Bugs: ${cyan("https://github.com/alephjs/aleph.js/issues")}`, + "", + ].join("\n"), + ); + Deno.exit(0); } async function isFolderEmpty(root: string, name: string): Promise { - const dir = join(root, name); - if (await existsFile(dir)) { - throw new Error(`Folder ${name} already exists as a file.`); - } - if (await existsDir(dir)) { - for await (const file of Deno.readDir(dir)) { - if (file.name !== ".DS_Store") { - return false; - } - } - } - return true; + const dir = join(root, name); + if (await existsFile(dir)) { + throw new Error(`Folder ${name} already exists as a file.`); + } + if (await existsDir(dir)) { + for await (const file of Deno.readDir(dir)) { + if (file.name !== ".DS_Store") { + return false; + } + } + } + return true; } async function ask(question = ":") { - await Deno.stdout.write( - new TextEncoder().encode(cyan("? ") + question + " ") - ); - const buf = new Uint8Array(1024); - const n = await Deno.stdin.read(buf); - const answer = new TextDecoder().decode(buf.subarray(0, n)); - return answer.trim(); + await Deno.stdout.write( + new TextEncoder().encode(cyan("? ") + question + " "), + ); + const buf = new Uint8Array(1024); + const n = await Deno.stdin.read(buf); + const answer = new TextDecoder().decode(buf.subarray(0, n)); + return answer.trim(); } async function confirm(question = "are you sure?") { - let a: string; - // deno-lint-ignore no-empty - while (!/^(y|n|)$/i.test((a = await ask(question + dim(" [y/N]"))))) {} - return a.toLowerCase() === "y"; + let a: string; + // deno-lint-ignore no-empty + while (!/^(y|n|)$/i.test(a = await ask(question + dim(" [y/N]")))) {} + return a.toLowerCase() === "y"; } function trimPrefix(s: string, prefix: string): string { - if (prefix !== "" && s.startsWith(prefix)) { - return s.slice(prefix.length); - } - return s; + if (prefix !== "" && s.startsWith(prefix)) { + return s.slice(prefix.length); + } + return s; } function getTemplateDisplayName(name: string) { - if (name === "api") { - return "REST API"; - } - if (name === "react-mdx") { - return "React with MDX"; - } - return name.at(0)?.toUpperCase() + name.slice(1); + if (name === "api") { + return "REST API"; + } + if (name === "react-mdx") { + return "React with MDX"; + } + return name.at(0)?.toUpperCase() + name.slice(1); } /** Check whether or not the given path exists as a directory. */ export async function existsDir(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isDirectory; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isDirectory; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } /** Check whether or not the given path exists as regular file. */ export async function existsFile(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isFile; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isFile; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } if (import.meta.main) { - const { _: args, ...options } = parse(Deno.args); - await init(args[0] as string | undefined, options); + const { _: args, ...options } = parse(Deno.args); + await init(args[0] as string | undefined, options); } diff --git a/server/deps.ts b/server/deps.ts index 630de9559..2cbe4eae4 100644 --- a/server/deps.ts +++ b/server/deps.ts @@ -15,14 +15,8 @@ export { parse as parseCliArgs } from "https://deno.land/std@0.192.0/flags/mod.t export * as esbuild from "https://deno.land/x/esbuild@v0.17.12/mod.js"; export * from "https://deno.land/x/aleph_compiler@0.9.3/mod.ts"; export * from "https://deno.land/x/aleph_compiler@0.9.3/types.ts"; -export { - default as initLolHtml, - HTMLRewriter, -} from "https://deno.land/x/lol_html@0.0.6/mod.ts"; +export { default as initLolHtml, HTMLRewriter } from "https://deno.land/x/lol_html@0.0.6/mod.ts"; export { default as lolHtmlWasm } from "https://deno.land/x/lol_html@0.0.6/wasm.js"; // npm -export { - default as mitt, - type Emitter, -} from "https://esm.sh/mitt@3.0.0?pin=v110"; +export { default as mitt, type Emitter } from "https://esm.sh/mitt@3.0.0?pin=v110"; diff --git a/server/dev.ts b/server/dev.ts index 269f1e837..1ba61f3f3 100644 --- a/server/dev.ts +++ b/server/dev.ts @@ -1,231 +1,215 @@ /** @format */ import { isFilledString } from "../shared/util.ts"; -import { - colors, - Emitter, - ensureDir, - mitt, - parseDeps, - path, - parseCliArgs, -} from "./deps.ts"; +import { colors, Emitter, ensureDir, mitt, parseCliArgs, parseDeps, path } from "./deps.ts"; import depGraph from "./graph.ts"; -import { - builtinModuleExts, - findFile, - getAlephConfig, - getImportMap, - watchFs, -} from "./helpers.ts"; +import { builtinModuleExts, findFile, getAlephConfig, getImportMap, watchFs } from "./helpers.ts"; import log from "./log.ts"; import { initRouter, toRouterRegExp } from "./router.ts"; import type { AlephConfig } from "./types.ts"; type WatchFsEvents = { - [key in - | "create" - | "remove" - | "modify" - | `modify:${string}` - | `hotUpdate:${string}`]: { - specifier: string; - }; + [ + key in + | "create" + | "remove" + | "modify" + | `modify:${string}` + | `hotUpdate:${string}` + ]: { + specifier: string; + }; }; const watchFsEmitters = new Set>(); /** Create a `watchFs` emitter. */ export function createWatchFsEmitter() { - const e = mitt(); - watchFsEmitters.add(e); - return e; + const e = mitt(); + watchFsEmitters.add(e); + return e; } /** Remove the emitter. */ export function removeWatchFsEmitter(e: Emitter) { - e.all.clear(); - watchFsEmitters.delete(e); + e.all.clear(); + watchFsEmitters.delete(e); } /** Watch for file changes. */ export function watch(appDir: string, onRouterChange?: () => void) { - const config = getAlephConfig(); - const emitter = createWatchFsEmitter(); - - emitter.on("*", async (kind, { specifier }) => { - if (kind === "create" || kind === "remove") { - // reload router when fs changess - const reg = toRouterRegExp(config?.router); - if (reg.test(specifier)) { - const router = await initRouter(appDir, config?.router); - Reflect.set(globalThis, "__ALEPH_ROUTER", router); - onRouterChange?.(); - } - } - }); - - if (onRouterChange) { - initRouter(appDir, config?.router).then(router => { - Reflect.set(globalThis, "__ALEPH_ROUTER", router); - onRouterChange(); - }); - } - - watchFs( - appDir, - (kind: "create" | "remove" | "modify", pathname: string) => { - const specifier = - "./" + path.relative(appDir, pathname).replaceAll("\\", "/"); - // delete global cached index html - if (specifier === "./index.html") { - Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML"); - } - if (kind === "remove") { - depGraph.unmark(specifier); - } else { - depGraph.update(specifier); - } - if (kind === "modify") { - watchFsEmitters.forEach(e => { - e.emit("modify", { specifier }); - e.emit(`modify:${specifier}`, { specifier }); - if (e.all.has(`hotUpdate:${specifier}`)) { - e.emit(`hotUpdate:${specifier}`, { specifier }); - } else if (specifier !== "./routes/_export.ts") { - depGraph.lookup(specifier, specifier => { - if (e.all.has(`hotUpdate:${specifier}`)) { - e.emit(`hotUpdate:${specifier}`, { specifier }); - return false; - } - }); - } - }); - } else { - watchFsEmitters.forEach(e => e.emit(kind, { specifier })); - } - } - ); + const config = getAlephConfig(); + const emitter = createWatchFsEmitter(); + + emitter.on("*", async (kind, { specifier }) => { + if (kind === "create" || kind === "remove") { + // reload router when fs changess + const reg = toRouterRegExp(config?.router); + if (reg.test(specifier)) { + const router = await initRouter(appDir, config?.router); + Reflect.set(globalThis, "__ALEPH_ROUTER", router); + onRouterChange?.(); + } + } + }); + + if (onRouterChange) { + initRouter(appDir, config?.router).then((router) => { + Reflect.set(globalThis, "__ALEPH_ROUTER", router); + onRouterChange(); + }); + } + + watchFs( + appDir, + (kind: "create" | "remove" | "modify", pathname: string) => { + const specifier = "./" + path.relative(appDir, pathname).replaceAll("\\", "/"); + // delete global cached index html + if (specifier === "./index.html") { + Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML"); + } + if (kind === "remove") { + depGraph.unmark(specifier); + } else { + depGraph.update(specifier); + } + if (kind === "modify") { + watchFsEmitters.forEach((e) => { + e.emit("modify", { specifier }); + e.emit(`modify:${specifier}`, { specifier }); + if (e.all.has(`hotUpdate:${specifier}`)) { + e.emit(`hotUpdate:${specifier}`, { specifier }); + } else if (specifier !== "./routes/_export.ts") { + depGraph.lookup(specifier, (specifier) => { + if (e.all.has(`hotUpdate:${specifier}`)) { + e.emit(`hotUpdate:${specifier}`, { specifier }); + return false; + } + }); + } + }); + } else { + watchFsEmitters.forEach((e) => e.emit(kind, { specifier })); + } + }, + ); } let devProcess: Deno.Process | null = null; let watched = false; export default async function dev(args?: string[]) { - const flags = parseCliArgs(args || [], { - boolean: ["unstable", "A"], - }); - const serverEntry = - typeof flags._[0] === "string" - ? flags._[0].startsWith("file://") - ? path.fromFileUrl(flags._[0]) - : path.resolve(flags._[0]) - : await findFile(builtinModuleExts.map(ext => `server.${ext}`)); - - if (!serverEntry) { - log.fatal("[dev] No server entry found."); - return; - } - - const appDir = path.dirname(serverEntry); - if (!watched) { - log.info(colors.dim("[dev]"), "Watching for file changes..."); - watch(appDir); - watched = true; - } - - const entry = `./${path.basename(serverEntry)}`; - const code = await Deno.readTextFile(serverEntry); - const importMap = await getImportMap(); - const deps = await parseDeps(entry, code, { - importMap: JSON.stringify(importMap), - }); - const exportTs = deps.find( - dep => - dep.specifier.startsWith("./") && - dep.specifier.endsWith("/_export.ts") - ); - - // reset the `_export.ts` module - if (exportTs) { - const fp = path.join(appDir, exportTs.specifier); - await ensureDir(path.dirname(fp)); - await Deno.writeTextFile(fp, "export default {}"); - } - - // watch server entry and its deps to restart the dev server - const emitter = createWatchFsEmitter(); - emitter.on("*", (kind, { specifier }) => { - if ( - kind === "modify" && - !specifier.endsWith("/_export.ts") && - (specifier === entry || - deps.some(dep => dep.specifier === specifier)) - ) { - console.clear(); - console.info(colors.dim("[dev] Restarting the server...")); - devProcess?.kill("SIGTERM"); - dev(args); - } - }); - - const cmd = [Deno.execPath(), "run", "-A", "--no-lock"]; - - flags.unstable && cmd.push("--unstable"); - cmd.push(serverEntry), cmd.push("--dev"); - console.log(cmd); - devProcess = Deno.run({ cmd, stderr: "inherit", stdout: "inherit" }); - await devProcess.status(); - removeWatchFsEmitter(emitter); + const flags = parseCliArgs(args || [], { + boolean: ["unstable", "A"], + }); + const serverEntry = typeof flags._[0] === "string" + ? flags._[0].startsWith("file://") ? path.fromFileUrl(flags._[0]) : path.resolve(flags._[0]) + : await findFile(builtinModuleExts.map((ext) => `server.${ext}`)); + + if (!serverEntry) { + log.fatal("[dev] No server entry found."); + return; + } + + const appDir = path.dirname(serverEntry); + if (!watched) { + log.info(colors.dim("[dev]"), "Watching for file changes..."); + watch(appDir); + watched = true; + } + + const entry = `./${path.basename(serverEntry)}`; + const code = await Deno.readTextFile(serverEntry); + const importMap = await getImportMap(); + const deps = await parseDeps(entry, code, { + importMap: JSON.stringify(importMap), + }); + const exportTs = deps.find( + (dep) => + dep.specifier.startsWith("./") && + dep.specifier.endsWith("/_export.ts"), + ); + + // reset the `_export.ts` module + if (exportTs) { + const fp = path.join(appDir, exportTs.specifier); + await ensureDir(path.dirname(fp)); + await Deno.writeTextFile(fp, "export default {}"); + } + + // watch server entry and its deps to restart the dev server + const emitter = createWatchFsEmitter(); + emitter.on("*", (kind, { specifier }) => { + if ( + kind === "modify" && + !specifier.endsWith("/_export.ts") && + (specifier === entry || + deps.some((dep) => dep.specifier === specifier)) + ) { + console.clear(); + console.info(colors.dim("[dev] Restarting the server...")); + devProcess?.kill("SIGTERM"); + dev(args); + } + }); + + const cmd = [Deno.execPath(), "run", "-A", "--no-lock"]; + + flags.unstable && cmd.push("--unstable"); + cmd.push(serverEntry), cmd.push("--dev"); + console.log(cmd); + devProcess = Deno.run({ cmd, stderr: "inherit", stdout: "inherit" }); + await devProcess.status(); + removeWatchFsEmitter(emitter); } export function handleHMR(req: Request): Response { - const { socket, response } = Deno.upgradeWebSocket(req); - const emitter = createWatchFsEmitter(); - const send = (message: Record) => { - try { - socket.send(JSON.stringify(message)); - } catch (err) { - log.warn("socket.send:", err.message); - } - }; - socket.addEventListener("close", () => { - removeWatchFsEmitter(emitter); - }); - socket.addEventListener("open", () => { - emitter.on("create", ({ specifier }) => { - const config: AlephConfig | undefined = Reflect.get( - globalThis, - "__ALEPH_CONFIG" - ); - if (config?.router) { - const reg = toRouterRegExp(config.router); - const routePattern = reg.exec(specifier); - if (routePattern) { - send({ type: "create", specifier, routePattern }); - return; - } - } - send({ type: "create", specifier }); - }); - emitter.on("remove", ({ specifier }) => { - emitter.off(`hotUpdate:${specifier}`); - send({ type: "remove", specifier }); - }); - }); - socket.addEventListener("message", e => { - if (isFilledString(e.data)) { - try { - const { type, specifier } = JSON.parse(e.data); - if (type === "hotAccept" && isFilledString(specifier)) { - emitter.on(`hotUpdate:${specifier}`, () => { - send({ type: "modify", specifier }); - }); - } - } catch (_e) { - log.error("invlid socket message:", e.data); - } - } - }); - return response; + const { socket, response } = Deno.upgradeWebSocket(req); + const emitter = createWatchFsEmitter(); + const send = (message: Record) => { + try { + socket.send(JSON.stringify(message)); + } catch (err) { + log.warn("socket.send:", err.message); + } + }; + socket.addEventListener("close", () => { + removeWatchFsEmitter(emitter); + }); + socket.addEventListener("open", () => { + emitter.on("create", ({ specifier }) => { + const config: AlephConfig | undefined = Reflect.get( + globalThis, + "__ALEPH_CONFIG", + ); + if (config?.router) { + const reg = toRouterRegExp(config.router); + const routePattern = reg.exec(specifier); + if (routePattern) { + send({ type: "create", specifier, routePattern }); + return; + } + } + send({ type: "create", specifier }); + }); + emitter.on("remove", ({ specifier }) => { + emitter.off(`hotUpdate:${specifier}`); + send({ type: "remove", specifier }); + }); + }); + socket.addEventListener("message", (e) => { + if (isFilledString(e.data)) { + try { + const { type, specifier } = JSON.parse(e.data); + if (type === "hotAccept" && isFilledString(specifier)) { + emitter.on(`hotUpdate:${specifier}`, () => { + send({ type: "modify", specifier }); + }); + } + } catch (_e) { + log.error("invlid socket message:", e.data); + } + } + }); + return response; } From 2392012149f490fe597fec9026a14172a362e264 Mon Sep 17 00:00:00 2001 From: Brock Donahue Date: Tue, 20 Jun 2023 12:20:46 -0400 Subject: [PATCH 3/6] changed react version --- deno.json | 2 +- import_map.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index f1de0e57a..9a8b49c13 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,7 @@ "./types.d.ts" ], "jsx": "react-jsx", - "jsxImportSource": "https://esm.sh/v113/react@18.2.0" + "jsxImportSource": "https://esm.sh/v126/react@18.2.0" }, "importMap": "./import_map.json", "fmt": { diff --git a/import_map.json b/import_map.json index 2288aa712..c1c944435 100644 --- a/import_map.json +++ b/import_map.json @@ -11,7 +11,7 @@ "aleph/plugins/react": "./framework/react/plugin.ts", "@unocss/core": "https://esm.sh/v113/@unocss/core@0.50.6", "@unocss/preset-uno": "https://esm.sh/v113/@unocss/preset-uno@0.50.6", - "react": "https://esm.sh/v113/react@18.2.0", + "react": "https://esm.sh/v126/react@18.2.0", "react-dom": "https://esm.sh/v113/react-dom@18.2.0", "react-dom/": "https://esm.sh/v113/react-dom@18.2.0/", "@mdx-js/react": "https://esm.sh/v113/@mdx-js/react@2.3.0" From f29b6f2bc1c319e6a44583e21354c6e83d02a818 Mon Sep 17 00:00:00 2001 From: Brock Donahue Date: Tue, 20 Jun 2023 12:41:07 -0400 Subject: [PATCH 4/6] all 113's changed --- examples/react-mdx-app/routes/_export.ts | 400 +++++++++- examples/react-mdx-app/server.ts | 28 +- framework/core/events.ts | 4 +- framework/react/refresh.ts | 22 +- import_map.json | 10 +- plugins/mdx.ts | 91 ++- server/helpers.ts | 934 +++++++++++++---------- 7 files changed, 985 insertions(+), 504 deletions(-) diff --git a/examples/react-mdx-app/routes/_export.ts b/examples/react-mdx-app/routes/_export.ts index bb479efac..c7ae5b715 100644 --- a/examples/react-mdx-app/routes/_export.ts +++ b/examples/react-mdx-app/routes/_export.ts @@ -1,41 +1,379 @@ +/** @format */ + // Exports router modules for serverless env that doesn't support the dynamic import. // This module will be updated automaticlly in develoment mode, do NOT edit it manually. // deno-fmt-ignore-file // deno-lint-ignore-file // @ts-nocheck -var u=Object.defineProperty;var c=(s,e)=>{for(var o in e)u(s,o,{get:e[o],enumerable:!0})};import*as y from"./_404.tsx";import*as k from"./_app.tsx";import*as v from"./index.tsx";import*as A from"./docs.tsx";var t={};c(t,{default:()=>b});import{Fragment as g,jsx as r,jsxs as i}from"https://esm.sh/react@18.2.0/jsx-runtime";import{useMDXComponents as h}from"https://esm.sh/v113/@mdx-js/react@2.3.0";import{Head as j}from"aleph/react";function d(s){let e=Object.assign({h1:"h1",p:"p",code:"code",pre:"pre"},h(),s.components);return i(g,{children:[r(j,{children:r("title",{children:"Get Started - Docs"})}),` -`,r(e.h1,{id:"get-started",children:"Get Started"}),` -`,i(e.p,{children:["Initialize a new project, you can pick a start template with ",r(e.code,{children:"--template"}),` flag, available templates: -`,r(e.code,{children:"[react, vue, api, yew]"})]}),` -`,r(e.pre,{children:r(e.code,{className:"hljs language-bash",children:`deno run -A https://deno.land/x/aleph@1.0.0-beta.18/init.ts -`})})]})}function f(s={}){let{wrapper:e}=Object.assign({},h(),s.components);return e?r(e,Object.assign({},s,{children:r(d,s)})):d(s)}var b=f;var l={};c(l,{default:()=>x});import{Fragment as N,jsx as n,jsxs as a}from"https://esm.sh/react@18.2.0/jsx-runtime";import{useMDXComponents as p}from"https://esm.sh/v113/@mdx-js/react@2.3.0";import{Head as w}from"aleph/react";function m(s){let e=Object.assign({h1:"h1",p:"p",strong:"strong",a:"a",blockquote:"blockquote",em:"em",code:"code",pre:"pre",span:"span"},p(),s.components);return a(N,{children:[n(w,{children:n("title",{children:"About - Docs"})}),` -`,n(e.h1,{id:"about",children:"About"}),` -`,a(e.p,{children:[n(e.strong,{children:"Aleph.js"})," (or ",n(e.strong,{children:"Aleph"})," or ",n(e.strong,{children:"\u05D0"})," or ",n(e.strong,{children:"\u963F\u83B1\u592B"}),", ",n("samp",{children:"\u02C8\u0251\u02D0l\u025Bf"}),`) is a -fullstack framework in `,n(e.a,{href:"https://deno.land",children:"Deno"}),". Inspired by ",n(e.a,{href:"https://nextjs.org",children:"Next.js"}),", ",n(e.a,{href:"https://remix.run",children:"Remix"})," and ",n(e.a,{href:"https://vitejs.dev",children:"Vite"}),"."]}),` -`,a(e.blockquote,{children:[` -`,a(e.p,{children:["The name is taken from the book ",n(e.a,{href:"http://phinnweb.org/links/literature/borges/aleph.html",children:n(e.em,{children:"The Aleph"})})," by ",n(e.strong,{children:"Jorge Luis Borges"}),"."]}),` -`]}),` -`,a(e.p,{children:["Aleph.js is modern framework that doesn't need ",n(e.strong,{children:"webpack"}),` or other bundler -since it uses the `,n(e.a,{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules",children:"ES Module"}),` syntax during development. Every module only needs +var u = Object.defineProperty; +var c = (s, e) => { + for (var o in e) u(s, o, { get: e[o], enumerable: !0 }); +}; +import * as y from "./_404.tsx"; +import * as k from "./_app.tsx"; +import * as v from "./index.tsx"; +import * as A from "./docs.tsx"; +var t = {}; +c(t, { default: () => b }); +import { + Fragment as g, + jsx as r, + jsxs as i, +} from "https://esm.sh/react@18.2.0/jsx-runtime"; +import { useMDXComponents as h } from "https://esm.sh/v126/@mdx-js/react@2.3.0"; +import { Head as j } from "aleph/react"; +function d(s) { + let e = Object.assign( + { h1: "h1", p: "p", code: "code", pre: "pre" }, + h(), + s.components + ); + return i(g, { + children: [ + r(j, { children: r("title", { children: "Get Started - Docs" }) }), + ` +`, + r(e.h1, { id: "get-started", children: "Get Started" }), + ` +`, + i(e.p, { + children: [ + "Initialize a new project, you can pick a start template with ", + r(e.code, { children: "--template" }), + ` flag, available templates: +`, + r(e.code, { children: "[react, vue, api, yew]" }), + ], + }), + ` +`, + r(e.pre, { + children: r(e.code, { + className: "hljs language-bash", + children: `deno run -A https://deno.land/x/aleph@1.0.0-beta.18/init.ts +`, + }), + }), + ], + }); +} +function f(s = {}) { + let { wrapper: e } = Object.assign({}, h(), s.components); + return e ? r(e, Object.assign({}, s, { children: r(d, s) })) : d(s); +} +var b = f; +var l = {}; +c(l, { default: () => x }); +import { + Fragment as N, + jsx as n, + jsxs as a, +} from "https://esm.sh/react@18.2.0/jsx-runtime"; +import { useMDXComponents as p } from "https://esm.sh/v126/@mdx-js/react@2.3.0"; +import { Head as w } from "aleph/react"; +function m(s) { + let e = Object.assign( + { + h1: "h1", + p: "p", + strong: "strong", + a: "a", + blockquote: "blockquote", + em: "em", + code: "code", + pre: "pre", + span: "span", + }, + p(), + s.components + ); + return a(N, { + children: [ + n(w, { children: n("title", { children: "About - Docs" }) }), + ` +`, + n(e.h1, { id: "about", children: "About" }), + ` +`, + a(e.p, { + children: [ + n(e.strong, { children: "Aleph.js" }), + " (or ", + n(e.strong, { children: "Aleph" }), + " or ", + n(e.strong, { children: "\u05D0" }), + " or ", + n(e.strong, { children: "\u963F\u83B1\u592B" }), + ", ", + n("samp", { children: "\u02C8\u0251\u02D0l\u025Bf" }), + `) is a +fullstack framework in `, + n(e.a, { href: "https://deno.land", children: "Deno" }), + ". Inspired by ", + n(e.a, { href: "https://nextjs.org", children: "Next.js" }), + ", ", + n(e.a, { href: "https://remix.run", children: "Remix" }), + " and ", + n(e.a, { href: "https://vitejs.dev", children: "Vite" }), + ".", + ], + }), + ` +`, + a(e.blockquote, { + children: [ + ` +`, + a(e.p, { + children: [ + "The name is taken from the book ", + n(e.a, { + href: "http://phinnweb.org/links/literature/borges/aleph.html", + children: n(e.em, { children: "The Aleph" }), + }), + " by ", + n(e.strong, { children: "Jorge Luis Borges" }), + ".", + ], + }), + ` +`, + ], + }), + ` +`, + a(e.p, { + children: [ + "Aleph.js is modern framework that doesn't need ", + n(e.strong, { children: "webpack" }), + ` or other bundler +since it uses the `, + n(e.a, { + href: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules", + children: "ES Module", + }), + ` syntax during development. Every module only needs to be compiled once, when a module changes, Aleph.js just needs to re-compile -that single module. There is no time wasted `,n(e.em,{children:"re-bundling"}),` everytime a change is -made. This, along with Hot Module Replacement (`,n(e.strong,{children:"HMR"}),") and ",n(e.strong,{children:"Fast Refresh"}),`, -leads to instant updates in the browser.`]}),` -`,a(e.p,{children:["Aleph.js uses modern tools to build your app. It transpiles code using ",n(e.a,{href:"https://swc.rs",children:"swc"}),` in -WASM with high performance, and bundles modules with `,n(e.a,{href:"https://github.com/evanw/esbuild",children:"esbuild"}),` at optimization -time extremely fast.`]}),` -`,a(e.p,{children:["Aleph.js works on top of ",n(e.strong,{children:"Deno"}),", a ",n(e.em,{children:"simple"}),", ",n(e.em,{children:"modern"})," and ",n(e.em,{children:"secure"}),` runtime for +that single module. There is no time wasted `, + n(e.em, { children: "re-bundling" }), + ` everytime a change is +made. This, along with Hot Module Replacement (`, + n(e.strong, { children: "HMR" }), + ") and ", + n(e.strong, { children: "Fast Refresh" }), + `, +leads to instant updates in the browser.`, + ], + }), + ` +`, + a(e.p, { + children: [ + "Aleph.js uses modern tools to build your app. It transpiles code using ", + n(e.a, { href: "https://swc.rs", children: "swc" }), + ` in +WASM with high performance, and bundles modules with `, + n(e.a, { + href: "https://github.com/evanw/esbuild", + children: "esbuild", + }), + ` at optimization +time extremely fast.`, + ], + }), + ` +`, + a(e.p, { + children: [ + "Aleph.js works on top of ", + n(e.strong, { children: "Deno" }), + ", a ", + n(e.em, { children: "simple" }), + ", ", + n(e.em, { children: "modern" }), + " and ", + n(e.em, { children: "secure" }), + ` runtime for JavaScript and TypeScript. All dependencies are imported using URLs, and managed -by Deno cache system. No `,n(e.code,{children:"package.json"})," and ",n(e.code,{children:"node_modules"})," directory needed."]}),` -`,n(e.pre,{children:a(e.code,{className:"hljs language-js",children:[n(e.span,{className:"hljs-keyword",children:"import"})," ",n(e.span,{className:"hljs-title class_",children:"React"})," ",n(e.span,{className:"hljs-keyword",children:"from"})," ",n(e.span,{className:"hljs-string",children:"'https://esm.sh/react'"}),` -`,n(e.span,{className:"hljs-keyword",children:"import"})," ",n(e.span,{className:"hljs-title class_",children:"Logo"})," ",n(e.span,{className:"hljs-keyword",children:"from"})," ",n(e.span,{className:"hljs-string",children:"'../components/logo.tsx'"}),` +by Deno cache system. No `, + n(e.code, { children: "package.json" }), + " and ", + n(e.code, { children: "node_modules" }), + " directory needed.", + ], + }), + ` +`, + n(e.pre, { + children: a(e.code, { + className: "hljs language-js", + children: [ + n(e.span, { + className: "hljs-keyword", + children: "import", + }), + " ", + n(e.span, { + className: "hljs-title class_", + children: "React", + }), + " ", + n(e.span, { + className: "hljs-keyword", + children: "from", + }), + " ", + n(e.span, { + className: "hljs-string", + children: "'https://esm.sh/react'", + }), + ` +`, + n(e.span, { + className: "hljs-keyword", + children: "import", + }), + " ", + n(e.span, { + className: "hljs-title class_", + children: "Logo", + }), + " ", + n(e.span, { + className: "hljs-keyword", + children: "from", + }), + " ", + n(e.span, { + className: "hljs-string", + children: "'../components/logo.tsx'", + }), + ` -`,n(e.span,{className:"hljs-keyword",children:"export"})," ",n(e.span,{className:"hljs-keyword",children:"default"})," ",n(e.span,{className:"hljs-keyword",children:"function"})," ",n(e.span,{className:"hljs-title function_",children:"Home"}),"(",n(e.span,{className:"hljs-params"}),`) { - `,n(e.span,{className:"hljs-keyword",children:"return"}),` ( - `,a(e.span,{className:"xml",children:[a(e.span,{className:"hljs-tag",children:["<",n(e.span,{className:"hljs-name",children:"div"}),">"]}),` - `,a(e.span,{className:"hljs-tag",children:["<",n(e.span,{className:"hljs-name",children:"Logo"})," />"]}),` - `,a(e.span,{className:"hljs-tag",children:["<",n(e.span,{className:"hljs-name",children:"h1"}),">"]}),"Hello World!",a(e.span,{className:"hljs-tag",children:[""]}),` - `,a(e.span,{className:"hljs-tag",children:[""]})]}),` +`, + n(e.span, { + className: "hljs-keyword", + children: "export", + }), + " ", + n(e.span, { + className: "hljs-keyword", + children: "default", + }), + " ", + n(e.span, { + className: "hljs-keyword", + children: "function", + }), + " ", + n(e.span, { + className: "hljs-title function_", + children: "Home", + }), + "(", + n(e.span, { className: "hljs-params" }), + `) { + `, + n(e.span, { + className: "hljs-keyword", + children: "return", + }), + ` ( + `, + a(e.span, { + className: "xml", + children: [ + a(e.span, { + className: "hljs-tag", + children: [ + "<", + n(e.span, { + className: "hljs-name", + children: "div", + }), + ">", + ], + }), + ` + `, + a(e.span, { + className: "hljs-tag", + children: [ + "<", + n(e.span, { + className: "hljs-name", + children: "Logo", + }), + " />", + ], + }), + ` + `, + a(e.span, { + className: "hljs-tag", + children: [ + "<", + n(e.span, { + className: "hljs-name", + children: "h1", + }), + ">", + ], + }), + "Hello World!", + a(e.span, { + className: "hljs-tag", + children: [ + "", + ], + }), + ` + `, + a(e.span, { + className: "hljs-tag", + children: [ + "", + ], + }), + ], + }), + ` ) } -`]})})]})}function _(s={}){let{wrapper:e}=Object.assign({},p(),s.components);return e?n(e,Object.assign({},s,{children:n(m,s)})):m(s)}var x=_;var O={"/_404":y,"/_app":k,"/":v,"/docs":A,"/docs/get-started":t,"/docs/index":l,depGraph:{"modules":[{"specifier":"./routes/docs/get-started.mdx"},{"specifier":"./routes/docs/index.mdx"}]}};export{O as default}; +`, + ], + }), + }), + ], + }); +} +function _(s = {}) { + let { wrapper: e } = Object.assign({}, p(), s.components); + return e ? n(e, Object.assign({}, s, { children: n(m, s) })) : m(s); +} +var x = _; +var O = { + "/_404": y, + "/_app": k, + "/": v, + "/docs": A, + "/docs/get-started": t, + "/docs/index": l, + depGraph: { + modules: [ + { specifier: "./routes/docs/get-started.mdx" }, + { specifier: "./routes/docs/index.mdx" }, + ], + }, +}; +export { O as default }; diff --git a/examples/react-mdx-app/server.ts b/examples/react-mdx-app/server.ts index 43f574070..e0cc04c63 100644 --- a/examples/react-mdx-app/server.ts +++ b/examples/react-mdx-app/server.ts @@ -1,3 +1,5 @@ +/** @format */ + import { serve } from "aleph/server"; import denoDeploy from "aleph/plugins/deploy"; import react from "aleph/plugins/react"; @@ -5,19 +7,19 @@ import mdx from "aleph/plugins/mdx"; import modules from "./routes/_export.ts"; // check https://mdxjs.com/docs/extending-mdx -import remarkFrontmatter from "https://esm.sh/v113/remark-frontmatter@4.0.1"; -import remarkGFM from "https://esm.sh/v113/remark-gfm@3.0.1"; -import rehypeHighlight from "https://esm.sh/v113/rehype-highlight@5.0.2"; -import rehypeSlug from "https://esm.sh/v113/rehype-slug@5.0.1"; +import remarkFrontmatter from "https://esm.sh/v126/remark-frontmatter@4.0.1"; +import remarkGFM from "https://esm.sh/v126/remark-gfm@3.0.1"; +import rehypeHighlight from "https://esm.sh/v126/rehype-highlight@5.0.2"; +import rehypeSlug from "https://esm.sh/v126/rehype-slug@5.0.1"; serve({ - plugins: [ - denoDeploy({ modules }), - mdx({ - remarkPlugins: [remarkFrontmatter, remarkGFM], - rehypePlugins: [rehypeHighlight, rehypeSlug], - providerImportSource: "@mdx-js/react", - }), - react({ ssr: true }), - ], + plugins: [ + denoDeploy({ modules }), + mdx({ + remarkPlugins: [remarkFrontmatter, remarkGFM], + rehypePlugins: [rehypeHighlight, rehypeSlug], + providerImportSource: "@mdx-js/react", + }), + react({ ssr: true }), + ], }); diff --git a/framework/core/events.ts b/framework/core/events.ts index 03077fd85..a91c00842 100644 --- a/framework/core/events.ts +++ b/framework/core/events.ts @@ -1,4 +1,6 @@ -import mitt from "https://esm.sh/v113/mitt@3.0.0"; +/** @format */ + +import mitt from "https://esm.sh/v126/mitt@3.0.0"; // shared event emitter for client(browser) export default mitt>>(); diff --git a/framework/react/refresh.ts b/framework/react/refresh.ts index e50fabda4..1c10fab7c 100644 --- a/framework/react/refresh.ts +++ b/framework/react/refresh.ts @@ -1,24 +1,26 @@ +/** @format */ + // react-refresh // @link https://github.com/facebook/react/issues/16604#issuecomment-528663101 -import runtime from "https://esm.sh/v113/react-refresh@0.14.0/runtime"; +import runtime from "https://esm.sh/v126/react-refresh@0.14.0/runtime"; let timer: number | null; const refresh = () => { - if (timer !== null) { - clearTimeout(timer); - } - timer = setTimeout(() => { - runtime.performReactRefresh(); - timer = null; - }, 50); + if (timer !== null) { + clearTimeout(timer); + } + timer = setTimeout(() => { + runtime.performReactRefresh(); + timer = null; + }, 50); }; runtime.injectIntoGlobalHook(window); Object.assign(window, { - $RefreshReg$: () => {}, - $RefreshSig$: () => (type: unknown) => type, + $RefreshReg$: () => {}, + $RefreshSig$: () => (type: unknown) => type, }); export { refresh as __REACT_REFRESH__, runtime as __REACT_REFRESH_RUNTIME__ }; diff --git a/import_map.json b/import_map.json index c1c944435..8fc9d7be1 100644 --- a/import_map.json +++ b/import_map.json @@ -9,11 +9,11 @@ "aleph/plugins/mdx": "./plugins/mdx.ts", "aleph/react": "./framework/react/mod.ts", "aleph/plugins/react": "./framework/react/plugin.ts", - "@unocss/core": "https://esm.sh/v113/@unocss/core@0.50.6", - "@unocss/preset-uno": "https://esm.sh/v113/@unocss/preset-uno@0.50.6", + "@unocss/core": "https://esm.sh/v126/@unocss/core@0.50.6", + "@unocss/preset-uno": "https://esm.sh/v126/@unocss/preset-uno@0.50.6", "react": "https://esm.sh/v126/react@18.2.0", - "react-dom": "https://esm.sh/v113/react-dom@18.2.0", - "react-dom/": "https://esm.sh/v113/react-dom@18.2.0/", - "@mdx-js/react": "https://esm.sh/v113/@mdx-js/react@2.3.0" + "react-dom": "https://esm.sh/v126/react-dom@18.2.0", + "react-dom/": "https://esm.sh/v126/react-dom@18.2.0/", + "@mdx-js/react": "https://esm.sh/v126/@mdx-js/react@2.3.0" } } diff --git a/plugins/mdx.ts b/plugins/mdx.ts index a98ee63f7..b16f85ab7 100644 --- a/plugins/mdx.ts +++ b/plugins/mdx.ts @@ -1,44 +1,65 @@ -import { compile, type CompileOptions } from "https://esm.sh/v113/@mdx-js/mdx@2.3.0"; -import type { ModuleLoader, ModuleLoaderEnv, ModuleLoaderOutput, Plugin } from "../server/types.ts"; +/** @format */ + +import { + compile, + type CompileOptions, +} from "https://esm.sh/v126/@mdx-js/mdx@2.3.0"; +import type { + ModuleLoader, + ModuleLoaderEnv, + ModuleLoaderOutput, + Plugin, +} from "../server/types.ts"; export class MDXLoader implements ModuleLoader { - #options: CompileOptions; + #options: CompileOptions; - constructor(options?: CompileOptions) { - this.#options = options ?? {}; - } + constructor(options?: CompileOptions) { + this.#options = options ?? {}; + } - test(path: string): boolean { - const exts = this.#options?.mdxExtensions ?? ["mdx"]; - return exts.some((ext) => path.endsWith(`.${ext}`)); - } + test(path: string): boolean { + const exts = this.#options?.mdxExtensions ?? ["mdx"]; + return exts.some(ext => path.endsWith(`.${ext}`)); + } - async load(specifier: string, content: string, env: ModuleLoaderEnv): Promise { - const ret = await compile( - { path: specifier, value: content }, - { - jsxImportSource: this.#options.jsxImportSource ?? env.jsxConfig?.jsxImportSource, - ...this.#options, - providerImportSource: this.#options.providerImportSource - ? env.importMap?.imports[this.#options.providerImportSource] ?? this.#options.providerImportSource - : undefined, - development: env.isDev, - }, - ); - return { - code: ret.toString(), - lang: "js", - }; - } + async load( + specifier: string, + content: string, + env: ModuleLoaderEnv + ): Promise { + const ret = await compile( + { path: specifier, value: content }, + { + jsxImportSource: + this.#options.jsxImportSource ?? + env.jsxConfig?.jsxImportSource, + ...this.#options, + providerImportSource: this.#options.providerImportSource + ? env.importMap?.imports[ + this.#options.providerImportSource + ] ?? this.#options.providerImportSource + : undefined, + development: env.isDev, + } + ); + return { + code: ret.toString(), + lang: "js", + }; + } } export default function MdxPlugin(options?: CompileOptions): Plugin { - return { - name: "mdx", - setup(aleph) { - const exts = options?.mdxExtensions ?? ["mdx"]; - aleph.loaders = [new MDXLoader(options), ...aleph.loaders ?? []]; - aleph.router = { ...aleph.router, exts: [...exts, ...(aleph.router?.exts ?? [])] }; - }, - }; + return { + name: "mdx", + setup(aleph) { + const exts = options?.mdxExtensions ?? ["mdx"]; + aleph.loaders = [new MDXLoader(options), ...(aleph.loaders ?? [])]; + aleph.router = { + ...aleph.router, + exts: [...exts, ...(aleph.router?.exts ?? [])], + }; + }, + }; } diff --git a/server/helpers.ts b/server/helpers.ts index 3bb56bf66..d406e38f5 100644 --- a/server/helpers.ts +++ b/server/helpers.ts @@ -1,160 +1,191 @@ -import { isFilledArray, isFilledString, isLikelyHttpURL, isPlainObject, trimSuffix } from "../shared/util.ts"; +/** @format */ + +import { + isFilledArray, + isFilledString, + isLikelyHttpURL, + isPlainObject, + trimSuffix, +} from "../shared/util.ts"; import { isCanary, VERSION } from "../version.ts"; import { cacheFetch } from "./cache.ts"; import { jsonc, path, type TransformOptions } from "./deps.ts"; import log from "./log.ts"; import { getContentType } from "./media_type.ts"; -import type { AlephConfig, CookieOptions, ImportMap, JSXConfig } from "./types.ts"; +import type { + AlephConfig, + CookieOptions, + ImportMap, + JSXConfig, +} from "./types.ts"; export const regJsxFile = /\.(jsx|tsx|mdx)$/; export const regFullVersion = /@\d+\.\d+\.\d+/; export const builtinModuleExts = ["tsx", "ts", "mts", "jsx", "js", "mjs"]; /** Stores and returns the `fn` output in the `globalThis` object. */ -export async function globalIt(name: string, fn: () => Promise): Promise { - const v: T | undefined = Reflect.get(globalThis, name); - if (v !== undefined) { - if (v instanceof Promise) { - const ret = await v; - Reflect.set(globalThis, name, ret); - return ret; - } - return v; - } - const ret = fn(); - if (ret !== undefined) { - Reflect.set(globalThis, name, ret); - } - return await ret.then((v) => { - Reflect.set(globalThis, name, v); - return v; - }); +export async function globalIt( + name: string, + fn: () => Promise +): Promise { + const v: T | undefined = Reflect.get(globalThis, name); + if (v !== undefined) { + if (v instanceof Promise) { + const ret = await v; + Reflect.set(globalThis, name, ret); + return ret; + } + return v; + } + const ret = fn(); + if (ret !== undefined) { + Reflect.set(globalThis, name, ret); + } + return await ret.then(v => { + Reflect.set(globalThis, name, v); + return v; + }); } /** Stores and returns the `fn` output in the `globalThis` object synchronously. */ export function globalItSync(name: string, fn: () => T): T { - const v: T | undefined = Reflect.get(globalThis, name); - if (v !== undefined) { - return v; - } - const ret = fn(); - if (ret !== undefined) { - Reflect.set(globalThis, name, ret); - } - return ret; + const v: T | undefined = Reflect.get(globalThis, name); + if (v !== undefined) { + return v; + } + const ret = fn(); + if (ret !== undefined) { + Reflect.set(globalThis, name, ret); + } + return ret; } export function getAppDir() { - return globalItSync( - "__ALEPH_APP_DIR", - () => Deno.mainModule ? path.dirname(path.fromFileUrl(Deno.mainModule)) : Deno.cwd(), - ); + return globalItSync("__ALEPH_APP_DIR", () => + Deno.mainModule + ? path.dirname(path.fromFileUrl(Deno.mainModule)) + : Deno.cwd() + ); } /** Get the module URI of Aleph.js */ export function getAlephPkgUri(): string { - return globalItSync("__ALEPH_PKG_URI", () => { - const uriEnv = Deno.env.get("ALEPH_PKG_URI"); - if (uriEnv) { - return uriEnv; - } - if (import.meta.url.startsWith("file://")) { - return "https://aleph"; - } - return `https://deno.land/x/${isCanary ? "aleph_canary" : "aleph"}@${VERSION}`; - }); + return globalItSync("__ALEPH_PKG_URI", () => { + const uriEnv = Deno.env.get("ALEPH_PKG_URI"); + if (uriEnv) { + return uriEnv; + } + if (import.meta.url.startsWith("file://")) { + return "https://aleph"; + } + return `https://deno.land/x/${ + isCanary ? "aleph_canary" : "aleph" + }@${VERSION}`; + }); } /** Get Aleph.js package URI. */ export function getAlephConfig(): AlephConfig | undefined { - return Reflect.get(globalThis, "__ALEPH_CONFIG"); + return Reflect.get(globalThis, "__ALEPH_CONFIG"); } /** Get the import maps. */ export async function getImportMap(appDir?: string): Promise { - return await globalIt("__ALEPH_IMPORT_MAP", () => loadImportMap(appDir)); + return await globalIt("__ALEPH_IMPORT_MAP", () => loadImportMap(appDir)); } /** Get the jsx config. */ export async function getJSXConfig(appDir?: string): Promise { - return await globalIt("__ALEPH_JSX_CONFIG", () => loadJSXConfig(appDir)); + return await globalIt("__ALEPH_JSX_CONFIG", () => loadJSXConfig(appDir)); } /** Get the deployment ID. */ export function getDeploymentId(): string | undefined { - const id = Deno.env.get("DENO_DEPLOYMENT_ID"); - if (id) { - return id; - } - - // or use git latest commit hash - return globalItSync("__ALEPH_DEPLOYMENT_ID", () => { - try { - if (!Deno.args.includes("--dev")) { - const gitDir = path.join(Deno.cwd(), ".git"); - if (Deno.statSync(gitDir).isDirectory) { - const head = Deno.readTextFileSync(path.join(gitDir, "HEAD")); - if (head.startsWith("ref: ")) { - const ref = head.slice(5).trim(); - const refFile = path.join(gitDir, ref); - return Deno.readTextFileSync(refFile).trim().slice(0, 8); - } - } - } - } catch { - // ignore - } - return null; - }) ?? undefined; + const id = Deno.env.get("DENO_DEPLOYMENT_ID"); + if (id) { + return id; + } + + // or use git latest commit hash + return ( + globalItSync("__ALEPH_DEPLOYMENT_ID", () => { + try { + if (!Deno.args.includes("--dev")) { + const gitDir = path.join(Deno.cwd(), ".git"); + if (Deno.statSync(gitDir).isDirectory) { + const head = Deno.readTextFileSync( + path.join(gitDir, "HEAD") + ); + if (head.startsWith("ref: ")) { + const ref = head.slice(5).trim(); + const refFile = path.join(gitDir, ref); + return Deno.readTextFileSync(refFile) + .trim() + .slice(0, 8); + } + } + } + } catch { + // ignore + } + return null; + }) ?? undefined + ); } -export function cookieHeader(name: string, value: string, options?: CookieOptions): string { - const cookie = [`${name}=${value}`]; - if (options) { - if (options.expires) { - cookie.push(`Expires=${new Date(options.expires).toUTCString()}`); - } - if (options.maxAge) { - cookie.push(`Max-Age=${options.maxAge}`); - } - if (options.domain) { - cookie.push(`Domain=${options.domain}`); - } - if (options.path) { - cookie.push(`Path=${options.path}`); - } - if (options.httpOnly) { - cookie.push("HttpOnly"); - } - if (options.secure) { - cookie.push("Secure"); - } - if (options.sameSite) { - cookie.push(`SameSite=${options.sameSite}`); - } - } - return cookie.join("; "); +export function cookieHeader( + name: string, + value: string, + options?: CookieOptions +): string { + const cookie = [`${name}=${value}`]; + if (options) { + if (options.expires) { + cookie.push(`Expires=${new Date(options.expires).toUTCString()}`); + } + if (options.maxAge) { + cookie.push(`Max-Age=${options.maxAge}`); + } + if (options.domain) { + cookie.push(`Domain=${options.domain}`); + } + if (options.path) { + cookie.push(`Path=${options.path}`); + } + if (options.httpOnly) { + cookie.push("HttpOnly"); + } + if (options.secure) { + cookie.push("Secure"); + } + if (options.sameSite) { + cookie.push(`SameSite=${options.sameSite}`); + } + } + return cookie.join("; "); } export function toResponse(v: unknown, init?: ResponseInit): Response { - if ( - v instanceof ArrayBuffer || - v instanceof Uint8Array || - v instanceof ReadableStream - ) { - return new Response(v, init); - } - if (v instanceof Blob || v instanceof File) { - const headers = new Headers(init?.headers); - headers.set("Content-Type", v.type); - headers.set("Content-Length", v.size.toString()); - return new Response(v, { ...init, headers }); - } - try { - return Response.json(v, init); - } catch (_) { - return new Response("Invalid response type: " + typeof v, { status: 500 }); - } + if ( + v instanceof ArrayBuffer || + v instanceof Uint8Array || + v instanceof ReadableStream + ) { + return new Response(v, init); + } + if (v instanceof Blob || v instanceof File) { + const headers = new Headers(init?.headers); + headers.set("Content-Type", v.type); + headers.set("Content-Length", v.size.toString()); + return new Response(v, { ...init, headers }); + } + try { + return Response.json(v, init); + } catch (_) { + return new Response("Invalid response type: " + typeof v, { + status: 500, + }); + } } /** @@ -162,357 +193,442 @@ export function toResponse(v: unknown, init?: ResponseInit): Response { * e.g. `https://esm.sh/react@18.2.0?dev` -> `/-/esm.sh/react@18.2.0?dev` */ export function toLocalPath(url: string): string { - if (isLikelyHttpURL(url)) { - let { hostname, pathname, port, protocol, search } = new URL(url); - const isHttp = protocol === "http:"; - if ((isHttp && port === "80") || (protocol === "https:" && port === "443")) { - port = ""; - } - return [ - "/-/", - isHttp && "http_", - hostname, - port && "_" + port, - trimSuffix(pathname, "/"), - search, - ].filter(Boolean).join(""); - } - return url; + if (isLikelyHttpURL(url)) { + let { hostname, pathname, port, protocol, search } = new URL(url); + const isHttp = protocol === "http:"; + if ( + (isHttp && port === "80") || + (protocol === "https:" && port === "443") + ) { + port = ""; + } + return [ + "/-/", + isHttp && "http_", + hostname, + port && "_" + port, + trimSuffix(pathname, "/"), + search, + ] + .filter(Boolean) + .join(""); + } + return url; } /** * Restore the remote url from local path. - * e.g. `/-/esm.sh/react@18.2.0` -> `https://esm.sh/v113/react@18.2.0` + * e.g. `/-/esm.sh/react@18.2.0` -> `https://esm.sh/v126/react@18.2.0` */ export function restoreUrl(pathname: string): string { - let [h, ...rest] = pathname.substring(3).split("/"); - let protocol = "https"; - if (h.startsWith("http_")) { - h = h.substring(5); - protocol = "http"; - } - const [host, port] = h.split("_"); - return `${protocol}://${host}${port ? ":" + port : ""}/${rest.join("/")}`; + let [h, ...rest] = pathname.substring(3).split("/"); + let protocol = "https"; + if (h.startsWith("http_")) { + h = h.substring(5); + protocol = "http"; + } + const [host, port] = h.split("_"); + return `${protocol}://${host}${port ? ":" + port : ""}/${rest.join("/")}`; } /** Check if the url is a npm package from esm.sh */ export function isNpmPkg(url: string) { - return url.startsWith("https://esm.sh/") && !url.endsWith(".js") && !url.endsWith(".css"); + return ( + url.startsWith("https://esm.sh/") && + !url.endsWith(".js") && + !url.endsWith(".css") + ); } /** Find config file in the `appDir` if exits, or find in current working directory. */ -async function findConfigFile(filenames: string[], appDir?: string): Promise { - let denoConfigFile: string | undefined; - if (appDir) { - denoConfigFile = await findFile(filenames, appDir); - } - // find config file in current working directory - if (!denoConfigFile) { - denoConfigFile = await findFile(filenames); - } - return denoConfigFile; +async function findConfigFile( + filenames: string[], + appDir?: string +): Promise { + let denoConfigFile: string | undefined; + if (appDir) { + denoConfigFile = await findFile(filenames, appDir); + } + // find config file in current working directory + if (!denoConfigFile) { + denoConfigFile = await findFile(filenames); + } + return denoConfigFile; } /** Check whether or not the given path exists as a directory. */ export async function existsDir(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isDirectory; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isDirectory; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } /** Check whether or not the given path exists as regular file. */ export async function existsFile(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isFile; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isFile; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } const { basename, dirname, fromFileUrl, join } = path; /** Find file in the `cwd` directory. */ -export async function findFile(filenames: string[], cwd = Deno.cwd()): Promise { - for (const filename of filenames) { - const fullPath = join(cwd, filename); - if (await existsFile(fullPath)) { - return fullPath; - } - } +export async function findFile( + filenames: string[], + cwd = Deno.cwd() +): Promise { + for (const filename of filenames) { + const fullPath = join(cwd, filename); + if (await existsFile(fullPath)) { + return fullPath; + } + } } /** Get files in the directory. */ export async function getFiles( - dir: string, - filter?: (filename: string) => boolean, - __path: string[] = [], + dir: string, + filter?: (filename: string) => boolean, + __path: string[] = [] ): Promise { - const list: string[] = []; - if (await existsDir(dir)) { - for await (const dirEntry of Deno.readDir(dir)) { - if (dirEntry.isDirectory) { - list.push(...await getFiles(join(dir, dirEntry.name), filter, [...__path, dirEntry.name])); - } else { - const filename = [".", ...__path, dirEntry.name].join("/"); - if (!filter || filter(filename)) { - list.push(filename); - } - } - } - } - return list; + const list: string[] = []; + if (await existsDir(dir)) { + for await (const dirEntry of Deno.readDir(dir)) { + if (dirEntry.isDirectory) { + list.push( + ...(await getFiles(join(dir, dirEntry.name), filter, [ + ...__path, + dirEntry.name, + ])) + ); + } else { + const filename = [".", ...__path, dirEntry.name].join("/"); + if (!filter || filter(filename)) { + list.push(filename); + } + } + } + } + return list; } /** Watch the directory and its subdirectories. */ -export async function watchFs(rootDir: string, listener: (kind: "create" | "remove" | "modify", path: string) => void) { - const timers = new Map(); - const debounce = (id: string, callback: () => void, delay: number) => { - if (timers.has(id)) { - clearTimeout(timers.get(id)!); - } - timers.set( - id, - setTimeout(() => { - timers.delete(id); - callback(); - }, delay), - ); - }; - const reIgnore = /[\/\\](\.git(hub)?|\.vscode|vendor|node_modules|dist|out(put)?|target)[\/\\]/; - const ignore = (path: string) => reIgnore.test(path) || path.endsWith(".DS_Store"); - const allFiles = new Set( - (await getFiles(rootDir)).map((name) => join(rootDir, name)).filter((path) => !ignore(path)), - ); - for await (const { kind, paths } of Deno.watchFs(rootDir, { recursive: true })) { - if (kind !== "create" && kind !== "remove" && kind !== "modify") { - continue; - } - for (const path of paths) { - if (ignore(path)) { - continue; - } - debounce(kind + path, async () => { - try { - await Deno.lstat(path); - if (!allFiles.has(path)) { - allFiles.add(path); - listener("create", path); - } else { - listener("modify", path); - } - } catch (error) { - if (error instanceof Deno.errors.NotFound) { - allFiles.delete(path); - listener("remove", path); - } else { - console.warn("watchFs:", error); - } - } - }, 100); - } - } +export async function watchFs( + rootDir: string, + listener: (kind: "create" | "remove" | "modify", path: string) => void +) { + const timers = new Map(); + const debounce = (id: string, callback: () => void, delay: number) => { + if (timers.has(id)) { + clearTimeout(timers.get(id)!); + } + timers.set( + id, + setTimeout(() => { + timers.delete(id); + callback(); + }, delay) + ); + }; + const reIgnore = + /[\/\\](\.git(hub)?|\.vscode|vendor|node_modules|dist|out(put)?|target)[\/\\]/; + const ignore = (path: string) => + reIgnore.test(path) || path.endsWith(".DS_Store"); + const allFiles = new Set( + (await getFiles(rootDir)) + .map(name => join(rootDir, name)) + .filter(path => !ignore(path)) + ); + for await (const { kind, paths } of Deno.watchFs(rootDir, { + recursive: true, + })) { + if (kind !== "create" && kind !== "remove" && kind !== "modify") { + continue; + } + for (const path of paths) { + if (ignore(path)) { + continue; + } + debounce( + kind + path, + async () => { + try { + await Deno.lstat(path); + if (!allFiles.has(path)) { + allFiles.add(path); + listener("create", path); + } else { + listener("modify", path); + } + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + allFiles.delete(path); + listener("remove", path); + } else { + console.warn("watchFs:", error); + } + } + }, + 100 + ); + } + } } /** Fetch source code from fs/cdn/cache. */ export async function fetchCode( - specifier: string, - target?: TransformOptions["target"], + specifier: string, + target?: TransformOptions["target"] ): Promise<[code: string, contentType: string]> { - if (isLikelyHttpURL(specifier)) { - const url = new URL(specifier); - if (url.host === "aleph") { - return [ - await Deno.readTextFile(fromFileUrl(new URL(".." + url.pathname, import.meta.url))), - getContentType(url.pathname), - ]; - } - if (url.hostname === "esm.sh") { - if (target && !url.pathname.includes(`/${target}/`) && !url.searchParams.has("target")) { - url.searchParams.set("target", target); - } - } - const res = await cacheFetch(url.href); - if (res.status >= 400) { - throw new Error(`fetch ${url.href}: ${res.status} - ${res.statusText}`); - } - return [await res.text(), res.headers.get("Content-Type") || getContentType(url.pathname)]; - } - - return [await Deno.readTextFile(path.join(getAppDir(), specifier)), getContentType(specifier)]; + if (isLikelyHttpURL(specifier)) { + const url = new URL(specifier); + if (url.host === "aleph") { + return [ + await Deno.readTextFile( + fromFileUrl(new URL(".." + url.pathname, import.meta.url)) + ), + getContentType(url.pathname), + ]; + } + if (url.hostname === "esm.sh") { + if ( + target && + !url.pathname.includes(`/${target}/`) && + !url.searchParams.has("target") + ) { + url.searchParams.set("target", target); + } + } + const res = await cacheFetch(url.href); + if (res.status >= 400) { + throw new Error( + `fetch ${url.href}: ${res.status} - ${res.statusText}` + ); + } + return [ + await res.text(), + res.headers.get("Content-Type") || getContentType(url.pathname), + ]; + } + + return [ + await Deno.readTextFile(path.join(getAppDir(), specifier)), + getContentType(specifier), + ]; } /** Load the JSX config base the given import maps and the existing deno config. */ export async function loadJSXConfig(appDir?: string): Promise { - const jsxConfig: JSXConfig = {}; - const denoConfigFile = await findConfigFile(["deno.jsonc", "deno.json", "tsconfig.json"], appDir); - if (denoConfigFile) { - try { - const { compilerOptions } = await parseJSONFile(denoConfigFile); - const { - jsx = "react", - jsxFactory = "React.createElement", - jsxFragmentFactory = "React.createElement", - jsxImportSource, - } = (compilerOptions ?? {}) as Record; - if (jsx === "preserve") { - jsxConfig.jsx = "preserve"; - } else if ((jsx === "react-jsx" || jsx === "react-jsxdev") && jsxImportSource) { - jsxConfig.jsx = "automatic"; - jsxConfig.jsxImportSource = jsxImportSource; - } else { - jsxConfig.jsx = "classic"; - jsxConfig.jsxPragma = jsxFactory; - jsxConfig.jsxPragmaFrag = jsxFragmentFactory; - } - log.debug(`jsx config from ${basename(denoConfigFile)} loaded`); - } catch (error) { - log.error(`Failed to parse ${basename(denoConfigFile)}: ${error.message}`); - } - } - return jsxConfig; + const jsxConfig: JSXConfig = {}; + const denoConfigFile = await findConfigFile( + ["deno.jsonc", "deno.json", "tsconfig.json"], + appDir + ); + if (denoConfigFile) { + try { + const { compilerOptions } = await parseJSONFile(denoConfigFile); + const { + jsx = "react", + jsxFactory = "React.createElement", + jsxFragmentFactory = "React.createElement", + jsxImportSource, + } = (compilerOptions ?? {}) as Record; + if (jsx === "preserve") { + jsxConfig.jsx = "preserve"; + } else if ( + (jsx === "react-jsx" || jsx === "react-jsxdev") && + jsxImportSource + ) { + jsxConfig.jsx = "automatic"; + jsxConfig.jsxImportSource = jsxImportSource; + } else { + jsxConfig.jsx = "classic"; + jsxConfig.jsxPragma = jsxFactory; + jsxConfig.jsxPragmaFrag = jsxFragmentFactory; + } + log.debug(`jsx config from ${basename(denoConfigFile)} loaded`); + } catch (error) { + log.error( + `Failed to parse ${basename(denoConfigFile)}: ${error.message}` + ); + } + } + return jsxConfig; } /** Load the import maps. */ export async function loadImportMap(appDir?: string): Promise { - const importMap: ImportMap = { __filename: "", imports: {}, scopes: {} }; - const denoConfigFile = await findConfigFile(["deno.jsonc", "deno.json"], appDir); - let importMapFilename: string | undefined; - if (denoConfigFile) { - const confg = await parseJSONFile & { importMap?: string }>(denoConfigFile); - if (!confg.importMap) { - if (isPlainObject(confg.imports)) { - Object.assign(importMap.imports, confg.imports); - } - if (isPlainObject(confg.scopes)) { - Object.assign(importMap.scopes, confg.scopes); - } - return importMap; - } - importMapFilename = join(dirname(denoConfigFile), confg.importMap); - } - if (!importMapFilename) { - importMapFilename = await findConfigFile( - ["import_map", "import-map", "importmap", "importMap"].map((v) => `${v}.json`), - appDir, - ); - } - if (importMapFilename) { - try { - const { __filename, imports, scopes } = await parseImportMap(importMapFilename); - if (import.meta.url.startsWith("file://") && appDir) { - const alephPkgUri = getAlephPkgUri(); - if (alephPkgUri === "https://aleph") { - Object.assign(imports, { - "aleph/": "https://aleph/", - "aleph/react": "https://aleph/framework/react/mod.ts", - }); - } - } - Object.assign(importMap, { __filename }); - Object.assign(importMap.imports, imports); - Object.assign(importMap.scopes, scopes); - log.debug(`import maps from ${basename(importMapFilename)} loaded`); - } catch (e) { - log.error("loadImportMap:", e.message); - } - } - return importMap; + const importMap: ImportMap = { __filename: "", imports: {}, scopes: {} }; + const denoConfigFile = await findConfigFile( + ["deno.jsonc", "deno.json"], + appDir + ); + let importMapFilename: string | undefined; + if (denoConfigFile) { + const confg = await parseJSONFile< + Partial & { importMap?: string } + >(denoConfigFile); + if (!confg.importMap) { + if (isPlainObject(confg.imports)) { + Object.assign(importMap.imports, confg.imports); + } + if (isPlainObject(confg.scopes)) { + Object.assign(importMap.scopes, confg.scopes); + } + return importMap; + } + importMapFilename = join(dirname(denoConfigFile), confg.importMap); + } + if (!importMapFilename) { + importMapFilename = await findConfigFile( + ["import_map", "import-map", "importmap", "importMap"].map( + v => `${v}.json` + ), + appDir + ); + } + if (importMapFilename) { + try { + const { __filename, imports, scopes } = await parseImportMap( + importMapFilename + ); + if (import.meta.url.startsWith("file://") && appDir) { + const alephPkgUri = getAlephPkgUri(); + if (alephPkgUri === "https://aleph") { + Object.assign(imports, { + "aleph/": "https://aleph/", + "aleph/react": "https://aleph/framework/react/mod.ts", + }); + } + } + Object.assign(importMap, { __filename }); + Object.assign(importMap.imports, imports); + Object.assign(importMap.scopes, scopes); + log.debug(`import maps from ${basename(importMapFilename)} loaded`); + } catch (e) { + log.error("loadImportMap:", e.message); + } + } + return importMap; } -export async function parseJSONFile>(jsonFile: string): Promise { - const raw = await Deno.readTextFile(jsonFile); - if (jsonFile.endsWith(".jsonc")) { - return jsonc.parse(raw) as T; - } - return JSON.parse(raw); +export async function parseJSONFile>( + jsonFile: string +): Promise { + const raw = await Deno.readTextFile(jsonFile); + if (jsonFile.endsWith(".jsonc")) { + return jsonc.parse(raw) as T; + } + return JSON.parse(raw); } -export async function parseImportMap(importMapFilename: string): Promise { - const importMap: ImportMap = { __filename: importMapFilename, imports: {}, scopes: {} }; - const data = await parseJSONFile(importMapFilename); - const imports: Record = toStringMap(data.imports); - const scopes: Record> = {}; - if (isPlainObject(data.scopes)) { - Object.entries(data.scopes).forEach(([scope, imports]) => { - scopes[scope] = toStringMap(imports); - }); - } - Object.assign(importMap, { imports, scopes }); - return importMap; +export async function parseImportMap( + importMapFilename: string +): Promise { + const importMap: ImportMap = { + __filename: importMapFilename, + imports: {}, + scopes: {}, + }; + const data = await parseJSONFile(importMapFilename); + const imports: Record = toStringMap(data.imports); + const scopes: Record> = {}; + if (isPlainObject(data.scopes)) { + Object.entries(data.scopes).forEach(([scope, imports]) => { + scopes[scope] = toStringMap(imports); + }); + } + Object.assign(importMap, { imports, scopes }); + return importMap; } function toStringMap(v: unknown): Record { - const m: Record = {}; - if (isPlainObject(v)) { - Object.entries(v).forEach(([key, value]) => { - if (key === "") { - return; - } - if (isFilledString(value)) { - m[key] = value; - return; - } - if (isFilledArray(value)) { - for (const v of value) { - if (isFilledString(v)) { - m[key] = v; - return; - } - } - } - }); - } - return m; + const m: Record = {}; + if (isPlainObject(v)) { + Object.entries(v).forEach(([key, value]) => { + if (key === "") { + return; + } + if (isFilledString(value)) { + m[key] = value; + return; + } + if (isFilledArray(value)) { + for (const v of value) { + if (isFilledString(v)) { + m[key] = v; + return; + } + } + } + }); + } + return m; } /** A `MagicString` alternative using byte offsets */ export class MagicString { - enc: TextEncoder; - dec: TextDecoder; - chunks: [number, Uint8Array][]; - - constructor(source: string) { - this.enc = new TextEncoder(); - this.dec = new TextDecoder(); - this.chunks = [[0, this.enc.encode(source)]]; - } - - overwrite(start: number, end: number, content: string) { - for (let i = 0; i < this.chunks.length; i++) { - const [offset, bytes] = this.chunks[i]; - if (offset !== -1 && start >= offset && end <= offset + bytes.length) { - const left = bytes.subarray(0, start - offset); - const right = bytes.subarray(end - offset); - const insert = this.enc.encode(content); - this.chunks.splice(i, 1, [offset, left], [-1, insert], [end, right]); - return; - } - } - throw new Error(`overwrite: invalid range: ${start}-${end}`); - } - - toBytes(): Uint8Array { - const length = this.chunks.reduce((sum, [, chunk]) => sum + chunk.length, 0); - const bytes = new Uint8Array(length); - let offset = 0; - for (const [, chunk] of this.chunks) { - bytes.set(chunk, offset); - offset += chunk.length; - } - return bytes; - } - - toString() { - return this.dec.decode(this.toBytes()); - } + enc: TextEncoder; + dec: TextDecoder; + chunks: [number, Uint8Array][]; + + constructor(source: string) { + this.enc = new TextEncoder(); + this.dec = new TextDecoder(); + this.chunks = [[0, this.enc.encode(source)]]; + } + + overwrite(start: number, end: number, content: string) { + for (let i = 0; i < this.chunks.length; i++) { + const [offset, bytes] = this.chunks[i]; + if ( + offset !== -1 && + start >= offset && + end <= offset + bytes.length + ) { + const left = bytes.subarray(0, start - offset); + const right = bytes.subarray(end - offset); + const insert = this.enc.encode(content); + this.chunks.splice( + i, + 1, + [offset, left], + [-1, insert], + [end, right] + ); + return; + } + } + throw new Error(`overwrite: invalid range: ${start}-${end}`); + } + + toBytes(): Uint8Array { + const length = this.chunks.reduce( + (sum, [, chunk]) => sum + chunk.length, + 0 + ); + const bytes = new Uint8Array(length); + let offset = 0; + for (const [, chunk] of this.chunks) { + bytes.set(chunk, offset); + offset += chunk.length; + } + return bytes; + } + + toString() { + return this.dec.decode(this.toBytes()); + } } From 46bd90de6634387519e7be872b954ce764642177 Mon Sep 17 00:00:00 2001 From: Brock Donahue Date: Tue, 20 Jun 2023 12:43:38 -0400 Subject: [PATCH 5/6] ahhh lint --- examples/react-app/routes/todos.tsx | 2 +- examples/react-mdx-app/server.ts | 18 +- framework/react/refresh.ts | 18 +- plugins/mdx.ts | 99 ++- server/helpers.ts | 1002 +++++++++++++-------------- 5 files changed, 558 insertions(+), 581 deletions(-) diff --git a/examples/react-app/routes/todos.tsx b/examples/react-app/routes/todos.tsx index 0788008e2..dcdd2eac0 100644 --- a/examples/react-app/routes/todos.tsx +++ b/examples/react-app/routes/todos.tsx @@ -17,7 +17,7 @@ const store = { }; export const data = () => { - return Response.json(store); + return Response.json(store); }; export async function mutation(req: Request): Promise { diff --git a/examples/react-mdx-app/server.ts b/examples/react-mdx-app/server.ts index e0cc04c63..416c06651 100644 --- a/examples/react-mdx-app/server.ts +++ b/examples/react-mdx-app/server.ts @@ -13,13 +13,13 @@ import rehypeHighlight from "https://esm.sh/v126/rehype-highlight@5.0.2"; import rehypeSlug from "https://esm.sh/v126/rehype-slug@5.0.1"; serve({ - plugins: [ - denoDeploy({ modules }), - mdx({ - remarkPlugins: [remarkFrontmatter, remarkGFM], - rehypePlugins: [rehypeHighlight, rehypeSlug], - providerImportSource: "@mdx-js/react", - }), - react({ ssr: true }), - ], + plugins: [ + denoDeploy({ modules }), + mdx({ + remarkPlugins: [remarkFrontmatter, remarkGFM], + rehypePlugins: [rehypeHighlight, rehypeSlug], + providerImportSource: "@mdx-js/react", + }), + react({ ssr: true }), + ], }); diff --git a/framework/react/refresh.ts b/framework/react/refresh.ts index 1c10fab7c..02b49c9da 100644 --- a/framework/react/refresh.ts +++ b/framework/react/refresh.ts @@ -7,20 +7,20 @@ import runtime from "https://esm.sh/v126/react-refresh@0.14.0/runtime"; let timer: number | null; const refresh = () => { - if (timer !== null) { - clearTimeout(timer); - } - timer = setTimeout(() => { - runtime.performReactRefresh(); - timer = null; - }, 50); + if (timer !== null) { + clearTimeout(timer); + } + timer = setTimeout(() => { + runtime.performReactRefresh(); + timer = null; + }, 50); }; runtime.injectIntoGlobalHook(window); Object.assign(window, { - $RefreshReg$: () => {}, - $RefreshSig$: () => (type: unknown) => type, + $RefreshReg$: () => {}, + $RefreshSig$: () => (type: unknown) => type, }); export { refresh as __REACT_REFRESH__, runtime as __REACT_REFRESH_RUNTIME__ }; diff --git a/plugins/mdx.ts b/plugins/mdx.ts index b16f85ab7..ebc9af327 100644 --- a/plugins/mdx.ts +++ b/plugins/mdx.ts @@ -1,65 +1,56 @@ /** @format */ -import { - compile, - type CompileOptions, -} from "https://esm.sh/v126/@mdx-js/mdx@2.3.0"; -import type { - ModuleLoader, - ModuleLoaderEnv, - ModuleLoaderOutput, - Plugin, -} from "../server/types.ts"; +import { compile, type CompileOptions } from "https://esm.sh/v126/@mdx-js/mdx@2.3.0"; +import type { ModuleLoader, ModuleLoaderEnv, ModuleLoaderOutput, Plugin } from "../server/types.ts"; export class MDXLoader implements ModuleLoader { - #options: CompileOptions; + #options: CompileOptions; - constructor(options?: CompileOptions) { - this.#options = options ?? {}; - } + constructor(options?: CompileOptions) { + this.#options = options ?? {}; + } - test(path: string): boolean { - const exts = this.#options?.mdxExtensions ?? ["mdx"]; - return exts.some(ext => path.endsWith(`.${ext}`)); - } + test(path: string): boolean { + const exts = this.#options?.mdxExtensions ?? ["mdx"]; + return exts.some((ext) => path.endsWith(`.${ext}`)); + } - async load( - specifier: string, - content: string, - env: ModuleLoaderEnv - ): Promise { - const ret = await compile( - { path: specifier, value: content }, - { - jsxImportSource: - this.#options.jsxImportSource ?? - env.jsxConfig?.jsxImportSource, - ...this.#options, - providerImportSource: this.#options.providerImportSource - ? env.importMap?.imports[ - this.#options.providerImportSource - ] ?? this.#options.providerImportSource - : undefined, - development: env.isDev, - } - ); - return { - code: ret.toString(), - lang: "js", - }; - } + async load( + specifier: string, + content: string, + env: ModuleLoaderEnv, + ): Promise { + const ret = await compile( + { path: specifier, value: content }, + { + jsxImportSource: this.#options.jsxImportSource ?? + env.jsxConfig?.jsxImportSource, + ...this.#options, + providerImportSource: this.#options.providerImportSource + ? env.importMap?.imports[ + this.#options.providerImportSource + ] ?? this.#options.providerImportSource + : undefined, + development: env.isDev, + }, + ); + return { + code: ret.toString(), + lang: "js", + }; + } } export default function MdxPlugin(options?: CompileOptions): Plugin { - return { - name: "mdx", - setup(aleph) { - const exts = options?.mdxExtensions ?? ["mdx"]; - aleph.loaders = [new MDXLoader(options), ...(aleph.loaders ?? [])]; - aleph.router = { - ...aleph.router, - exts: [...exts, ...(aleph.router?.exts ?? [])], - }; - }, - }; + return { + name: "mdx", + setup(aleph) { + const exts = options?.mdxExtensions ?? ["mdx"]; + aleph.loaders = [new MDXLoader(options), ...(aleph.loaders ?? [])]; + aleph.router = { + ...aleph.router, + exts: [...exts, ...(aleph.router?.exts ?? [])], + }; + }, + }; } diff --git a/server/helpers.ts b/server/helpers.ts index d406e38f5..ca66ca99a 100644 --- a/server/helpers.ts +++ b/server/helpers.ts @@ -1,23 +1,12 @@ /** @format */ -import { - isFilledArray, - isFilledString, - isLikelyHttpURL, - isPlainObject, - trimSuffix, -} from "../shared/util.ts"; +import { isFilledArray, isFilledString, isLikelyHttpURL, isPlainObject, trimSuffix } from "../shared/util.ts"; import { isCanary, VERSION } from "../version.ts"; import { cacheFetch } from "./cache.ts"; import { jsonc, path, type TransformOptions } from "./deps.ts"; import log from "./log.ts"; import { getContentType } from "./media_type.ts"; -import type { - AlephConfig, - CookieOptions, - ImportMap, - JSXConfig, -} from "./types.ts"; +import type { AlephConfig, CookieOptions, ImportMap, JSXConfig } from "./types.ts"; export const regJsxFile = /\.(jsx|tsx|mdx)$/; export const regFullVersion = /@\d+\.\d+\.\d+/; @@ -25,167 +14,164 @@ export const builtinModuleExts = ["tsx", "ts", "mts", "jsx", "js", "mjs"]; /** Stores and returns the `fn` output in the `globalThis` object. */ export async function globalIt( - name: string, - fn: () => Promise + name: string, + fn: () => Promise, ): Promise { - const v: T | undefined = Reflect.get(globalThis, name); - if (v !== undefined) { - if (v instanceof Promise) { - const ret = await v; - Reflect.set(globalThis, name, ret); - return ret; - } - return v; - } - const ret = fn(); - if (ret !== undefined) { - Reflect.set(globalThis, name, ret); - } - return await ret.then(v => { - Reflect.set(globalThis, name, v); - return v; - }); + const v: T | undefined = Reflect.get(globalThis, name); + if (v !== undefined) { + if (v instanceof Promise) { + const ret = await v; + Reflect.set(globalThis, name, ret); + return ret; + } + return v; + } + const ret = fn(); + if (ret !== undefined) { + Reflect.set(globalThis, name, ret); + } + return await ret.then((v) => { + Reflect.set(globalThis, name, v); + return v; + }); } /** Stores and returns the `fn` output in the `globalThis` object synchronously. */ export function globalItSync(name: string, fn: () => T): T { - const v: T | undefined = Reflect.get(globalThis, name); - if (v !== undefined) { - return v; - } - const ret = fn(); - if (ret !== undefined) { - Reflect.set(globalThis, name, ret); - } - return ret; + const v: T | undefined = Reflect.get(globalThis, name); + if (v !== undefined) { + return v; + } + const ret = fn(); + if (ret !== undefined) { + Reflect.set(globalThis, name, ret); + } + return ret; } export function getAppDir() { - return globalItSync("__ALEPH_APP_DIR", () => - Deno.mainModule - ? path.dirname(path.fromFileUrl(Deno.mainModule)) - : Deno.cwd() - ); + return globalItSync( + "__ALEPH_APP_DIR", + () => Deno.mainModule ? path.dirname(path.fromFileUrl(Deno.mainModule)) : Deno.cwd(), + ); } /** Get the module URI of Aleph.js */ export function getAlephPkgUri(): string { - return globalItSync("__ALEPH_PKG_URI", () => { - const uriEnv = Deno.env.get("ALEPH_PKG_URI"); - if (uriEnv) { - return uriEnv; - } - if (import.meta.url.startsWith("file://")) { - return "https://aleph"; - } - return `https://deno.land/x/${ - isCanary ? "aleph_canary" : "aleph" - }@${VERSION}`; - }); + return globalItSync("__ALEPH_PKG_URI", () => { + const uriEnv = Deno.env.get("ALEPH_PKG_URI"); + if (uriEnv) { + return uriEnv; + } + if (import.meta.url.startsWith("file://")) { + return "https://aleph"; + } + return `https://deno.land/x/${isCanary ? "aleph_canary" : "aleph"}@${VERSION}`; + }); } /** Get Aleph.js package URI. */ export function getAlephConfig(): AlephConfig | undefined { - return Reflect.get(globalThis, "__ALEPH_CONFIG"); + return Reflect.get(globalThis, "__ALEPH_CONFIG"); } /** Get the import maps. */ export async function getImportMap(appDir?: string): Promise { - return await globalIt("__ALEPH_IMPORT_MAP", () => loadImportMap(appDir)); + return await globalIt("__ALEPH_IMPORT_MAP", () => loadImportMap(appDir)); } /** Get the jsx config. */ export async function getJSXConfig(appDir?: string): Promise { - return await globalIt("__ALEPH_JSX_CONFIG", () => loadJSXConfig(appDir)); + return await globalIt("__ALEPH_JSX_CONFIG", () => loadJSXConfig(appDir)); } /** Get the deployment ID. */ export function getDeploymentId(): string | undefined { - const id = Deno.env.get("DENO_DEPLOYMENT_ID"); - if (id) { - return id; - } - - // or use git latest commit hash - return ( - globalItSync("__ALEPH_DEPLOYMENT_ID", () => { - try { - if (!Deno.args.includes("--dev")) { - const gitDir = path.join(Deno.cwd(), ".git"); - if (Deno.statSync(gitDir).isDirectory) { - const head = Deno.readTextFileSync( - path.join(gitDir, "HEAD") - ); - if (head.startsWith("ref: ")) { - const ref = head.slice(5).trim(); - const refFile = path.join(gitDir, ref); - return Deno.readTextFileSync(refFile) - .trim() - .slice(0, 8); - } - } - } - } catch { - // ignore - } - return null; - }) ?? undefined - ); + const id = Deno.env.get("DENO_DEPLOYMENT_ID"); + if (id) { + return id; + } + + // or use git latest commit hash + return ( + globalItSync("__ALEPH_DEPLOYMENT_ID", () => { + try { + if (!Deno.args.includes("--dev")) { + const gitDir = path.join(Deno.cwd(), ".git"); + if (Deno.statSync(gitDir).isDirectory) { + const head = Deno.readTextFileSync( + path.join(gitDir, "HEAD"), + ); + if (head.startsWith("ref: ")) { + const ref = head.slice(5).trim(); + const refFile = path.join(gitDir, ref); + return Deno.readTextFileSync(refFile) + .trim() + .slice(0, 8); + } + } + } + } catch { + // ignore + } + return null; + }) ?? undefined + ); } export function cookieHeader( - name: string, - value: string, - options?: CookieOptions + name: string, + value: string, + options?: CookieOptions, ): string { - const cookie = [`${name}=${value}`]; - if (options) { - if (options.expires) { - cookie.push(`Expires=${new Date(options.expires).toUTCString()}`); - } - if (options.maxAge) { - cookie.push(`Max-Age=${options.maxAge}`); - } - if (options.domain) { - cookie.push(`Domain=${options.domain}`); - } - if (options.path) { - cookie.push(`Path=${options.path}`); - } - if (options.httpOnly) { - cookie.push("HttpOnly"); - } - if (options.secure) { - cookie.push("Secure"); - } - if (options.sameSite) { - cookie.push(`SameSite=${options.sameSite}`); - } - } - return cookie.join("; "); + const cookie = [`${name}=${value}`]; + if (options) { + if (options.expires) { + cookie.push(`Expires=${new Date(options.expires).toUTCString()}`); + } + if (options.maxAge) { + cookie.push(`Max-Age=${options.maxAge}`); + } + if (options.domain) { + cookie.push(`Domain=${options.domain}`); + } + if (options.path) { + cookie.push(`Path=${options.path}`); + } + if (options.httpOnly) { + cookie.push("HttpOnly"); + } + if (options.secure) { + cookie.push("Secure"); + } + if (options.sameSite) { + cookie.push(`SameSite=${options.sameSite}`); + } + } + return cookie.join("; "); } export function toResponse(v: unknown, init?: ResponseInit): Response { - if ( - v instanceof ArrayBuffer || - v instanceof Uint8Array || - v instanceof ReadableStream - ) { - return new Response(v, init); - } - if (v instanceof Blob || v instanceof File) { - const headers = new Headers(init?.headers); - headers.set("Content-Type", v.type); - headers.set("Content-Length", v.size.toString()); - return new Response(v, { ...init, headers }); - } - try { - return Response.json(v, init); - } catch (_) { - return new Response("Invalid response type: " + typeof v, { - status: 500, - }); - } + if ( + v instanceof ArrayBuffer || + v instanceof Uint8Array || + v instanceof ReadableStream + ) { + return new Response(v, init); + } + if (v instanceof Blob || v instanceof File) { + const headers = new Headers(init?.headers); + headers.set("Content-Type", v.type); + headers.set("Content-Length", v.size.toString()); + return new Response(v, { ...init, headers }); + } + try { + return Response.json(v, init); + } catch (_) { + return new Response("Invalid response type: " + typeof v, { + status: 500, + }); + } } /** @@ -193,27 +179,27 @@ export function toResponse(v: unknown, init?: ResponseInit): Response { * e.g. `https://esm.sh/react@18.2.0?dev` -> `/-/esm.sh/react@18.2.0?dev` */ export function toLocalPath(url: string): string { - if (isLikelyHttpURL(url)) { - let { hostname, pathname, port, protocol, search } = new URL(url); - const isHttp = protocol === "http:"; - if ( - (isHttp && port === "80") || - (protocol === "https:" && port === "443") - ) { - port = ""; - } - return [ - "/-/", - isHttp && "http_", - hostname, - port && "_" + port, - trimSuffix(pathname, "/"), - search, - ] - .filter(Boolean) - .join(""); - } - return url; + if (isLikelyHttpURL(url)) { + let { hostname, pathname, port, protocol, search } = new URL(url); + const isHttp = protocol === "http:"; + if ( + (isHttp && port === "80") || + (protocol === "https:" && port === "443") + ) { + port = ""; + } + return [ + "/-/", + isHttp && "http_", + hostname, + port && "_" + port, + trimSuffix(pathname, "/"), + search, + ] + .filter(Boolean) + .join(""); + } + return url; } /** @@ -221,414 +207,414 @@ export function toLocalPath(url: string): string { * e.g. `/-/esm.sh/react@18.2.0` -> `https://esm.sh/v126/react@18.2.0` */ export function restoreUrl(pathname: string): string { - let [h, ...rest] = pathname.substring(3).split("/"); - let protocol = "https"; - if (h.startsWith("http_")) { - h = h.substring(5); - protocol = "http"; - } - const [host, port] = h.split("_"); - return `${protocol}://${host}${port ? ":" + port : ""}/${rest.join("/")}`; + let [h, ...rest] = pathname.substring(3).split("/"); + let protocol = "https"; + if (h.startsWith("http_")) { + h = h.substring(5); + protocol = "http"; + } + const [host, port] = h.split("_"); + return `${protocol}://${host}${port ? ":" + port : ""}/${rest.join("/")}`; } /** Check if the url is a npm package from esm.sh */ export function isNpmPkg(url: string) { - return ( - url.startsWith("https://esm.sh/") && - !url.endsWith(".js") && - !url.endsWith(".css") - ); + return ( + url.startsWith("https://esm.sh/") && + !url.endsWith(".js") && + !url.endsWith(".css") + ); } /** Find config file in the `appDir` if exits, or find in current working directory. */ async function findConfigFile( - filenames: string[], - appDir?: string + filenames: string[], + appDir?: string, ): Promise { - let denoConfigFile: string | undefined; - if (appDir) { - denoConfigFile = await findFile(filenames, appDir); - } - // find config file in current working directory - if (!denoConfigFile) { - denoConfigFile = await findFile(filenames); - } - return denoConfigFile; + let denoConfigFile: string | undefined; + if (appDir) { + denoConfigFile = await findFile(filenames, appDir); + } + // find config file in current working directory + if (!denoConfigFile) { + denoConfigFile = await findFile(filenames); + } + return denoConfigFile; } /** Check whether or not the given path exists as a directory. */ export async function existsDir(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isDirectory; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isDirectory; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } /** Check whether or not the given path exists as regular file. */ export async function existsFile(path: string): Promise { - try { - const stat = await Deno.lstat(path); - return stat.isFile; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return false; - } - throw err; - } + try { + const stat = await Deno.lstat(path); + return stat.isFile; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return false; + } + throw err; + } } const { basename, dirname, fromFileUrl, join } = path; /** Find file in the `cwd` directory. */ export async function findFile( - filenames: string[], - cwd = Deno.cwd() + filenames: string[], + cwd = Deno.cwd(), ): Promise { - for (const filename of filenames) { - const fullPath = join(cwd, filename); - if (await existsFile(fullPath)) { - return fullPath; - } - } + for (const filename of filenames) { + const fullPath = join(cwd, filename); + if (await existsFile(fullPath)) { + return fullPath; + } + } } /** Get files in the directory. */ export async function getFiles( - dir: string, - filter?: (filename: string) => boolean, - __path: string[] = [] + dir: string, + filter?: (filename: string) => boolean, + __path: string[] = [], ): Promise { - const list: string[] = []; - if (await existsDir(dir)) { - for await (const dirEntry of Deno.readDir(dir)) { - if (dirEntry.isDirectory) { - list.push( - ...(await getFiles(join(dir, dirEntry.name), filter, [ - ...__path, - dirEntry.name, - ])) - ); - } else { - const filename = [".", ...__path, dirEntry.name].join("/"); - if (!filter || filter(filename)) { - list.push(filename); - } - } - } - } - return list; + const list: string[] = []; + if (await existsDir(dir)) { + for await (const dirEntry of Deno.readDir(dir)) { + if (dirEntry.isDirectory) { + list.push( + ...(await getFiles(join(dir, dirEntry.name), filter, [ + ...__path, + dirEntry.name, + ])), + ); + } else { + const filename = [".", ...__path, dirEntry.name].join("/"); + if (!filter || filter(filename)) { + list.push(filename); + } + } + } + } + return list; } /** Watch the directory and its subdirectories. */ export async function watchFs( - rootDir: string, - listener: (kind: "create" | "remove" | "modify", path: string) => void + rootDir: string, + listener: (kind: "create" | "remove" | "modify", path: string) => void, ) { - const timers = new Map(); - const debounce = (id: string, callback: () => void, delay: number) => { - if (timers.has(id)) { - clearTimeout(timers.get(id)!); - } - timers.set( - id, - setTimeout(() => { - timers.delete(id); - callback(); - }, delay) - ); - }; - const reIgnore = - /[\/\\](\.git(hub)?|\.vscode|vendor|node_modules|dist|out(put)?|target)[\/\\]/; - const ignore = (path: string) => - reIgnore.test(path) || path.endsWith(".DS_Store"); - const allFiles = new Set( - (await getFiles(rootDir)) - .map(name => join(rootDir, name)) - .filter(path => !ignore(path)) - ); - for await (const { kind, paths } of Deno.watchFs(rootDir, { - recursive: true, - })) { - if (kind !== "create" && kind !== "remove" && kind !== "modify") { - continue; - } - for (const path of paths) { - if (ignore(path)) { - continue; - } - debounce( - kind + path, - async () => { - try { - await Deno.lstat(path); - if (!allFiles.has(path)) { - allFiles.add(path); - listener("create", path); - } else { - listener("modify", path); - } - } catch (error) { - if (error instanceof Deno.errors.NotFound) { - allFiles.delete(path); - listener("remove", path); - } else { - console.warn("watchFs:", error); - } - } - }, - 100 - ); - } - } + const timers = new Map(); + const debounce = (id: string, callback: () => void, delay: number) => { + if (timers.has(id)) { + clearTimeout(timers.get(id)!); + } + timers.set( + id, + setTimeout(() => { + timers.delete(id); + callback(); + }, delay), + ); + }; + const reIgnore = /[\/\\](\.git(hub)?|\.vscode|vendor|node_modules|dist|out(put)?|target)[\/\\]/; + const ignore = (path: string) => reIgnore.test(path) || path.endsWith(".DS_Store"); + const allFiles = new Set( + (await getFiles(rootDir)) + .map((name) => join(rootDir, name)) + .filter((path) => !ignore(path)), + ); + for await ( + const { kind, paths } of Deno.watchFs(rootDir, { + recursive: true, + }) + ) { + if (kind !== "create" && kind !== "remove" && kind !== "modify") { + continue; + } + for (const path of paths) { + if (ignore(path)) { + continue; + } + debounce( + kind + path, + async () => { + try { + await Deno.lstat(path); + if (!allFiles.has(path)) { + allFiles.add(path); + listener("create", path); + } else { + listener("modify", path); + } + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + allFiles.delete(path); + listener("remove", path); + } else { + console.warn("watchFs:", error); + } + } + }, + 100, + ); + } + } } /** Fetch source code from fs/cdn/cache. */ export async function fetchCode( - specifier: string, - target?: TransformOptions["target"] + specifier: string, + target?: TransformOptions["target"], ): Promise<[code: string, contentType: string]> { - if (isLikelyHttpURL(specifier)) { - const url = new URL(specifier); - if (url.host === "aleph") { - return [ - await Deno.readTextFile( - fromFileUrl(new URL(".." + url.pathname, import.meta.url)) - ), - getContentType(url.pathname), - ]; - } - if (url.hostname === "esm.sh") { - if ( - target && - !url.pathname.includes(`/${target}/`) && - !url.searchParams.has("target") - ) { - url.searchParams.set("target", target); - } - } - const res = await cacheFetch(url.href); - if (res.status >= 400) { - throw new Error( - `fetch ${url.href}: ${res.status} - ${res.statusText}` - ); - } - return [ - await res.text(), - res.headers.get("Content-Type") || getContentType(url.pathname), - ]; - } - - return [ - await Deno.readTextFile(path.join(getAppDir(), specifier)), - getContentType(specifier), - ]; + if (isLikelyHttpURL(specifier)) { + const url = new URL(specifier); + if (url.host === "aleph") { + return [ + await Deno.readTextFile( + fromFileUrl(new URL(".." + url.pathname, import.meta.url)), + ), + getContentType(url.pathname), + ]; + } + if (url.hostname === "esm.sh") { + if ( + target && + !url.pathname.includes(`/${target}/`) && + !url.searchParams.has("target") + ) { + url.searchParams.set("target", target); + } + } + const res = await cacheFetch(url.href); + if (res.status >= 400) { + throw new Error( + `fetch ${url.href}: ${res.status} - ${res.statusText}`, + ); + } + return [ + await res.text(), + res.headers.get("Content-Type") || getContentType(url.pathname), + ]; + } + + return [ + await Deno.readTextFile(path.join(getAppDir(), specifier)), + getContentType(specifier), + ]; } /** Load the JSX config base the given import maps and the existing deno config. */ export async function loadJSXConfig(appDir?: string): Promise { - const jsxConfig: JSXConfig = {}; - const denoConfigFile = await findConfigFile( - ["deno.jsonc", "deno.json", "tsconfig.json"], - appDir - ); - if (denoConfigFile) { - try { - const { compilerOptions } = await parseJSONFile(denoConfigFile); - const { - jsx = "react", - jsxFactory = "React.createElement", - jsxFragmentFactory = "React.createElement", - jsxImportSource, - } = (compilerOptions ?? {}) as Record; - if (jsx === "preserve") { - jsxConfig.jsx = "preserve"; - } else if ( - (jsx === "react-jsx" || jsx === "react-jsxdev") && - jsxImportSource - ) { - jsxConfig.jsx = "automatic"; - jsxConfig.jsxImportSource = jsxImportSource; - } else { - jsxConfig.jsx = "classic"; - jsxConfig.jsxPragma = jsxFactory; - jsxConfig.jsxPragmaFrag = jsxFragmentFactory; - } - log.debug(`jsx config from ${basename(denoConfigFile)} loaded`); - } catch (error) { - log.error( - `Failed to parse ${basename(denoConfigFile)}: ${error.message}` - ); - } - } - return jsxConfig; + const jsxConfig: JSXConfig = {}; + const denoConfigFile = await findConfigFile( + ["deno.jsonc", "deno.json", "tsconfig.json"], + appDir, + ); + if (denoConfigFile) { + try { + const { compilerOptions } = await parseJSONFile(denoConfigFile); + const { + jsx = "react", + jsxFactory = "React.createElement", + jsxFragmentFactory = "React.createElement", + jsxImportSource, + } = (compilerOptions ?? {}) as Record; + if (jsx === "preserve") { + jsxConfig.jsx = "preserve"; + } else if ( + (jsx === "react-jsx" || jsx === "react-jsxdev") && + jsxImportSource + ) { + jsxConfig.jsx = "automatic"; + jsxConfig.jsxImportSource = jsxImportSource; + } else { + jsxConfig.jsx = "classic"; + jsxConfig.jsxPragma = jsxFactory; + jsxConfig.jsxPragmaFrag = jsxFragmentFactory; + } + log.debug(`jsx config from ${basename(denoConfigFile)} loaded`); + } catch (error) { + log.error( + `Failed to parse ${basename(denoConfigFile)}: ${error.message}`, + ); + } + } + return jsxConfig; } /** Load the import maps. */ export async function loadImportMap(appDir?: string): Promise { - const importMap: ImportMap = { __filename: "", imports: {}, scopes: {} }; - const denoConfigFile = await findConfigFile( - ["deno.jsonc", "deno.json"], - appDir - ); - let importMapFilename: string | undefined; - if (denoConfigFile) { - const confg = await parseJSONFile< - Partial & { importMap?: string } - >(denoConfigFile); - if (!confg.importMap) { - if (isPlainObject(confg.imports)) { - Object.assign(importMap.imports, confg.imports); - } - if (isPlainObject(confg.scopes)) { - Object.assign(importMap.scopes, confg.scopes); - } - return importMap; - } - importMapFilename = join(dirname(denoConfigFile), confg.importMap); - } - if (!importMapFilename) { - importMapFilename = await findConfigFile( - ["import_map", "import-map", "importmap", "importMap"].map( - v => `${v}.json` - ), - appDir - ); - } - if (importMapFilename) { - try { - const { __filename, imports, scopes } = await parseImportMap( - importMapFilename - ); - if (import.meta.url.startsWith("file://") && appDir) { - const alephPkgUri = getAlephPkgUri(); - if (alephPkgUri === "https://aleph") { - Object.assign(imports, { - "aleph/": "https://aleph/", - "aleph/react": "https://aleph/framework/react/mod.ts", - }); - } - } - Object.assign(importMap, { __filename }); - Object.assign(importMap.imports, imports); - Object.assign(importMap.scopes, scopes); - log.debug(`import maps from ${basename(importMapFilename)} loaded`); - } catch (e) { - log.error("loadImportMap:", e.message); - } - } - return importMap; + const importMap: ImportMap = { __filename: "", imports: {}, scopes: {} }; + const denoConfigFile = await findConfigFile( + ["deno.jsonc", "deno.json"], + appDir, + ); + let importMapFilename: string | undefined; + if (denoConfigFile) { + const confg = await parseJSONFile< + Partial & { importMap?: string } + >(denoConfigFile); + if (!confg.importMap) { + if (isPlainObject(confg.imports)) { + Object.assign(importMap.imports, confg.imports); + } + if (isPlainObject(confg.scopes)) { + Object.assign(importMap.scopes, confg.scopes); + } + return importMap; + } + importMapFilename = join(dirname(denoConfigFile), confg.importMap); + } + if (!importMapFilename) { + importMapFilename = await findConfigFile( + ["import_map", "import-map", "importmap", "importMap"].map( + (v) => `${v}.json`, + ), + appDir, + ); + } + if (importMapFilename) { + try { + const { __filename, imports, scopes } = await parseImportMap( + importMapFilename, + ); + if (import.meta.url.startsWith("file://") && appDir) { + const alephPkgUri = getAlephPkgUri(); + if (alephPkgUri === "https://aleph") { + Object.assign(imports, { + "aleph/": "https://aleph/", + "aleph/react": "https://aleph/framework/react/mod.ts", + }); + } + } + Object.assign(importMap, { __filename }); + Object.assign(importMap.imports, imports); + Object.assign(importMap.scopes, scopes); + log.debug(`import maps from ${basename(importMapFilename)} loaded`); + } catch (e) { + log.error("loadImportMap:", e.message); + } + } + return importMap; } export async function parseJSONFile>( - jsonFile: string + jsonFile: string, ): Promise { - const raw = await Deno.readTextFile(jsonFile); - if (jsonFile.endsWith(".jsonc")) { - return jsonc.parse(raw) as T; - } - return JSON.parse(raw); + const raw = await Deno.readTextFile(jsonFile); + if (jsonFile.endsWith(".jsonc")) { + return jsonc.parse(raw) as T; + } + return JSON.parse(raw); } export async function parseImportMap( - importMapFilename: string + importMapFilename: string, ): Promise { - const importMap: ImportMap = { - __filename: importMapFilename, - imports: {}, - scopes: {}, - }; - const data = await parseJSONFile(importMapFilename); - const imports: Record = toStringMap(data.imports); - const scopes: Record> = {}; - if (isPlainObject(data.scopes)) { - Object.entries(data.scopes).forEach(([scope, imports]) => { - scopes[scope] = toStringMap(imports); - }); - } - Object.assign(importMap, { imports, scopes }); - return importMap; + const importMap: ImportMap = { + __filename: importMapFilename, + imports: {}, + scopes: {}, + }; + const data = await parseJSONFile(importMapFilename); + const imports: Record = toStringMap(data.imports); + const scopes: Record> = {}; + if (isPlainObject(data.scopes)) { + Object.entries(data.scopes).forEach(([scope, imports]) => { + scopes[scope] = toStringMap(imports); + }); + } + Object.assign(importMap, { imports, scopes }); + return importMap; } function toStringMap(v: unknown): Record { - const m: Record = {}; - if (isPlainObject(v)) { - Object.entries(v).forEach(([key, value]) => { - if (key === "") { - return; - } - if (isFilledString(value)) { - m[key] = value; - return; - } - if (isFilledArray(value)) { - for (const v of value) { - if (isFilledString(v)) { - m[key] = v; - return; - } - } - } - }); - } - return m; + const m: Record = {}; + if (isPlainObject(v)) { + Object.entries(v).forEach(([key, value]) => { + if (key === "") { + return; + } + if (isFilledString(value)) { + m[key] = value; + return; + } + if (isFilledArray(value)) { + for (const v of value) { + if (isFilledString(v)) { + m[key] = v; + return; + } + } + } + }); + } + return m; } /** A `MagicString` alternative using byte offsets */ export class MagicString { - enc: TextEncoder; - dec: TextDecoder; - chunks: [number, Uint8Array][]; - - constructor(source: string) { - this.enc = new TextEncoder(); - this.dec = new TextDecoder(); - this.chunks = [[0, this.enc.encode(source)]]; - } - - overwrite(start: number, end: number, content: string) { - for (let i = 0; i < this.chunks.length; i++) { - const [offset, bytes] = this.chunks[i]; - if ( - offset !== -1 && - start >= offset && - end <= offset + bytes.length - ) { - const left = bytes.subarray(0, start - offset); - const right = bytes.subarray(end - offset); - const insert = this.enc.encode(content); - this.chunks.splice( - i, - 1, - [offset, left], - [-1, insert], - [end, right] - ); - return; - } - } - throw new Error(`overwrite: invalid range: ${start}-${end}`); - } - - toBytes(): Uint8Array { - const length = this.chunks.reduce( - (sum, [, chunk]) => sum + chunk.length, - 0 - ); - const bytes = new Uint8Array(length); - let offset = 0; - for (const [, chunk] of this.chunks) { - bytes.set(chunk, offset); - offset += chunk.length; - } - return bytes; - } - - toString() { - return this.dec.decode(this.toBytes()); - } + enc: TextEncoder; + dec: TextDecoder; + chunks: [number, Uint8Array][]; + + constructor(source: string) { + this.enc = new TextEncoder(); + this.dec = new TextDecoder(); + this.chunks = [[0, this.enc.encode(source)]]; + } + + overwrite(start: number, end: number, content: string) { + for (let i = 0; i < this.chunks.length; i++) { + const [offset, bytes] = this.chunks[i]; + if ( + offset !== -1 && + start >= offset && + end <= offset + bytes.length + ) { + const left = bytes.subarray(0, start - offset); + const right = bytes.subarray(end - offset); + const insert = this.enc.encode(content); + this.chunks.splice( + i, + 1, + [offset, left], + [-1, insert], + [end, right], + ); + return; + } + } + throw new Error(`overwrite: invalid range: ${start}-${end}`); + } + + toBytes(): Uint8Array { + const length = this.chunks.reduce( + (sum, [, chunk]) => sum + chunk.length, + 0, + ); + const bytes = new Uint8Array(length); + let offset = 0; + for (const [, chunk] of this.chunks) { + bytes.set(chunk, offset); + offset += chunk.length; + } + return bytes; + } + + toString() { + return this.dec.decode(this.toBytes()); + } } From a96e99ddef6cb4136a225f23e222713e3699ac21 Mon Sep 17 00:00:00 2001 From: Brocktho Date: Fri, 30 Jun 2023 19:29:33 -0400 Subject: [PATCH 6/6] deleted markup.ts please pull :( I need unstable flag for my app --- framework/react/markup.ts | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 framework/react/markup.ts diff --git a/framework/react/markup.ts b/framework/react/markup.ts deleted file mode 100644 index ef23cc1f2..000000000 --- a/framework/react/markup.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** @format */ - -const ESCAPE_LOOKUP: { [match: string]: string } = { - "&": "\\u0026", - ">": "\\u003e", - "<": "\\u003c", - "\u2028": "\\u2028", - "\u2029": "\\u2029", -}; - -const ESCAPE_REGEX = /[&><\u2028\u2029]/g; - -export function escapeHtml(html: string) { - return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); -} - -export interface SafeHtml { - __html: string; -} - -export function createHtml(html: string): SafeHtml { - return { __html: html }; -}