diff --git a/packages/fontaine/README.md b/packages/fontaine/README.md
index 35f33774..ba508d2e 100644
--- a/packages/fontaine/README.md
+++ b/packages/fontaine/README.md
@@ -43,6 +43,16 @@ Or, with `yarn`
yarn add -D fontaine
```
+## CLI usage
+
+You can also run fontaine directly against a CSS file:
+
+```bash
+npx fontaine ./src/styles.css ./dist/styles.css
+```
+
+If the output file is omitted, fontaine writes to `.fontaine.css`.
+
## Usage
```js
diff --git a/packages/fontaine/package.json b/packages/fontaine/package.json
index 04041c28..92cb03d9 100644
--- a/packages/fontaine/package.json
+++ b/packages/fontaine/package.json
@@ -33,6 +33,9 @@
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+ "bin": {
+ "fontaine": "./dist/cli.mjs"
+ },
"files": [
"dist"
],
diff --git a/packages/fontaine/src/cli.ts b/packages/fontaine/src/cli.ts
new file mode 100644
index 00000000..f5ad58f5
--- /dev/null
+++ b/packages/fontaine/src/cli.ts
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+import type { FontaineTransformOptions } from './transform'
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { basename, dirname, extname, resolve } from 'node:path'
+import process from 'node:process'
+import { pathToFileURL } from 'node:url'
+import { FontaineTransform } from './transform'
+
+/** Options for transforming a CSS file from the fontaine CLI or API. */
+export interface FontaineCliOptions extends Partial {
+ /** CSS file to transform. */
+ input: string
+ /** Output file path. Defaults to `.fontaine.css`. */
+ output?: string
+}
+
+/**
+ * Transform a CSS file with Fontaine and write the generated output.
+ *
+ * @returns The resolved output path.
+ */
+export async function transformCssFile({ input, output, ...options }: FontaineCliOptions): Promise {
+ const inputPath = resolve(input)
+ const source = readFileSync(inputPath, 'utf8')
+ const { fallbacks = ['Arial'], resolvePath, ...transformOptions } = options
+ const plugin = FontaineTransform.rollup({
+ ...transformOptions,
+ fallbacks,
+ resolvePath: resolvePath || (id => pathToFileURL(resolve(dirname(inputPath), id)).toString()),
+ })
+ const transformPlugin = Array.isArray(plugin) ? plugin[0]! : plugin
+ const transform = transformPlugin.transform
+ if (!transform || typeof transform === 'string' || typeof transform === 'function')
+ throw new Error('Fontaine transform hook is unavailable')
+
+ const transformed = await transform.handler.call({} as any, source, inputPath)
+ const code = typeof transformed === 'string' ? transformed : (transformed?.code ?? source)
+ const outputPath = output
+ ? resolve(output)
+ : `${inputPath.slice(0, Math.max(0, inputPath.length - extname(inputPath).length))}.fontaine.css`
+
+ mkdirSync(dirname(outputPath), { recursive: true })
+ writeFileSync(outputPath, code)
+ return outputPath
+}
+
+/** Print command-line usage information. */
+function printHelp() {
+ console.log(`Usage: fontaine [output.css]
+
+Transforms a CSS file and writes fallback font-face rules using fontaine.
+
+Arguments:
+ input.css CSS file to transform
+ output.css Output path. Defaults to .fontaine.css
+`)
+}
+
+/** Run the Fontaine command-line interface. */
+async function main() {
+ const [, , ...args] = process.argv
+
+ if (args.includes('-h') || args.includes('--help')) {
+ printHelp()
+ return
+ }
+
+ const [input, output] = args
+
+ if (args.length > 2) {
+ console.error('Too many positional arguments. Expected: fontaine [output.css]')
+ process.exitCode = 1
+ return
+ }
+
+ if (!input) {
+ printHelp()
+ process.exitCode = 1
+ return
+ }
+
+ if (!existsSync(input)) {
+ console.error(`Input file not found: ${input}`)
+ process.exitCode = 1
+ return
+ }
+
+ const outputPath = await transformCssFile({ input, output })
+ console.log(`Generated ${basename(outputPath)}`)
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main().catch((error) => {
+ console.error(error)
+ process.exitCode = 1
+ })
+}
diff --git a/packages/fontaine/src/index.ts b/packages/fontaine/src/index.ts
index c31a2ad9..041febf4 100644
--- a/packages/fontaine/src/index.ts
+++ b/packages/fontaine/src/index.ts
@@ -1,7 +1,8 @@
+export { transformCssFile } from './cli'
+export type { FontaineCliOptions } from './cli'
export { generateFallbackName, generateFontFace } from './css'
export { DEFAULT_CATEGORY_FALLBACKS, type FontCategory, resolveCategoryFallbacks } from './fallbacks'
export type { ResolveCategoryFallbacksOptions } from './fallbacks'
export { getMetricsForFamily, readMetrics } from './metrics'
-
export { FontaineTransform } from './transform'
export type { FontaineTransformOptions } from './transform'
diff --git a/packages/fontaine/tsdown.config.ts b/packages/fontaine/tsdown.config.ts
index 5bcf1e28..fae2385e 100644
--- a/packages/fontaine/tsdown.config.ts
+++ b/packages/fontaine/tsdown.config.ts
@@ -2,7 +2,7 @@ import fs from 'node:fs'
import { defineConfig } from 'tsdown'
export default defineConfig({
- entry: ['src/index.ts'],
+ entry: ['src/index.ts', 'src/cli.ts'],
format: ['es', 'cjs'],
dts: {
oxc: true,