tcmPlugin
uses node:path
pageFiles, for a browser build: the stylesheets its code only links to, by the name the page knows each by
(styles.css), filled as the build goes, for the page to name the file each is now (withStylesheetFiles), as the
import map does for chunks.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { codecSource } from "#codebase/tcmts/cli/codec"; import { flightAlias } from "#codebase/tcmts/cli/flight"; import type { AppPlan } from "#codebase/tcmts/cli/placement"; import { rolesIn } from "#codebase/tcmts/cli/roles"; import { reactRuntimeSource } from "#codebase/tcmts/cli/rsc"; import { cacheableComponents, CODEC, compileReact, HASH_FILE, linkedStylesheets, modulePathIn, pinnedReacts, REACT_RUNTIME, reactCompiler, RUNTIME, runtimeSource, storeLookup, STUB, stylesheetAsset, stylesheetRegistration, stylesheetStandIn, } from "#codebase/tcmts/cli/web"; import type { Lookup } from "#codebase/tcmts/cli/web"; import { exportId } from "#codebase/tcmts/core/hash"; import { npmAlias } from "#codebase/tcmts/loader/packages"; import path from "node:path"; import type { Plugin } from "vite"; export function tcmPlugin( cb: Codebase, current: () => AppPlan, look: Lookup = storeLookup(cb), pageFiles?: Map<string, string>, ): Plugin { const cacheableFor = new WeakMap<AppPlan, Set<string>>(); const linkedFor = new WeakMap<AppPlan, Set<string>>(); let serving = false; // the stylesheets the dev server has loaded, by the file each is served as const sheets = new Map<string, string>(); return { name: "tcm", // ahead of Vite's own resolver: server components must be swapped for stubs before they are resolved as files enforce: "pre", // before Vite starts, what compiling components changes: compiled modules import React itself (`compileReact`), // which Vite's scan of the source can't see (found as the page loads, it would be optimized again then, and the // modules loaded before would hold another React); and the compiler is installed now, not among the packages of // a server already running config() { const reacts = pinnedReacts(cb); if (!reacts.length) return null; reactCompiler(cb); return { optimizeDeps: { include: reacts } }; }, configResolved(config) { serving = config.command === "serve"; }, // a stylesheet of the codebase, served as a file of its own (`<Stylesheet>` links to it): by the dev server // from what it's loaded, and by a build as one of its assets configureServer(server) { server.middlewares.use((req, res, next) => { const m = /^\/_app\/assets\/([^/?#]+\.css)(?:[?#]|$)/.exec(req.url ?? ""); const css = m ? sheets.get(m[1]!) : undefined; if (css === undefined) return next(); res.setHeader("content-type", "text/css; charset=utf-8"); res.end(css); }); }, async resolveId(source, importer) { if (source === RUNTIME || source === CODEC) return "\0" + source; if (source.startsWith(REACT_RUNTIME)) return source; const m = HASH_FILE.exec(source); if (m && (/^\.\.?\//.test(source) || path.isAbsolute(source)) && current().stubbed.has(m[1]!)) return STUB + m[1]; // generated modules import pinned packages (npm__react__19.3.1), installed in the cache if ( source.startsWith("npm__") && importer && !importer.startsWith(cb.lay.cacheDir + path.sep) ) return this.resolve(source, path.join(cb.lay.cacheDir, "index.js"), { skipSelf: true }); return null; }, // React Compiler: each module of components written against React memoized (`compileReact`), in the browser's // bundle and the server's alike transform(code, id, options) { const m = HASH_FILE.exec(id); if (!m || !id.startsWith(cb.lay.cacheDir + path.sep)) return null; const members = look.members(m[1]!); // a stylesheet: said to `<Stylesheet>` as its module loads, by the file it's served as (code is only appended: // the module's source map stays true) const sheet = members.find((x) => x.kind === "file" && x.signature === "css"); if (sheet) { const asset = stylesheetAsset(sheet); const id = exportId(sheet.hash); if (serving) sheets.set(asset, sheet.sourceText); else if (!options?.ssr) this.emitFile({ type: "asset", fileName: `assets/${asset}`, source: sheet.sourceText }); // one the browser's code only links to is sent as the file alone: its module there holds a stand-in, not // the text (the server's has the text, and links to the same file). In a build, the page names the file by // the stylesheet's name, so a change to it changes neither its module nor the chunk holding it (unless two // the app links to share a name: those name their files themselves) const plan = current(); let linked = linkedFor.get(plan); if (!linked) linkedFor.set(plan, (linked = linkedStylesheets(plan, look, rolesIn(cb)))); if (!options?.ssr && linked.has(sheet.hash)) { const key = `${stylesheetAsset(sheet).replace(/-[a-z2-7]{8}\.css$/, "")}.css`; const shared = [...linked].filter((h) => look.get(h)?.nameHint === sheet.nameHint).length > 1; const fromPage = !!pageFiles && !shared; if (fromPage) pageFiles.set(key, asset); return { code: `export const ${id}: string = ${JSON.stringify(stylesheetStandIn(fromPage ? key : asset))};${stylesheetRegistration(id, fromPage ? key : asset, fromPage)}`, map: { mappings: "" }, }; } return { code: code + stylesheetRegistration(id, asset), map: null }; } const jsx = members.find((x) => x.jsx?.startsWith("npm:react@"))?.jsx; if (!jsx) return null; const out = compileReact(cb, id, code, jsx, serving); return out ? { code: out, map: null } : null; }, load(id) { if (id === "\0" + RUNTIME) return runtimeSource; if (id === "\0" + CODEC) return codecSource(); if (id.startsWith(REACT_RUNTIME)) // (`\0tcm-react:<react>+<flight>`) return reactRuntimeSource( ...(id.slice(REACT_RUNTIME.length).split("+") as [string, string]), RUNTIME, CODEC, ); if (!id.startsWith(STUB)) return null; const members = look.members(id.slice(STUB.length)); const plan = current(); const lines = [`import { remote, remoteStream } from ${JSON.stringify(RUNTIME)};`]; for (const m of members) { if (plan.components.has(m.hash) && m.jsx?.startsWith("npm:")) { // a server component: the browser components its tree can contain ship with it, referenced by hash const refs = [...(plan.refsOf.get(m.hash) ?? [])]; lines.unshift( `import { serverComponent as serverComponent_${m.hash} } from ${JSON.stringify(`${REACT_RUNTIME}${npmAlias(m.jsx)}+${flightAlias(m.jsx)}`)};`, ); for (const r of refs) lines.push( `import { ${exportId(r)} } from ${JSON.stringify(modulePathIn(cb, look, look.get(r)!.componentHash))};`, ); // and the packages' browser components it renders, by the key the server sends them by const packaged = [...(plan.packageRefsOf.get(m.hash) ?? [])].filter( ([, e]) => e.export !== "*", ); packaged.forEach(([, e], i) => { const spec = JSON.stringify(npmAlias(e.module) + (e.subpath ? `/${e.subpath}` : "")); lines.push( e.export === "default" ? `import package_${m.hash}_${i} from ${spec};` : `import { ${e.export} as package_${m.hash}_${i} } from ${spec};`, ); }); // (and whether it renders the same for the same props: its tree is then asked for so a browser keeps it) let pure = cacheableFor.get(plan); if (!pure) cacheableFor.set(plan, (pure = cacheableComponents(cb, plan.components))); lines.push( `export const ${exportId(m.hash)} = serverComponent_${m.hash}(${JSON.stringify(m.hash)}, ${JSON.stringify(m.nameHint)}, { ${[...refs.map((r) => `${JSON.stringify(r)}: ${exportId(r)}`), ...packaged.map(([key], i) => `${JSON.stringify(key)}: package_${m.hash}_${i}`)].join(", ")} }, ${pure.has(m.hash)});`, ); } else if (plan.streaming.has(m.hash)) lines.push( `export const ${exportId(m.hash)} = remoteStream(${JSON.stringify(m.hash)}, ${JSON.stringify(m.nameHint)});`, ); else lines.push( `export const ${exportId(m.hash)} = remote(${JSON.stringify(m.hash)}, ${JSON.stringify(m.nameHint)}, ${plan.endpoints.has(m.hash)});`, ); } return lines.join("\n"); }, }; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.codec.codecSource
- tcmts.cli.flight.flightAlias
- tcmts.cli.placement.AppPlan
- tcmts.cli.roles.rolesIn
- tcmts.cli.rsc.reactRuntimeSource
- tcmts.cli.web.CODEC
- tcmts.cli.web.HASH_FILE
- tcmts.cli.web.Lookup
- tcmts.cli.web.REACT_RUNTIME
- tcmts.cli.web.RUNTIME
- tcmts.cli.web.STUB
- tcmts.cli.web.cacheableComponents
- tcmts.cli.web.compileReact
- tcmts.cli.web.linkedStylesheets
- tcmts.cli.web.modulePathIn
- tcmts.cli.web.pinnedReacts
- tcmts.cli.web.reactCompiler
- tcmts.cli.web.runtimeSource
- tcmts.cli.web.storeLookup
- tcmts.cli.web.stylesheetAsset
- tcmts.cli.web.stylesheetRegistration
- tcmts.cli.web.stylesheetStandIn
- tcmts.core.hash.exportId
- tcmts.loader.packages.npmAlias