import { colors, Command, CompletionsCommand, debounce, deferred, fs, path, prettyBytes, RollupCache, Table, } from "./deps/mod.ts"; import { bundle } from "./src/bundle.ts"; import { dependencyList } from "./src/dependency_graph.ts"; import { exportCommand } from "./src/export.ts"; import { serve } from "./src/serve.ts"; import { findPages, printError } from "./src/util.ts"; const VERSION = "0.10.5"; try { await new Command() .throwErrors() .name("dext") .version(VERSION) .description("The Preact Framework for Deno") .action(function () { console.log(this.getHelp()); }) .command("build [root]") .option( "--typecheck [enabled:boolean]", "If TypeScript code should be typechecked.", { default: true }, ) .option( "--prerender [enabled:boolean]", "If static pages should be server side prerendered.", { default: true }, ) .option( "--debug [include:boolean]", "If preact/debug should be included in the bundle.", { default: false }, ) .description("Build your application.") .action(build) .command("start [root]") .option("-a --address
", "The address to listen on.", { default: ":3000", }) .option("--quiet", "If access logs should be printed.") .description("Start a built application.") .action(start) .command("dev [root]") .option("-a --address
", "The address to listen on.", { default: ":3000", }) .option( "--hot-refresh [enabled:boolean]", "If hot refresh should be disabled.", { default: true }, ) .option( "--hot-refresh-host ", "The hostname to use for the hot refresh websocket endpoint. Useful for proxies.", { depends: ["hot-refresh"] }, ) .option( "--typecheck [enabled:boolean]", "If TypeScript code should be typechecked.", { default: false }, ) .option( "--prerender [enabled:boolean]", "If static pages should be server side prerendered.", { default: false }, ) .option( "--debug [include:boolean]", "If preact/debug should be included in the bundle.", { default: true }, ) .description("Start your application in development mode.") .action(dev) .command("create [root]") .description("Scaffold new application.") .action(create) .command("export", exportCommand()) .description("Export a project for Netlify or other providers.") .command("completions", new CompletionsCommand()) .parse(Deno.args); } catch (err) { printError(err); Deno.exit(1); } async function build( options: { typecheck: boolean; prerender: boolean; debug: boolean }, root?: string, ) { root = path.resolve(Deno.cwd(), root ?? ""); const tsconfigPath = path.join(root, "tsconfig.json"); if (!(await fs.exists(tsconfigPath))) { console.log( colors.red(colors.bold("Error: ") + "Missing tsconfig.json file."), ); Deno.exit(1); } // Collect list of all pages const pagesDir = path.join(root, "pages"); const pages = await findPages(pagesDir); // Create .dext folder and emit page map const dextDir = path.join(root, ".dext"); await fs.ensureDir(dextDir); const pagemapPath = path.join(dextDir, "pagemap.json"); await Deno.writeTextFile( pagemapPath, JSON.stringify( pages.pages.map((page) => ({ name: page.name, route: page.route, hasGetStaticPaths: page.hasGetStaticPaths, })), ), ); // Do bundling const outDir = path.join(dextDir, "static"); const { stats } = await bundle(pages, { rootDir: root, outDir, tsconfigPath, minify: true, hotRefresh: false, typecheck: options.typecheck, prerender: options.prerender, debug: options.debug, }); console.log(colors.green(colors.bold("Build success.\n"))); if (stats) { const sharedKeys = Object.keys(stats.shared); new Table() .header([ colors.bold("Page"), colors.bold("Size"), colors.bold("First Load JS"), ]) .body([ ...stats.routes.map((route, i) => { const prefix = stats.routes.length === 1 ? "-" : i === 0 ? "┌" : i === stats.routes.length - 1 ? "└" : "├"; return [ `${prefix} ${route.hasGetStaticData ? "●" : "○"} ${route.route}`, prettyBytes(route.size.brotli), prettyBytes(route.firstLoad.brotli), ]; }), [], [ "+ First Load JS shared by all", prettyBytes(stats.framework.brotli), "", ], ...sharedKeys.map((name, i) => { const size = stats.shared[name]; const isLast = i === sharedKeys.length - 1; return [ ` ${isLast ? "└" : "├"} ${name}`, prettyBytes(size.brotli), "", ]; }), ]) .padding(2) .render(); console.log(); console.log("○ (Static) automatically rendered as static HTML"); console.log( "● (SSG) automatically generated as static HTML + JSON (uses getStaticData)", ); console.log(); console.log( colors.gray("File sizes are measured after brotli compression."), ); } } async function start( options: { address: string; quiet: boolean }, root?: string, ) { root = path.resolve(Deno.cwd(), root ?? ""); const dextDir = path.join(root, ".dext"); const pagemapPath = path.join(dextDir, "pagemap.json"); if (!(await fs.exists(pagemapPath))) { console.log( colors.red( colors.bold("Error: ") + "Page map does not exist. Did you build the project?", ), ); Deno.exit(1); } const pagemap = JSON.parse(await Deno.readTextFile(pagemapPath)); const staticDir = path.join(dextDir, "static"); await serve(pagemap, { staticDir, address: options.address, quiet: options.quiet, }); } async function dev( options: { address: string; hotRefresh: boolean; hotRefreshHost: string; typecheck: boolean; prerender: boolean; debug: boolean; }, maybeRoot?: string, ) { const root = path.resolve(Deno.cwd(), maybeRoot ?? ""); const tsconfigPath = path.join(root, "tsconfig.json"); if (!(await fs.exists(tsconfigPath))) { console.log( colors.red(colors.bold("Error: ") + "Missing tsconfig.json file."), ); Deno.exit(1); } let cache: RollupCache = { modules: [] }; // Collect list of all pages const pagesDir = path.join(root, "pages"); const pages = await findPages(pagesDir); const dextDir = path.join(root, ".dext"); await fs.ensureDir(dextDir); const outDir = path.join(dextDir, "static"); let doHotRefresh = deferred(); const hotRefresh = (async function* () { while (true) { await doHotRefresh; doHotRefresh = deferred(); yield; } })(); const run = debounce(async function () { const start = new Date(); console.log(colors.cyan(colors.bold("Started build..."))); try { const out = await bundle(pages, { rootDir: root, outDir, tsconfigPath, cache, minify: false, hotRefresh: options.hotRefresh, hotRefreshHost: options.hotRefreshHost, typecheck: options.typecheck, prerender: options.prerender, debug: options.debug, }); cache = out.cache!; doHotRefresh.resolve(); console.log( colors.green( colors.bold( `Build success done ${ ( new Date().getTime() - start.getTime() ).toFixed(0) }ms`, ), ), ); } catch (err) { printError(err); } }, 100); const pagesPaths = pages.pages.map((page) => page.path); if (pages.app) pagesPaths.push(pages.app.path); if (pages.document) pagesPaths.push(pages.document.path); const deps = await dependencyList(pagesPaths); const toWatch = deps .filter((dep) => dep.startsWith(`file://`)) .map(path.fromFileUrl) .filter((dep) => dep.startsWith(root)); const publicDir = path.join(root, "public"); if (await fs.exists(publicDir)) toWatch.push(publicDir); (async () => { for await (const { kind } of Deno.watchFs(toWatch, { recursive: true })) { if (kind === "any" || kind === "access") continue; await run(); } })(); const server = serve(pages.pages, { staticDir: outDir, address: options.address, quiet: true, hotRefresh, }); await run(); await server; } async function create(_options: unknown, maybeRoot?: string) { const root = path.resolve(Deno.cwd(), maybeRoot ?? ""); await fs.ensureDir(root); const gitIgnorePath = path.join(root, ".gitignore"); await Deno.writeTextFile(gitIgnorePath, "/.dext\n"); const tsconfigPath = path.join(root, "tsconfig.json"); await Deno.writeTextFile( tsconfigPath, JSON.stringify({ compilerOptions: { lib: ["esnext", "dom", "deno.ns"], jsx: "react", jsxFactory: "h", jsxFragmentFactory: "Fragment", }, }), ); const pagesDir = path.join(root, "pages"); await fs.ensureDir(pagesDir); const depsPath = path.join(root, "deps.ts"); const depsText = `export { Fragment, h } from "https://deno.land/x/dext@${VERSION}/deps/preact/mod.ts"; export type { AppProps, GetStaticData, GetStaticDataContext, GetStaticPaths, PageProps, } from "https://deno.land/x/dext@${VERSION}/mod.ts"; `; await Deno.writeTextFile(depsPath, depsText); const indexPath = path.join(pagesDir, "index.tsx"); const indexText = `import { h, Fragment } from "../deps.ts"; import type { PageProps, GetStaticData } from "../deps.ts"; interface Data { random: string; } function IndexPage(props: PageProps) { return ( <>

Hello World!!!

This is the index page.

The random is {props.data.random}.

Go to @lucacasonato

); } export const getStaticData = (): GetStaticData => { return { data: { random: Math.random().toString(), }, }; }; export default IndexPage; `; await Deno.writeTextFile(indexPath, indexText); const userDir = path.join(pagesDir, "user"); await fs.ensureDir(userDir); const userPath = path.join(userDir, "[name].tsx"); const userText = `import { h, Fragment } from "../../deps.ts"; import type { PageProps } from "../../deps.ts"; function UserPage(props: PageProps) { const name = props.route?.name ?? ""; return ( <>

This is the page for {name}

Go home

); } export default UserPage; `; await Deno.writeTextFile(userPath, userText); console.log(colors.green(colors.bold(`New project created in ${root}`))); }