Files
Reactive-Resume/packages/api/src/features/resume/stylesheet-preflight.ts
T

193 lines
6.1 KiB
TypeScript

import type { StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
import type { CompileStylesheetResult } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { StylesheetSnapshot } from "./stylesheet-service";
import { ORPCError } from "@orpc/client";
import { compileStylesheet } from "@reactive-resume/resume/stylesheet";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { recordSemanticCssEvent } from "./stylesheet-observability";
type PrepareImportedResumeDataInput = {
data: ResumeData;
resumeId: string;
revision: number;
runner?: StylesheetPreflightRunner | undefined;
};
type ValidateHistoricalStylesheetInput = {
data: ResumeData;
resumeId: string;
revision: number;
stylesheet: SemanticStylesheet;
runner?: StylesheetPreflightRunner | undefined;
};
const byteCount = (source: StylesheetSource): number => new TextEncoder().encode(source.text).byteLength;
const observeCompile = (
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision">,
source: StylesheetSource,
): CompileStylesheetResult => {
const startedAt = performance.now();
try {
const result = compileStylesheet(source);
recordSemanticCssEvent({
name: "semantic_css.compile",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: input.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: null,
revision: input.revision,
success: result.program !== null,
});
return result;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.compile",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: false,
});
throw error;
}
};
const unavailableError = () =>
new ORPCError("SEMANTIC_STYLESHEET_UNAVAILABLE", {
status: 503,
message: "Semantic stylesheet PDF preflight is unavailable.",
});
const validationError = (message: string) =>
new ORPCError("STYLESHEET_VALIDATION_FAILED", {
status: 400,
message,
});
const runPreflight = async (
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision" | "runner">,
stylesheet: StylesheetSource,
) => {
const startedAt = performance.now();
try {
if (!input.runner) throw unavailableError();
const result = await input.runner.run({
data: input.data,
template: input.data.metadata.template,
stylesheet,
});
recordSemanticCssEvent({
name: "semantic_css.preflight",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: byteCount(stylesheet),
template: input.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: result.ok ? result.pageCount : null,
revision: input.revision,
success: result.ok,
});
return result;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.preflight",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: byteCount(stylesheet),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: false,
});
throw error;
}
};
const validApplied = async (input: PrepareImportedResumeDataInput, stylesheet: StylesheetSource): Promise<boolean> => {
const compiled = observeCompile(input, stylesheet);
return compiled.program !== null && (await runPreflight(input, stylesheet)).ok;
};
export async function prepareImportedResumeData(input: PrepareImportedResumeDataInput): Promise<ResumeData> {
const stylesheet = input.data.metadata.stylesheet;
if (stylesheet?.mode !== "semantic") return input.data;
let applied = stylesheet.source;
if (!(await validApplied(input, stylesheet.source))) {
applied = stylesheet.applied;
if (!(await validApplied(input, stylesheet.applied))) {
applied = { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE };
if (!(await validApplied(input, applied))) {
throw validationError("The empty stylesheet failed PDF preflight.");
}
}
}
return {
...input.data,
metadata: {
...input.data.metadata,
stylesheet: { ...stylesheet, applied },
},
};
}
export async function validateHistoricalStylesheet(
input: ValidateHistoricalStylesheetInput,
): Promise<SemanticStylesheet> {
const compiled = observeCompile(input, input.stylesheet.applied);
if (!compiled.program) throw validationError("The historical applied stylesheet is invalid.");
if (!(await runPreflight(input, input.stylesheet.applied)).ok) {
throw validationError("The historical applied stylesheet failed PDF preflight.");
}
return input.stylesheet;
}
export async function convertLegacyStylesheet(snapshot: StylesheetSnapshot): Promise<StylesheetSource> {
const startedAt = performance.now();
try {
const { convertLegacyStyleRules } = await import("@reactive-resume/pdf/semantic");
const source = convertLegacyStyleRules(snapshot.data).source;
recordSemanticCssEvent({
name: "semantic_css.convert_legacy",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: true,
});
return source;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.convert_legacy",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: 1,
sourceBytes: 0,
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: false,
});
throw error;
}
}