Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions infra/scripts/release/oxfmt-inprocess/api-bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ApiExportMissing, WorkerImportUnrecognised } from '#oxfmt-inprocess/errors';
import { err, ok } from '#oxfmt-inprocess/result';
import type { Result } from '#oxfmt-inprocess/result';

/** How oxfmt's worker entry imports the functions the shim delegates to. */
const WORKER_IMPORT = /import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/;

/**
* One imported binding, in either form a bundler may emit: `x as localName`
* (rolldown's minified re-export) or a bare `localName`.
*/
const BINDING = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/;

/**
* Where oxfmt's embedded-formatting functions live, and what each is exported
* as.
*
* oxfmt's worker entry (`cli-worker.js`) is a two-line module that re-exports
* four functions from a content-hashed API module, e.g.
*
* ```js
* import { i as sortTailwindClasses, n as formatEmbeddedDoc, r as formatFile, t as formatEmbeddedCode } from './apis-CvFX8LhR.js';
* ```
*
* Both the hash and the single-letter aliases are bundler output and change
* between oxfmt releases, so they are read from the worker entry rather than
* hard-coded. Parsing succeeds only when all four functions are present, which
* is what lets the getters be total.
*/
export class ApiBindings {
readonly #moduleSpecifier: string;
readonly #formatFile: string;
readonly #formatEmbeddedCode: string;
readonly #formatEmbeddedDoc: string;
readonly #sortTailwindClasses: string;

private constructor(moduleSpecifier: string, formatFile: string, formatEmbeddedCode: string, formatEmbeddedDoc: string, sortTailwindClasses: string) {
this.#moduleSpecifier = moduleSpecifier;
this.#formatFile = formatFile;
this.#formatEmbeddedCode = formatEmbeddedCode;
this.#formatEmbeddedDoc = formatEmbeddedDoc;
this.#sortTailwindClasses = sortTailwindClasses;
}

/**
* Read the API module and its export aliases out of oxfmt's worker entry.
*
* @param workerSource - The contents of oxfmt's `cli-worker.js`.
* @param workerPath - Where that file came from, for error reporting.
* @returns The parsed bindings, or the reason the worker entry was not recognised.
*/
static parse(workerSource: string, workerPath: string): Result<ApiBindings, ApiExportMissing | WorkerImportUnrecognised> {
const workerImport = WORKER_IMPORT.exec(workerSource);

if (workerImport === null) {
return err(new WorkerImportUnrecognised(workerPath, 'cannot find the API import'));
}

const [, bindingList, moduleSpecifier] = workerImport;

if (bindingList === undefined || moduleSpecifier === undefined) {
return err(new WorkerImportUnrecognised(workerPath, 'the API import carries no bindings'));
}

const exportsByRole = new Map<string, string>();

for (const binding of bindingList.split(',')) {
const trimmed = binding.trim();
const parsed = BINDING.exec(trimmed);
const exported = parsed?.[1];

if (parsed === null || exported === undefined) {
return err(new WorkerImportUnrecognised(workerPath, `unexpected binding "${trimmed}"`));
}

// A bare binding is imported under its own name, so it is its own role.
exportsByRole.set(parsed[2] ?? exported, exported);
}

const formatFile = exportsByRole.get('formatFile');
const formatEmbeddedCode = exportsByRole.get('formatEmbeddedCode');
const formatEmbeddedDoc = exportsByRole.get('formatEmbeddedDoc');
const sortTailwindClasses = exportsByRole.get('sortTailwindClasses');

if (formatFile === undefined) {
return err(new ApiExportMissing('formatFile', workerPath));
}

if (formatEmbeddedCode === undefined) {
return err(new ApiExportMissing('formatEmbeddedCode', workerPath));
}

if (formatEmbeddedDoc === undefined) {
return err(new ApiExportMissing('formatEmbeddedDoc', workerPath));
}

if (sortTailwindClasses === undefined) {
return err(new ApiExportMissing('sortTailwindClasses', workerPath));
}

return ok(new ApiBindings(moduleSpecifier, formatFile, formatEmbeddedCode, formatEmbeddedDoc, sortTailwindClasses));
}

/** The module specifier the API functions are imported from. */
get moduleSpecifier(): string {
return this.#moduleSpecifier;
}

/** The name `formatFile` is exported as. */
get formatFile(): string {
return this.#formatFile;
}

/** The name `formatEmbeddedCode` is exported as. */
get formatEmbeddedCode(): string {
return this.#formatEmbeddedCode;
}

/** The name `formatEmbeddedDoc` is exported as. */
get formatEmbeddedDoc(): string {
return this.#formatEmbeddedDoc;
}

/** The name `sortTailwindClasses` is exported as. */
get sortTailwindClasses(): string {
return this.#sortTailwindClasses;
}
}
141 changes: 141 additions & 0 deletions infra/scripts/release/oxfmt-inprocess/cli-patcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { join } from 'node:path';
import { ApiBindings } from '#oxfmt-inprocess/api-bindings';
import { CliAlreadyPatched, CliAnchorMissing, CliPatchIncomplete } from '#oxfmt-inprocess/errors';
import type { OxfmtPatchError } from '#oxfmt-inprocess/errors';
import { err, isErr, ok } from '#oxfmt-inprocess/result';
import type { Result } from '#oxfmt-inprocess/result';
import { SHIM_MARKER, ShimSource } from '#oxfmt-inprocess/shim-source';
import type { TextFiles } from '#oxfmt-inprocess/text-files';

/** oxfmt's import of the worker-pool library, replaced by the shim's import. */
const TINYPOOL_IMPORT = 'import Tinypool from "tinypool";';

/**
* The structures the rewrite edits. Absent any one of them, oxfmt's CLI has
* changed shape and the rewrite must be re-derived rather than misapplied.
*/
const ANCHORS: ReadonlyArray<string> = [TINYPOOL_IMPORT, '//#region src-js/cli/worker-proxy.ts', 'runtime: "child_process"'];

/** oxfmt's `worker-proxy` region, from its marker to the first region end. */
const WORKER_PROXY_REGION = /\/\/#region src-js\/cli\/worker-proxy\.ts[\s\S]*?\/\/#endregion/;

/** Worker-pool code that must not survive the rewrite. */
const RESIDUE: ReadonlyArray<string> = ['Tinypool', 'pool.run', 'pool = '];

/** What a successful rewrite changed. */
export type PatchOutcome = {
/** The CLI file that was rewritten. */
readonly cliPath: string;

/** The API module the shim now calls directly. */
readonly apiModuleSpecifier: string;
};

/**
* Rewrites oxfmt's CLI to format embedded code in-process.
*
* oxfmt formats embedded code — Vue `<template>`/`<style>`, markdown fences,
* HTML — by handing it to a Tinypool pool running with
* `runtime: "child_process"`. That pool forks worker entry scripts which, in a
* `bun build --compile` binary, exist only as virtual `/$bunfs/root/…` paths and
* are never carved out as real files: every worker dies on startup and
* `Tinypool.destroy()` then awaits an exit event that never fires, wedging the
* process. Plain `.ts` never initialises the pool, which is why only files with
* embedded code hang.
*
* The four pooled functions are stateless prettier calls, so the pool only ever
* bought cross-file parallelism — not isolation. Calling them directly is
* therefore behaviourally identical, minus the worker layer that cannot work
* here. For the embedded-code fraction of a codebase the lost parallelism is
* negligible, and the per-worker prettier startup cost disappears with it.
*
* This runs at release-staging time against a freshly installed oxfmt in a
* throwaway workdir; the committed tree is never touched.
*/
export class OxfmtCliPatcher {
readonly #files: TextFiles;

/** @param files - Reads the CLI and worker entry, and writes the result back. */
constructor(files: TextFiles) {
this.#files = files;
}

/**
* Rewrite oxfmt's CLI in place.
*
* @param distDir - oxfmt's `dist` directory, holding `cli.js` and `cli-worker.js`.
* @returns What was rewritten, or the reason oxfmt no longer matches the rewrite.
*/
patch(distDir: string): Result<PatchOutcome, OxfmtPatchError> {
const cliPath = join(distDir, 'cli.js');
const workerPath = join(distDir, 'cli-worker.js');

const cliSource = this.#files.readText(cliPath);

if (isErr(cliSource)) {
return cliSource;
}

const workerSource = this.#files.readText(workerPath);

if (isErr(workerSource)) {
return workerSource;
}

const bindings = ApiBindings.parse(workerSource.value, workerPath);

if (isErr(bindings)) {
return bindings;
}

const rewritten = OxfmtCliPatcher.#rewrite(cliSource.value, bindings.value, cliPath);

if (isErr(rewritten)) {
return rewritten;
}

const written = this.#files.writeText(cliPath, rewritten.value);

if (isErr(written)) {
return written;
}

return ok({ cliPath, apiModuleSpecifier: bindings.value.moduleSpecifier });
}

/**
* Swap the worker-pool proxy for the in-process shim.
*
* @param cliSource - The contents of oxfmt's `cli.js`.
* @param bindings - Where the API functions live and what they are exported as.
* @param cliPath - Where the CLI came from, for error reporting.
* @returns The rewritten CLI, or the reason it could not be rewritten safely.
*/
static #rewrite(cliSource: string, bindings: ApiBindings, cliPath: string): Result<string, CliAlreadyPatched | CliAnchorMissing | CliPatchIncomplete> {
if (cliSource.includes(SHIM_MARKER)) {
return err(new CliAlreadyPatched(cliPath));
}

for (const anchor of ANCHORS) {
if (!cliSource.includes(anchor)) {
return err(new CliAnchorMissing(anchor, cliPath));
}
}

const imported = cliSource.replace(TINYPOOL_IMPORT, ShimSource.importStatement(bindings));

if (!WORKER_PROXY_REGION.test(imported)) {
return err(new CliAnchorMissing('the worker-proxy region', cliPath));
}

const patched = imported.replace(WORKER_PROXY_REGION, ShimSource.workerProxyRegion());

for (const residue of RESIDUE) {
if (patched.includes(residue)) {
return err(new CliPatchIncomplete(residue, cliPath));
}
}

return ok(patched);
}
}
Loading
Loading