dev

tcmts.cli.web.dev · #fjyofhnp · term · runs on the server

uses node:path

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { sentByReference, useFlight } from "#codebase/tcmts/cli/flight";
import { bold, dim, yellow } from "#codebase/tcmts/cli/style";
import {
  cacheableComponents, clearLeftovers, httpHandler, isPageRequest, lateCookieHandler, ownId, planApp, printRoutes,
  renderPage, reportPlan, rpcHandler, sourceMapHandler, startBackground, tcmPlugin, watchCodebase, writeBuildRoot,
} from "#codebase/tcmts/cli/web";
import type { ComponentPlan, HttpRoutes, RenderApp } from "#codebase/tcmts/cli/web";
import { shortHash } from "#codebase/tcmts/core/hash";
import fs from "node:fs";
import path from "node:path";

export async function dev(
  cb: Codebase,
  appName: string,
  port: number,
  routes?: HttpRoutes,
): Promise<{ url: string; close: () => Promise<void> }> {
  const { createServer } = await import("vite");
  // its files are its own: another dev server (of this app or another) writes its own as it goes
  const web = path.join(cb.lay.tcmDir, "web");
  clearLeftovers(web);
  const root = path.join(web, ownId());
  let state = planApp(cb, appName);
  writeBuildRoot(cb, root, appName, state);

  let cacheable = cacheableComponents(cb, state.plan.components);
  let sent = sentByReference(state.plan);
  // server code loads as React's server environment has it, what server components send by reference as references
  useFlight(cb, {
    client: (h) => sent.has(h),
    server: (h) => state.plan.serverRefs.has(h),
    packageImports: (c) => [...(state.plan.packageImports.get(c) ?? [])],
  });
  const components: ComponentPlan = {
    isComponent: (h) => state.plan.components.has(h),
    isClientRef: (h) => sent.has(h),
    isCacheable: (h) => cacheable.has(h),
  };
  const rpc = rpcHandler(cb, (h) => state.plan.endpoints.has(h), undefined, undefined, components);
  const server = await createServer({
    configFile: false,
    root,
    logLevel: "warn",
    clearScreen: false,
    appType: "spa",
    plugins: [
      tcmPlugin(cb, () => state.plan),
      {
        name: "tcm-rpc",
        configureServer: (s) => {
          s.middlewares.use(rpc);
          // server components' code, for their stacks in the browser's tools
          s.middlewares.use(sourceMapHandler(cb));
          s.middlewares.use(lateCookieHandler);
          if (routes)
            s.middlewares.use(httpHandler(cb, routes, { production: false, secure: false }));
          // a component app's pages are rendered on the server first, then hydrated
          s.middlewares.use(async (req, res, next) => {
            if (!state.react || state.plan.errors.length || !isPageRequest(req)) return next();
            try {
              const template = await s.transformIndexHtml(
                req.url ?? "/",
                fs.readFileSync(path.join(root, "index.html"), "utf8"),
              );
              const { render } = (await s.ssrLoadModule(path.join(root, "ssr.ts"))) as {
                render: RenderApp;
              };
              if (
                await renderPage(cb, req, res, template, render, components, {
                  production: false,
                  secure: false,
                })
              )
                return;
              res.setHeader("content-type", "text/html");
              res.end(template);
            } catch (e) {
              next(e);
            }
          });
        },
      },
    ],
    server: {
      port,
      host: "127.0.0.1",
      // (and where it really is: a codebase reached by a symlink, as a worktree's is, is served from its real path)
      fs: { allow: [cb.lay.tcmDir, fs.realpathSync(cb.lay.tcmDir)] },
    },
  });
  await server.listen();
  const url = server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port}/`;
  console.log(`${bold("tcm dev")} ${appName} ${dim(shortHash(state.hash))}\n  url      ${url}`);
  reportPlan(cb, appName, state.plan);
  if (routes) printRoutes(cb, routes, url);
  const background = await startBackground(cb, appName);

  const stop = watchCodebase(cb, async () => {
    const before = state.hash;
    state = planApp(cb, appName);
    cacheable = cacheableComponents(cb, state.plan.components);
    sent = sentByReference(state.plan);
    writeBuildRoot(cb, root, appName, state);
    server.moduleGraph.invalidateAll();
    server.ws.send({ type: "full-reload" });
    console.log(
      `\n${yellow("reloaded")} ${appName} ${dim(before === state.hash ? "(names changed)" : `${shortHash(before)} → ${shortHash(state.hash)}`)}`,
    );
    reportPlan(cb, appName, state.plan);
    await background.refresh();
  });
  return {
    url,
    close: async () => {
      stop();
      background.stop();
      await server.close();
      fs.rmSync(root, { recursive: true, force: true });
    },
  };
}

uses

used by