chore(root): migrate build scripts and plugins to TypeScript (#1052)

This commit is contained in:
rahim
2026-03-19 22:58:38 -07:00
committed by GitHub
parent b7e6286233
commit 221bcc975a
13 changed files with 151 additions and 114 deletions
+10 -1
View File
@@ -119,6 +119,16 @@
}
}
}
},
{
"includes": ["build/plugins/tests/**"],
"linter": {
"rules": {
"suspicious": {
"noTemplateCurlyInString": "off"
}
}
}
}
],
"files": {
@@ -131,7 +141,6 @@
"!**/.next",
"!**/.opencode",
"!**/.github",
"!build",
"!**/dist",
"!**/examples",
"!**/styles/vjs.css",
@@ -1,11 +1,14 @@
import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { resolveImports } from './resolve-css-imports.mjs';
import { resolveImports } from './resolve-css-imports.ts';
import type { BuildPlugin } from './types.ts';
/**
* @param {{ skinsDir: string; outDir: string }} options
*/
export function copyCssPlugin(options) {
interface CopyCssPluginOptions {
skinsDir: string;
outDir: string;
}
export function copyCssPlugin(options: CopyCssPluginOptions): BuildPlugin {
const { skinsDir, outDir } = options;
return {
@@ -1,20 +1,25 @@
import { readFileSync } from 'node:fs';
import { dirname, relative, resolve } from 'node:path';
import { transform } from 'lightningcss';
import { resolveImports } from './resolve-css-imports.mjs';
import { resolveImports } from './resolve-css-imports.ts';
import type { BuildPlugin } from './types.ts';
const INLINE_PREFIX = 'inline-css:';
const INLINE_SUFFIX = '?inline';
interface InlineCssPluginOptions {
skinsDir: string;
rootDir?: string;
minify?: boolean;
}
/**
* Rolldown/tsdown plugin that inlines `.css?inline` imports as JavaScript
* modules exporting the resolved CSS string. Mirrors the Vite `?inline`
* convention so the same source works in both Vite dev and tsdown builds.
*
* @param {{ skinsDir: string; rootDir?: string; minify?: boolean }} options
*/
export function inlineCssPlugin(options) {
export function inlineCssPlugin(options: InlineCssPluginOptions): BuildPlugin {
const { skinsDir, rootDir = process.cwd(), minify = true } = options;
return {
@@ -24,7 +29,7 @@ export function inlineCssPlugin(options) {
if (!source.endsWith(INLINE_SUFFIX)) return null;
const cssPath = source.slice(0, -INLINE_SUFFIX.length);
const abs = resolve(dirname(importer), cssPath);
const abs = resolve(dirname(importer!), cssPath);
const rel = relative(rootDir, abs);
// Use .js extension so rolldown doesn't apply its CSS loader.
@@ -1,8 +1,13 @@
import { transform } from 'lightningcss';
import type { BuildPlugin } from './types.ts';
const HTML_MARKER = '/*html*/';
const CSS_MARKER = '/* css */';
interface TemplatePluginOptions {
minify?: boolean;
}
/**
* Rolldown/tsdown transform plugin that minifies tagged template literals
* marked with {@link HTML_MARKER} or {@link CSS_MARKER}.
@@ -12,10 +17,8 @@ const CSS_MARKER = '/* css */';
*
* Only the static parts (quasis) are processed `${...}` expression
* interpolations are preserved as-is.
*
* @param {{ minify?: boolean }} options
*/
export function inlineTemplatePlugin(options = {}) {
export function inlineTemplatePlugin(options: TemplatePluginOptions = {}): BuildPlugin {
const { minify = true } = options;
return {
@@ -40,7 +43,7 @@ export function inlineTemplatePlugin(options = {}) {
* Walk `code` looking for known markers followed by a backtick template
* literal. For each match, minify the static parts and reassemble.
*/
function processTemplates(code) {
function processTemplates(code: string): string {
let out = '';
let pos = 0;
@@ -49,8 +52,8 @@ function processTemplates(code) {
const htmlIdx = code.indexOf(HTML_MARKER, pos);
const cssIdx = code.indexOf(CSS_MARKER, pos);
let idx;
let marker;
let idx: number;
let marker: string;
if (htmlIdx === -1 && cssIdx === -1) {
out += code.slice(pos);
@@ -68,7 +71,7 @@ function processTemplates(code) {
// Skip marker + optional whitespace to find the opening backtick.
let i = idx + marker.length;
while (i < code.length && isWhitespace(code[i])) i++;
while (i < code.length && isWhitespace(code[i]!)) i++;
if (i >= code.length || code[i] !== '`') {
// Not a tagged template literal — copy the marker verbatim.
@@ -79,17 +82,14 @@ function processTemplates(code) {
const { quasis, expressions, end } = parseTemplateLiteral(code, i);
const minified =
marker === HTML_MARKER
? minifyHtmlQuasis(quasis)
: minifyCssQuasis(quasis);
const minified = marker === HTML_MARKER ? minifyHtmlQuasis(quasis) : minifyCssQuasis(quasis);
// Reassemble — keep the marker for IDE syntax-highlighting.
out += marker + ' `';
out += `${marker} \``;
for (let q = 0; q < minified.length; q++) {
out += minified[q];
if (q < expressions.length) {
out += '${' + expressions[q] + '}';
out += `\${${expressions[q]}}`;
}
}
out += '`';
@@ -104,7 +104,7 @@ function processTemplates(code) {
// ---------------------------------------------------------------------------
/** Minify the static parts of an HTML template literal. */
function minifyHtmlQuasis(quasis) {
function minifyHtmlQuasis(quasis: string[]): string[] {
return quasis.map((q, qIdx) => {
let s = q;
@@ -130,7 +130,7 @@ function minifyHtmlQuasis(quasis) {
* while respecting quoted attribute values (so `> <` inside an attribute
* like `title="a > <b"` is left untouched).
*/
function collapseInterTagWhitespace(s) {
function collapseInterTagWhitespace(s: string): string {
let out = '';
let i = 0;
@@ -144,7 +144,7 @@ function collapseInterTagWhitespace(s) {
let j = i + 1;
while (j < s.length && s[j] !== '>') {
if (s[j] === '"' || s[j] === "'") {
const q = s[j++];
const q = s[j++]!;
while (j < s.length && s[j] !== q) j++;
if (j < s.length) j++; // skip closing quote
} else {
@@ -182,23 +182,23 @@ const EXPR_PLACEHOLDER = '___EXPR_';
* Expressions are replaced with safe placeholder tokens before minification,
* then restored afterward.
*/
function minifyCssQuasis(quasis) {
function minifyCssQuasis(quasis: string[]): string[] {
// Fast path: single quasi (no expressions) — minify directly.
if (quasis.length === 1) {
return [minifyCss(quasis[0]).trim()];
return [minifyCss(quasis[0]!).trim()];
}
// Join quasis with numbered placeholders so lightningcss sees valid-ish CSS.
const joined = quasis.map((q, i) => (i < quasis.length - 1 ? q + EXPR_PLACEHOLDER + i + '___' : q)).join('');
const joined = quasis.map((q, i) => (i < quasis.length - 1 ? `${q + EXPR_PLACEHOLDER + i}___` : q)).join('');
const minified = minifyCss(joined);
// Split back on placeholders to recover individual quasis.
const result = [];
const result: string[] = [];
let remaining = minified;
for (let i = 0; i < quasis.length - 1; i++) {
const token = EXPR_PLACEHOLDER + i + '___';
const token = `${EXPR_PLACEHOLDER + i}___`;
const tokenIdx = remaining.indexOf(token);
if (tokenIdx === -1) {
@@ -216,7 +216,7 @@ function minifyCssQuasis(quasis) {
return result;
}
function minifyCss(css) {
function minifyCss(css: string): string {
const { code } = transform({
filename: 'template.css',
code: Buffer.from(css),
@@ -230,22 +230,31 @@ function minifyCss(css) {
// Template-literal parser (shared)
// ---------------------------------------------------------------------------
interface ParsedExpression {
src: string;
_end: number;
}
interface ParsedTemplate {
quasis: string[];
expressions: string[];
end: number;
}
/**
* Parse a backtick template literal starting at `start` (the opening `` ` ``).
* Returns the static quasis, the raw expression source strings, and the index
* immediately after the closing backtick.
*/
function parseTemplateLiteral(code, start) {
/** @type {string[]} */
const quasis = [];
/** @type {string[]} */
const expressions = [];
function parseTemplateLiteral(code: string, start: number): ParsedTemplate {
const quasis: string[] = [];
const expressions: ParsedExpression[] = [];
let i = start + 1; // skip opening backtick
let quasi = '';
while (i < code.length) {
const ch = code[i];
const ch = code[i]!;
// Escape sequence — preserve verbatim.
if (ch === '\\' && i + 1 < code.length) {
@@ -267,7 +276,7 @@ function parseTemplateLiteral(code, start) {
quasi = '';
i += 2; // skip ${
expressions.push(collectExpression(code, i));
i = expressions[expressions.length - 1]._end;
i = expressions[expressions.length - 1]!._end;
continue;
}
@@ -282,17 +291,22 @@ function parseTemplateLiteral(code, start) {
// Expression collector — tracks brace depth, skips strings & nested templates.
// ---------------------------------------------------------------------------
interface SkipResult {
text: string;
end: number;
}
/**
* Starting right after the `${`, collect everything up to the matching `}`.
* Returns `{ src, _end }` where `_end` is the index after the closing `}`.
*/
function collectExpression(code, start) {
function collectExpression(code: string, start: number): ParsedExpression {
let i = start;
let depth = 1;
let src = '';
while (i < code.length && depth > 0) {
const ch = code[i];
const ch = code[i]!;
if (ch === '{') {
depth++;
@@ -324,23 +338,23 @@ function collectExpression(code, start) {
}
/** Skip a single- or double-quoted string starting at `start`. */
function skipString(code, start) {
const quote = code[start];
function skipString(code: string, start: number): SkipResult {
const quote = code[start]!;
let text = quote;
let i = start + 1;
while (i < code.length && code[i] !== quote) {
if (code[i] === '\\' && i + 1 < code.length) {
text += code[i] + code[i + 1];
text += code[i]! + code[i + 1]!;
i += 2;
} else {
text += code[i];
text += code[i]!;
i++;
}
}
if (i < code.length) {
text += code[i]; // closing quote
text += code[i]!; // closing quote
i++;
}
@@ -348,13 +362,13 @@ function skipString(code, start) {
}
/** Skip a nested backtick template literal (with its own `${…}` blocks). */
function skipNestedTemplate(code, start) {
function skipNestedTemplate(code: string, start: number): SkipResult {
let text = '`';
let i = start + 1;
let exprDepth = 0;
while (i < code.length) {
const ch = code[i];
const ch = code[i]!;
if (ch === '\\' && i + 1 < code.length) {
text += ch + code[i + 1];
@@ -380,6 +394,6 @@ function skipNestedTemplate(code, start) {
return { text, end: i };
}
function isWhitespace(ch) {
function isWhitespace(ch: string): boolean {
return ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r';
}
-27
View File
@@ -1,27 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
/**
* Resolves `@import` directives in CSS content, inlining referenced files.
*
* @param {string} content - The CSS content to resolve.
* @param {string} baseDir - The directory to resolve relative imports from.
* @param {string} skinsDir - The directory to resolve `@videojs/skins` imports from.
* @returns {string} The resolved CSS content.
*/
export function resolveImports(content, baseDir, skinsDir) {
return content.replace(/@import\s+['"]([^'"]+)['"]\s*;/g, (_, importPath) => {
let file;
if (importPath.startsWith('@videojs/skins/')) {
file = resolve(skinsDir, importPath.replace('@videojs/skins/', ''));
} else if (importPath.startsWith('.')) {
file = resolve(baseDir, importPath);
} else {
return _;
}
const nested = readFileSync(file, 'utf-8');
return resolveImports(nested, dirname(file), skinsDir);
});
}
+20
View File
@@ -0,0 +1,20 @@
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
/** Resolves `@import` directives in CSS content, inlining referenced files. */
export function resolveImports(content: string, baseDir: string, skinsDir: string): string {
return content.replace(/@import\s+['"]([^'"]+)['"]\s*;/g, (match, importPath: string) => {
let file: string;
if (importPath.startsWith('@videojs/skins/')) {
file = resolve(skinsDir, importPath.replace('@videojs/skins/', ''));
} else if (importPath.startsWith('.')) {
file = resolve(baseDir, importPath);
} else {
return match;
}
const nested = readFileSync(file, 'utf-8');
return resolveImports(nested, dirname(file), skinsDir);
});
}
@@ -1,16 +1,16 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { inlineTemplatePlugin } from '../inline-template-plugin.mjs';
import { inlineTemplatePlugin } from '../inline-template-plugin.ts';
// Helper: run the plugin's transform on a code string.
function transform(code) {
function transform(code: string): string {
const plugin = inlineTemplatePlugin({ minify: true });
const result = plugin.transform(code);
const result = plugin.transform?.(code, 'test.ts');
return result?.code ?? code;
}
// Helper: extract the minified template body (between the backticks).
function minifyHTML(template) {
function minifyHTML(template: string): string {
const code = `/*html*/ \`${template}\``;
const result = transform(code);
const match = result.match(/`([\s\S]*)`/);
@@ -70,10 +70,7 @@ describe('minifyHtmlQuasis', () => {
});
it('strips comments between elements', () => {
assert.equal(
minifyHTML('<div></div><!-- separator --><span></span>'),
'<div></div><span></span>'
);
assert.equal(minifyHTML('<div></div><!-- separator --><span></span>'), '<div></div><span></span>');
});
// ------- empty elements -------
@@ -100,10 +97,7 @@ describe('minifyHtmlQuasis', () => {
<slot></slot>
<slot name="poster"></slot>
`;
assert.equal(
minifyHTML(input),
'<slot name="media"></slot><slot></slot><slot name="poster"></slot>'
);
assert.equal(minifyHTML(input), '<slot name="media"></slot><slot></slot><slot name="poster"></slot>');
});
it('preserves empty void-like elements', () => {
@@ -113,24 +107,15 @@ describe('minifyHtmlQuasis', () => {
// ------- attribute values with special characters -------
it('does not corrupt attribute values containing > <', () => {
assert.equal(
minifyHTML('<div data-expr="a > b < c"></div>'),
'<div data-expr="a > b < c"></div>'
);
assert.equal(minifyHTML('<div data-expr="a > b < c"></div>'), '<div data-expr="a > b < c"></div>');
});
it('does not corrupt single-quoted attribute values containing > <', () => {
assert.equal(
minifyHTML("<div data-expr='a > b < c'></div>"),
"<div data-expr='a > b < c'></div>"
);
assert.equal(minifyHTML("<div data-expr='a > b < c'></div>"), "<div data-expr='a > b < c'></div>");
});
it('handles attribute values with > followed by space and <', () => {
assert.equal(
minifyHTML('<div title="test > <value"></div>'),
'<div title="test > <value"></div>'
);
assert.equal(minifyHTML('<div title="test > <value"></div>'), '<div title="test > <value"></div>');
});
// ------- text content -------
@@ -247,12 +232,12 @@ describe('minifyCssQuasis', () => {
describe('edge cases', () => {
it('returns null when minify is disabled', () => {
const plugin = inlineTemplatePlugin({ minify: false });
assert.equal(plugin.transform('/*html*/ `<div></div>`'), null);
assert.equal(plugin.transform?.('/*html*/ `<div></div>`', 'test.ts'), null);
});
it('returns null when code contains no markers', () => {
const plugin = inlineTemplatePlugin({ minify: true });
assert.equal(plugin.transform('const x = 1;'), null);
assert.equal(plugin.transform?.('const x = 1;', 'test.ts'), null);
});
it('returns null when marker is not followed by a template literal', () => {
@@ -262,10 +247,7 @@ describe('edge cases', () => {
});
it('handles multiple HTML templates in one file', () => {
const code = [
'const a = /*html*/ `<div> </div>`;',
'const b = /*html*/ `<span> </span>`;',
].join('\n');
const code = ['const a = /*html*/ `<div> </div>`;', 'const b = /*html*/ `<span> </span>`;'].join('\n');
const result = transform(code);
assert.match(result, /`<div><\/div>`/);
assert.match(result, /`<span><\/span>`/);
+17
View File
@@ -0,0 +1,17 @@
/**
* Minimal rolldown plugin interface covering only the hooks used by our build
* plugins. The full `Plugin` type lives in `rolldown` which is a transitive
* dependency (via tsdown) and not directly resolvable from the repo root.
*/
export interface BuildPlugin {
name: string;
transform?: (this: void, code: string, id: string) => { code: string } | null;
resolveId?: (
this: void,
source: string,
importer: string | undefined
) => { id: string; moduleSideEffects: boolean } | null;
load?: (this: void, id: string) => { code: string; moduleSideEffects: boolean } | null;
buildStart?: (this: { addWatchFile: (file: string) => void }) => void;
writeBundle?: (this: void) => void;
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": false,
"declaration": false,
"declarationMap": false,
"emitDeclarationOnly": false,
"noEmit": true,
"allowImportingTsExtensions": true,
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["plugins/**/*.ts"]
}
+2 -2
View File
@@ -3,8 +3,8 @@ import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { UserConfig } from 'tsdown';
import { defineConfig } from 'tsdown';
import { inlineCssPlugin } from '../../build/plugins/inline-css-plugin.mjs';
import { inlineTemplatePlugin } from '../../build/plugins/inline-template-plugin.mjs';
import { inlineCssPlugin } from '../../build/plugins/inline-css-plugin.ts';
import { inlineTemplatePlugin } from '../../build/plugins/inline-template-plugin.ts';
type BuildMode = 'dev' | 'prod';
+3 -3
View File
@@ -3,9 +3,9 @@ import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { UserConfig } from 'tsdown';
import { defineConfig } from 'tsdown';
import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.mjs';
import { inlineCssPlugin } from '../../build/plugins/inline-css-plugin.mjs';
import { inlineTemplatePlugin } from '../../build/plugins/inline-template-plugin.mjs';
import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.ts';
import { inlineCssPlugin } from '../../build/plugins/inline-css-plugin.ts';
import { inlineTemplatePlugin } from '../../build/plugins/inline-template-plugin.ts';
type BuildMode = 'dev' | 'default';
+1 -1
View File
@@ -2,7 +2,7 @@ import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { UserConfig } from 'tsdown';
import { defineConfig } from 'tsdown';
import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.mjs';
import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.ts';
type BuildMode = 'dev' | 'default';
+1 -1
View File
@@ -14,7 +14,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import ts from 'typescript';
import { resolveImports } from '../../build/plugins/resolve-css-imports.mjs';
import { resolveImports } from '../../build/plugins/resolve-css-imports.ts';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '../..');