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; }