diff --git a/packages/fontless/README.md b/packages/fontless/README.md index a4fedd6f..13e731ab 100644 --- a/packages/fontless/README.md +++ b/packages/fontless/README.md @@ -9,7 +9,7 @@ ## Features -- 🚀 **Optimized font loading**: Automatically loads and configures fonts with proper fallbacks +- 🚀 **Optimized font loading**: Automatically loads and configures fonts with proper fallbacks and preload links. - 🔤 **Multiple provider support**: Google Fonts, Bunny Fonts, FontShare, FontSource, and more using [unifont](https://github.com/unjs/unifont) - 📦 **Zero runtime overhead**: Pure CSS solution with no JavaScript required at runtime - 📏 **Metric-based fallbacks**: Reduces Cumulative Layout Shift (CLS) by using font metrics from [fontaine](https://github.com/unjs/fontaine) @@ -115,6 +115,95 @@ fontless({ }) ``` +## Preloading Fonts + +Fontless provides an option to select fonts to preload via `preload` option. For Vite SPA, the selected preload fonts are automatically injected into the HTML. + +For SSR meta-frameworks which don't rely on [`transformIndexHtml` plugin hook](https://vite.dev/guide/api-plugin.html#transformindexhtml), you need to manually render preload links on the server. Fontless provides `fontless/runtime` module for server to access the necessary data for preload links generation, for example: + +- Vanilla + +```tsx +import { preloads } from "fontless/runtime"; + +function renderHtml() { + const renderedPreloads = preloads + .map( + (attrs) => + ``, + ) + .join("\n"); + return `\ + + + ${renderedPreloads} + + + ... + + +`; +} +``` + +- [Qwik](./examples/qwik-app) + +```tsx +import { preloads } from "fontless/runtime" + +export const RouterHead = component$(() => { + return ( + <> + {preloads.map((l) => ( + + ))} + ... + + ) +}) +``` + +- [React](./examples/react-router-app) + +```tsx +import { preloads } from 'fontless/runtime' + +function Layout() { + return ( + + + {preloads.map(({crossorigin, ...attrs}) => ( + + ))} + ... + + + ... + + + ) +} +``` + +- [SvelteKit]./examples/sveltekit-app) + +```svelte + + + + + {#each preloads as attrs} + + {/each} + +``` + ## How It Works Fontless works by: diff --git a/packages/fontless/build.config.ts b/packages/fontless/build.config.ts index 55adfa7d..b8143cc0 100644 --- a/packages/fontless/build.config.ts +++ b/packages/fontless/build.config.ts @@ -5,5 +5,9 @@ export default defineBuildConfig({ rollup: { dts: { respectExternal: false }, }, + entries: [ + 'src/index.ts', + 'src/runtime.ts', + ], externals: ['vite'], }) diff --git a/packages/fontless/e2e/basic.test.ts b/packages/fontless/e2e/basic.test.ts index 5e1f2b03..7b4a74f1 100644 --- a/packages/fontless/e2e/basic.test.ts +++ b/packages/fontless/e2e/basic.test.ts @@ -38,3 +38,86 @@ test.describe('dev vanilla', () => { ) }) }) + +test.describe('build react-rotuer', () => { + let cli: ReturnType + let baseURL: string + + test.beforeAll(async () => { + cli = runCli({ command: 'pnpm build', cwd: 'examples/react-router-app' }) + await cli.done + cli = runCli({ command: 'pnpm start', cwd: 'examples/react-router-app' }) + const port = await cli.findPort() + baseURL = `http://localhost:${port}` + }) + + test.afterAll(async () => { + if (!cli) + return + cli.kill() + await cli.done + }) + + test('basic', async ({ page }) => { + await page.goto(baseURL) + const fonts = await page.evaluate(async () => { + const fonts = await (globalThis as any).document.fonts.ready + return [...fonts].map((f: any) => ({ family: f.family, status: f.status })) + }) + expect(fonts).toEqual( + expect.arrayContaining([ + { family: 'Poppins', status: 'loaded' }, + ]), + ) + }) + + test.describe('no js', () => { + test.use({ javaScriptEnabled: false }) + + test('ssr preload links', async ({ page }) => { + await page.goto(baseURL) + await expect(page.locator('head > link[rel="preload"][as="font"]').first()).toBeAttached() + }) + }) +}) + +test.describe('build sveltekit', () => { + let cli: ReturnType + let baseURL: string + + test.beforeAll(async () => { + const build = runCli({ command: 'pnpm build', cwd: 'examples/sveltekit-app' }) + await build.done + cli = runCli({ command: 'pnpm preview', cwd: 'examples/sveltekit-app' }) + const port = await cli.findPort() + baseURL = `http://localhost:${port}` + }) + + test.afterAll(async () => { + if (!cli) + return + cli.kill() + await cli.done + }) + + test('basic', async ({ page }) => { + await page.goto(baseURL) + const fonts = await page.evaluate(async () => { + const fonts = await (globalThis as any).document.fonts.ready + return [...fonts].map((f: any) => ({ family: f.family, status: f.status })) + }) + expect(fonts).toEqual( + expect.arrayContaining([ + { family: 'Fira Sans', status: 'loaded' }, + ]), + ) + }) + + test.describe('no js', () => { + test.use({ javaScriptEnabled: false }) + test('ssr preload links', async ({ page }) => { + await page.goto(baseURL) + await expect(page.locator('head > link[rel="preload"][as="font"]').first()).toBeAttached() + }) + }) +}) diff --git a/packages/fontless/examples/qwik-app/package.json b/packages/fontless/examples/qwik-app/package.json index fe212c69..e0d3b530 100644 --- a/packages/fontless/examples/qwik-app/package.json +++ b/packages/fontless/examples/qwik-app/package.json @@ -15,11 +15,11 @@ "build": "qwik build", "build.client": "vite build", "build.preview": "vite build --ssr src/entry.preview.tsx", - "build.types": "tsc --incremental --noEmit", + "x-build.types": "tsc --incremental --noEmit", "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", "dev": "vite --mode ssr --port 5173", "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", - "lint": "eslint \"src/**/*.ts*\"", + "x-lint": "eslint \"src/**/*.ts*\"", "preview": "qwik build preview && vite preview --open --port 5173", "start": "vite --open --mode ssr", "qwik": "qwik" diff --git a/packages/fontless/examples/qwik-app/src/components/router-head/router-head.tsx b/packages/fontless/examples/qwik-app/src/components/router-head/router-head.tsx index 8a35f4e1..f057fba2 100644 --- a/packages/fontless/examples/qwik-app/src/components/router-head/router-head.tsx +++ b/packages/fontless/examples/qwik-app/src/components/router-head/router-head.tsx @@ -1,5 +1,6 @@ import { component$ } from '@qwik.dev/core' import { useDocumentHead, useLocation } from '@qwik.dev/router' +import { preloads } from "fontless/runtime" /** * The RouterHead component is placed inside of the document `` element. @@ -24,6 +25,10 @@ export const RouterHead = component$(() => { ))} + {preloads.map((l) => ( + + ))} + {head.styles.map(s => ( diff --git a/packages/fontless/examples/svelte-app/src/app.css b/packages/fontless/examples/svelte-app/src/app.css deleted file mode 100644 index c54ee7d1..00000000 --- a/packages/fontless/examples/svelte-app/src/app.css +++ /dev/null @@ -1,4 +0,0 @@ -:root { - color: rgba(255, 255, 255, 0.87); - background-color: #242424; -} diff --git a/packages/fontless/examples/svelte-app/src/black-fox.ttf b/packages/fontless/examples/svelte-app/src/black-fox.ttf deleted file mode 100644 index 9497048b..00000000 Binary files a/packages/fontless/examples/svelte-app/src/black-fox.ttf and /dev/null differ diff --git a/packages/fontless/examples/svelte-app/src/main.ts b/packages/fontless/examples/svelte-app/src/main.ts deleted file mode 100644 index 702d4b59..00000000 --- a/packages/fontless/examples/svelte-app/src/main.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { mount } from 'svelte' -import App from './App.svelte' -import './app.css' - -const app = mount(App, { - target: document.getElementById('app')!, -}) - -export default app diff --git a/packages/fontless/examples/svelte-app/src/vite-env.d.ts b/packages/fontless/examples/svelte-app/src/vite-env.d.ts deleted file mode 100644 index 4078e747..00000000 --- a/packages/fontless/examples/svelte-app/src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/packages/fontless/examples/svelte-app/svelte.config.js b/packages/fontless/examples/svelte-app/svelte.config.js deleted file mode 100644 index b0683fd2..00000000 --- a/packages/fontless/examples/svelte-app/svelte.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), -} diff --git a/packages/fontless/examples/svelte-app/tsconfig.app.json b/packages/fontless/examples/svelte-app/tsconfig.app.json deleted file mode 100644 index cde3b9d3..00000000 --- a/packages/fontless/examples/svelte-app/tsconfig.app.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "@tsconfig/svelte/tsconfig.json", - "compilerOptions": { - "target": "ESNext", - "moduleDetection": "force", - "useDefineForClassFields": true, - "module": "ESNext", - "resolveJsonModule": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable checkJs if you'd like to use dynamic types in JS. - * Note that setting allowJs false does not prevent the use - * of JS in `.svelte` files. - */ - "allowJs": true, - "checkJs": true, - "isolatedModules": true - }, - "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] -} diff --git a/packages/fontless/examples/svelte-app/tsconfig.json b/packages/fontless/examples/svelte-app/tsconfig.json deleted file mode 100644 index a2efd469..00000000 --- a/packages/fontless/examples/svelte-app/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ], - "files": [] -} diff --git a/packages/fontless/examples/svelte-app/tsconfig.node.json b/packages/fontless/examples/svelte-app/tsconfig.node.json deleted file mode 100644 index d6fbfae8..00000000 --- a/packages/fontless/examples/svelte-app/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "moduleDetection": "force", - "module": "ESNext", - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - - /* Linting */ - "strict": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noEmit": true, - "isolatedModules": true, - "skipLibCheck": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/fontless/examples/svelte-app/vite.config.ts b/packages/fontless/examples/svelte-app/vite.config.ts deleted file mode 100644 index 15731a80..00000000 --- a/packages/fontless/examples/svelte-app/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { svelte } from '@sveltejs/vite-plugin-svelte' -import { fontless } from 'fontless' -import { defineConfig } from 'vite' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [svelte(), fontless()], -}) diff --git a/packages/fontless/examples/sveltekit-app/.gitignore b/packages/fontless/examples/sveltekit-app/.gitignore new file mode 100644 index 00000000..3b462cb0 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/packages/fontless/examples/sveltekit-app/.npmrc b/packages/fontless/examples/sveltekit-app/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/packages/fontless/examples/sveltekit-app/README.md b/packages/fontless/examples/sveltekit-app/README.md new file mode 100644 index 00000000..75842c40 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/packages/fontless/examples/sveltekit-app/package.json b/packages/fontless/examples/sveltekit-app/package.json new file mode 100644 index 00000000..2012509c --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "sveltekit-app", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "fontless": "latest", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^7.0.4" + } +} diff --git a/packages/fontless/examples/sveltekit-app/src/app.css b/packages/fontless/examples/sveltekit-app/src/app.css new file mode 100644 index 00000000..ce9adc67 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/app.css @@ -0,0 +1,3 @@ +:root { + font-family: "Fira Sans", sans-serif; +} diff --git a/packages/fontless/examples/sveltekit-app/src/app.d.ts b/packages/fontless/examples/sveltekit-app/src/app.d.ts new file mode 100644 index 00000000..da08e6da --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/fontless/examples/sveltekit-app/src/app.html b/packages/fontless/examples/sveltekit-app/src/app.html new file mode 100644 index 00000000..f273cc58 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/fontless/examples/sveltekit-app/src/lib/assets/favicon.svg b/packages/fontless/examples/sveltekit-app/src/lib/assets/favicon.svg new file mode 100644 index 00000000..cc5dc66a --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/packages/fontless/examples/sveltekit-app/src/routes/+layout.svelte b/packages/fontless/examples/sveltekit-app/src/routes/+layout.svelte new file mode 100644 index 00000000..03fc2096 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/routes/+layout.svelte @@ -0,0 +1,16 @@ + + + + + {#each preloads as attrs} + + {/each} + + +{@render children?.()} diff --git a/packages/fontless/examples/sveltekit-app/src/routes/+page.svelte b/packages/fontless/examples/sveltekit-app/src/routes/+page.svelte new file mode 100644 index 00000000..cc88df0e --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/packages/fontless/examples/sveltekit-app/static/robots.txt b/packages/fontless/examples/sveltekit-app/static/robots.txt new file mode 100644 index 00000000..b6dd6670 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/packages/fontless/examples/sveltekit-app/svelte.config.js b/packages/fontless/examples/sveltekit-app/svelte.config.js new file mode 100644 index 00000000..1295460d --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/packages/fontless/examples/sveltekit-app/tsconfig.json b/packages/fontless/examples/sveltekit-app/tsconfig.json new file mode 100644 index 00000000..a5567ee6 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/packages/fontless/examples/sveltekit-app/vite.config.ts b/packages/fontless/examples/sveltekit-app/vite.config.ts new file mode 100644 index 00000000..e80b7224 --- /dev/null +++ b/packages/fontless/examples/sveltekit-app/vite.config.ts @@ -0,0 +1,15 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import { fontless } from "fontless" + +export default defineConfig({ + plugins: [ + sveltekit(), + fontless({ + provider: 'google', + defaults: { + preload: true, + } + }) + ] +}); diff --git a/packages/fontless/package.json b/packages/fontless/package.json index cd210e73..9be48329 100644 --- a/packages/fontless/package.json +++ b/packages/fontless/package.json @@ -23,7 +23,8 @@ ], "sideEffects": false, "exports": { - ".": "./dist/index.mjs" + ".": "./dist/index.mjs", + "./runtime": "./dist/runtime.mjs" }, "main": "./dist/index.mjs", "module": "./dist/index.mjs", diff --git a/packages/fontless/src/runtime.ts b/packages/fontless/src/runtime.ts new file mode 100644 index 00000000..e35555f4 --- /dev/null +++ b/packages/fontless/src/runtime.ts @@ -0,0 +1,9 @@ +/** the runtime value is generated via plugin */ +export const preloads: LinkAttributes[] = [] + +export interface LinkAttributes { + rel: 'preload' + as: 'font' + href: string + crossorigin: 'anonymous' | 'use-credentials' | '' | undefined +} diff --git a/packages/fontless/src/vite.ts b/packages/fontless/src/vite.ts index a3dffd27..4046cb5a 100644 --- a/packages/fontless/src/vite.ts +++ b/packages/fontless/src/vite.ts @@ -1,10 +1,12 @@ -import type { Plugin } from 'vite' +import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import type { NormalizeFontDataContext } from './assets' +import type { LinkAttributes } from './runtime' import type { FontlessOptions } from './types' -import type { FontFamilyInjectionPluginOptions } from './utils' +import type { FontFamilyInjectionPluginOptions } from './utils' import { Buffer } from 'node:buffer' import { defu } from 'defu' +import MagicString from 'magic-string' import { joinURL } from 'ufo' import { normalizeFontData } from './assets' import { defaultOptions } from './defaults' @@ -19,16 +21,30 @@ const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/ // Copied from vue-bundle-renderer utils const CSS_EXTENSIONS_RE = /\.(?:css|scss|sass|postcss|pcss|less|stylus|styl)(?:\?[^.]+)?$/ -export function fontless(_options?: FontlessOptions): Plugin { +export function fontless(_options?: FontlessOptions): Plugin[] { const options = defu(_options, defaultOptions satisfies FontlessOptions) as FontlessOptions let cssTransformOptions: FontFamilyInjectionPluginOptions let assetContext: NormalizeFontDataContext + let resolvedConfig: ResolvedConfig + let server: ViteDevServer | undefined + const RUNTIME_NAME = 'fontless/runtime' + + function getPreloads(): LinkAttributes[] { + const hrefs = [...cssTransformOptions.fontsToPreload.values()].flatMap(v => [...v]) + return hrefs.map(href => ({ + rel: 'preload', + as: 'font', + href, + crossorigin: '', + })) + } - return { + const mainPlugin: Plugin = { name: 'vite-plugin-fontless', apply: (_config, env) => !env.isPreview, async configResolved(config) { + resolvedConfig = config assetContext = { dev: config.mode === 'development', renderedFontURLs: new Map(), @@ -74,9 +90,10 @@ export function fontless(_options?: FontlessOptions): Plugin { } } }, - configureServer(server) { + configureServer(server_) { // serve font assets via middleware during dev // based on https://github.com/nuxt/fonts/blob/e7f537a0357896d34be9c17031b3178fb4e79042/src/assets.ts#L30 + server = server_ server.middlewares.use(assetContext.assetsBaseURL, async (req, res, next) => { try { const filename = req.url!.slice(1) @@ -113,6 +130,10 @@ export function fontless(_options?: FontlessOptions): Plugin { const s = await transformCSS(cssTransformOptions, code, id) if (s.hasChanged()) { + // invalidate virtual module to ensure fresh preloads list during dev + if (server) { + invalidateModuleByid(server, `\0${RUNTIME_NAME}`) + } return { code: s.toString(), map: s.generateMap({ hires: true }), @@ -143,17 +164,79 @@ export function fontless(_options?: FontlessOptions): Plugin { handler() { // Preload doesn't work on initial rendering during dev since `fontsToPreload` // is empty before css is transformed. - const hrefs = [...cssTransformOptions.fontsToPreload.values()].flatMap(v => [...v]) - return hrefs.map(href => ({ + return getPreloads().map(attrs => ({ tag: 'link', - attrs: { - rel: 'preload', - as: 'font', - href, - crossorigin: '', - }, + attrs: attrs as unknown as Record, })) }, }, } + + const RUNTIME_PLACEHOLDER = '__FONTLESS_RUNTIME_BUILD_PLACEHOLDER__' + const runtimePlugin: Plugin = { + name: 'fontless-runtime', + config() { + return { + ssr: { + // ensure 'fontless/runtime' is loaded through vite + noExternal: ['fontless'], + }, + } + }, + configEnvironment() { + return { + resolve: { + noExternal: ['fontless'], + }, + } + }, + resolveId: { + // override Vite's node resolution + order: 'pre', + handler(source) { + if (source === RUNTIME_NAME) { + return `\0${RUNTIME_NAME}` + } + }, + }, + load: { + handler(id) { + if (id === `\0${RUNTIME_NAME}`) { + // during build, postpone replacement until `renderChunk` + // to ensure fonts are collected through css transform + if (resolvedConfig.command === 'build') { + return `export const { preloads } = ${RUNTIME_PLACEHOLDER}` + } + return `export const { preloads } = ${JSON.stringify({ preloads: getPreloads() })}` + } + }, + }, + renderChunk(code, _chunk) { + if (code.includes(RUNTIME_PLACEHOLDER)) { + const s = new MagicString(code) + s.replaceAll( + RUNTIME_PLACEHOLDER, + JSON.stringify({ preloads: getPreloads() }), + ) + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } + } + }, + } + + return [ + mainPlugin, + runtimePlugin, + ] +} + +function invalidateModuleByid(server: ViteDevServer, id: string) { + for (const environment of Object.values(server.environments)) { + const mod = environment.moduleGraph.getModuleById(id) + if (mod) { + environment.moduleGraph.invalidateModule(mod) + } + } } diff --git a/packages/fontless/test/e2e.spec.ts b/packages/fontless/test/e2e.spec.ts index 5c7ec3fe..b26857f5 100644 --- a/packages/fontless/test/e2e.spec.ts +++ b/packages/fontless/test/e2e.spec.ts @@ -6,10 +6,13 @@ import { join, resolve } from 'pathe' import { build } from 'vite' import { describe, expect, it } from 'vitest' -const fixtures = await Array.fromAsync(fsp.glob('*', { +let fixtures = await Array.fromAsync(fsp.glob('*', { cwd: fileURLToPath(new URL('../examples', import.meta.url)), })) +// tested via playwright +fixtures = fixtures.filter(i => !['react-router-app', 'sveltekit-app'].includes(i)) + describe.each(fixtures)('e2e %s', (fixture) => { it('should compile', { timeout: 20_000 }, async () => { const root = fileURLToPath(new URL(`../examples/${fixture}`, import.meta.url)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62139fda..bc7e6d96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -452,28 +452,31 @@ importers: specifier: ^2.11.8 version: 2.11.8(solid-js@1.9.9)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) - packages/fontless/examples/svelte-app: + packages/fontless/examples/sveltekit-app: devDependencies: + '@sveltejs/adapter-auto': + specifier: ^6.0.0 + version: 6.1.0(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))) + '@sveltejs/kit': + specifier: ^2.22.0 + version: 2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) '@sveltejs/vite-plugin-svelte': - specifier: ^5.1.1 - version: 5.1.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) - '@tsconfig/svelte': - specifier: ^5.0.5 - version: 5.0.5 + specifier: ^6.0.0 + version: 6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) fontless: specifier: workspace:* version: link:../.. svelte: - specifier: ^5.39.5 + specifier: ^5.0.0 version: 5.39.5 svelte-check: - specifier: ^4.3.2 + specifier: ^4.0.0 version: 4.3.2(picomatch@4.0.3)(svelte@5.39.5)(typescript@5.9.2) typescript: - specifier: ~5.9.2 + specifier: ^5.0.0 version: 5.9.2 vite: - specifier: ^7.1.7 + specifier: ^7.0.4 version: 7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1) packages/fontless/examples/tailwind: @@ -4214,6 +4217,9 @@ packages: '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@stylistic/eslint-plugin@5.4.0': resolution: {integrity: sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4225,20 +4231,38 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1': - resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22} + '@sveltejs/adapter-auto@6.1.0': + resolution: {integrity: sha512-shOuLI5D2s+0zTv2ab5M5PqfknXqWbKi+0UwB9yLTRIdzsK1R93JOO8jNhIYSHdW+IYXIYnLniu+JZqXs7h9Wg==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.43.5': + resolution: {integrity: sha512-44Mm5csR4mesKx2Eyhtk8UVrLJ4c04BT2wMTfYGKJMOkUqpHP5KLL2DPV0hXUA4t4+T3ZYe0aBygd42lVYv2cA==} + engines: {node: '>=18.13'} + hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^5.0.0 + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': + resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 svelte: ^5.0.0 - vite: ^6.0.0 + vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@5.1.1': - resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22} + '@sveltejs/vite-plugin-svelte@6.2.1': + resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.0.0 - vite: ^6.0.0 + vite: ^6.3.0 || ^7.0.0 '@swc/core-darwin-arm64@1.13.5': resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} @@ -4427,9 +4451,6 @@ packages: '@ts-morph/common@0.22.0': resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} - '@tsconfig/svelte@5.0.5': - resolution: {integrity: sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ==} - '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4468,6 +4489,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/css-tree@2.3.10': resolution: {integrity: sha512-WcaBazJ84RxABvRttQjjFWgTcHvZR9jGr0Y3hccPkHjFyk/a3N8EuxjKr+QfrwjoM5b1yI1Uj1i7EzOAAwBwag==} @@ -5893,6 +5917,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -15661,6 +15689,8 @@ snapshots: '@speed-highlight/core@1.2.7': {} + '@standard-schema/spec@1.0.0': {} + '@stylistic/eslint-plugin@5.4.0(eslint@9.36.0(jiti@2.6.0))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) @@ -15675,21 +15705,43 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/adapter-auto@6.1.0(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))': + dependencies: + '@sveltejs/kit': 2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) + + '@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.3.2 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.19 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.2 + svelte: 5.39.5 + vite: 7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1) + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) debug: 4.4.3 svelte: 5.39.5 vite: 7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.39.5)(vite@7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1)) debug: 4.4.3 deepmerge: 4.3.1 - kleur: 4.1.5 magic-string: 0.30.19 svelte: 5.39.5 vite: 7.1.7(@types/node@22.18.7)(jiti@2.6.0)(less@4.2.2)(lightningcss@1.30.2)(sass@1.85.0)(terser@5.44.0)(yaml@2.8.1) @@ -15843,8 +15895,6 @@ snapshots: mkdirp: 3.0.1 path-browserify: 1.0.1 - '@tsconfig/svelte@5.0.5': {} - '@tufjs/canonical-json@2.0.0': {} '@tufjs/models@3.0.1': @@ -15900,6 +15950,8 @@ snapshots: dependencies: '@types/node': 22.18.7 + '@types/cookie@0.6.0': {} + '@types/css-tree@2.3.10': {} '@types/debug@4.1.12': @@ -17766,6 +17818,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie@0.6.0: {} + cookie@0.7.1: {} cookie@1.0.2: {}