├── .github └── workflows │ ├── ci.yml │ ├── pkg-pr-new.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENCE ├── README.md ├── eslint.config.js ├── index.html ├── package.json ├── playground ├── index.html ├── package.json ├── public │ ├── favicon-dark.svg │ └── favicon.svg ├── src │ ├── main.ts │ └── vite-env.d.ts ├── test │ └── vite.test.ts ├── tsconfig.json └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── pack-wasm.js ├── src ├── index.ts ├── shims.d.ts ├── utils.ts ├── vite.ts └── wasm.js ├── test ├── esm.test.ts ├── iife.browser.ts └── shared.ts ├── tsconfig.json ├── tsdown.config.ts ├── vitest.browser.config.ts └── vitest.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - master 10 | pull_request: 11 | branches: 12 | - main 13 | - master 14 | workflow_dispatch: {} 15 | merge_group: {} 16 | 17 | concurrency: 18 | group: ci-${{ github.event.pull_request.number || github.ref }} 19 | cancel-in-progress: true 20 | 21 | env: 22 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright 23 | 24 | jobs: 25 | ci: 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest, windows-latest] 29 | fail-fast: false 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | # workaround for npm registry key change 34 | # ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack 35 | # - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091 36 | - run: npm i -g --force corepack && corepack enable 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: 22 40 | cache: pnpm 41 | 42 | - name: 📦 Install dependencies 43 | run: pnpm install --frozen-lockfile 44 | 45 | - name: Install Playwright Dependencies 46 | run: pnpm exec playwright install chromium --with-deps --only-shell 47 | 48 | - name: Lint 49 | run: pnpm lint 50 | 51 | - name: Build project 52 | run: pnpm prepack 53 | 54 | - name: Test project 55 | run: pnpm test:ci 56 | timeout-minutes: 10 57 | -------------------------------------------------------------------------------- /.github/workflows/pkg-pr-new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | tags: 9 | - '!**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Set node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | cache: pnpm 29 | 30 | - name: Install 31 | run: pnpm install 32 | 33 | - name: Build 34 | run: pnpm prepack 35 | 36 | - name: Release 37 | run: pnpm dlx pkg-pr-new publish --no-template --pnpm 38 | # run: pnpm dlx pkg-pr-new publish --compact --no-template --pnpm 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - run: pnpm dlx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | 29 | # # Uncomment the following lines to publish to npm on CI 30 | # 31 | # - run: pnpm install 32 | # - run: pnpm publish --no-git-checks -r --access public 33 | # env: 34 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | # NPM_CONFIG_PROVENANCE: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .idea/ 5 | src/ttf2woff2.wasm.js 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | shell-emulator=true -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020-PRESENT Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVG Packer 2 | 3 | Pack SVGs to Icon Fonts - **In Browser**! 4 | 5 | > This was born from [Icônes](https://github.com/antfu-collective/icones), an icon explorer that allows you to choice from 6,000+ icons then pack what you want into iconfonts! Do check it out :) 6 | 7 | ### NPM 8 | 9 | ```bash 10 | npm i svg-packer 11 | ``` 12 | 13 | ### CDN 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | ### Playground 20 | 21 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/antfu/svg-packer) 22 | 23 | > [!NOTE] 24 | > The Vitest tests won't work in StackBlitz, the Vite playground will be started. 25 | 26 | ## Usage 27 | 28 | Packing: 29 | 30 | ```js 31 | const result = await SvgPacker({ 32 | fontName: 'My Awesome Font', 33 | fileName: 'awesome-font', 34 | cssPrefix: 'af', 35 | icons: [{ 36 | name: 'add', 37 | svg: '...svg content' 38 | }, { 39 | name: 'pencil', 40 | svg: '...svg content' 41 | }] 42 | }) 43 | 44 | // Download zip with all files 45 | save(result.zip.url) 46 | save(result.zip.blob) 47 | 48 | // Download individual font files 49 | save(result.files.svg.url) // svg font 50 | save(result.files.ttf.url) 51 | save(result.files.woff.url) 52 | save(result.files.woff2.url) 53 | save(result.files.css.url) 54 | save(result.files.demoHTML.url) 55 | ``` 56 | 57 | Use: 58 | 59 | ```html 60 | 61 | 62 | 63 | 64 | ``` 65 | 66 | ## Vite 67 | 68 | From version `v1.0.0` you can use `svg-packer` with Vite: 69 | 70 | Add the following plugin to your `vite.config.ts` file 71 | 72 | ```js 73 | // vite.config.ts 74 | import { SvgPackerVitePlugin } from 'svg-packer/vite' 75 | import { defineConfig } from 'vite' 76 | 77 | export default defineConfig({ 78 | plugins: [SvgPackerVitePlugin()], 79 | }) 80 | ``` 81 | 82 | then in your logic use static or dynamic import: 83 | 84 | ```ts 85 | import { SvgPacker } from 'svg-packer' 86 | 87 | const result = await SvgPacker({ /* options */ }) 88 | // Download zip with all files 89 | save(result.zip.url) 90 | save(result.zip.blob) 91 | 92 | // Download individual font files 93 | save(result.files.svg.url) // svg font 94 | save(result.files.ttf.url) 95 | save(result.files.woff.url) 96 | save(result.files.woff2.url) 97 | save(result.files.css.url) 98 | save(result.files.demoHTML.url) 99 | ``` 100 | 101 | ## Running tests in your local environment 102 | 103 | > [!NOTE] 104 | > To run the tests in your local environment, you will need: 105 | > - Node 22 LTS or later 106 | > - Install playwright browsers by running `pnpm exec playwright install` 107 | 108 | We're using [Vitest](https://vitest.dev) for testing the library, you can run the following tests in your local environment using the following scripts from the root folder: 109 | - `test:node`: run ESM test in Node in watch mode ([esm.test.ts](./test/esm.test.ts)) 110 | - `test:node:run`: run ESM test in Node without the watch mode ([esm.test.ts](./test/esm.test.ts)) 111 | - `test:node:ui`: run ESM test in Node in watch mode with Vitest UI reporter ([esm.test.ts](./test/esm.test.ts)) 112 | - `test:browser`: run IIFE test in the browser in watch mode using Vitest Browser mode with Playwright ([iife.browser.ts](./test/iife.browser.ts)) 113 | - `test:browser:headless`: run IIFE test in the browser using Vitest Browser mode with Playwright with the headless mode ([iife.browser.ts](./test/iife.browser.ts)) 114 | - `test:browser:preview`: run IIFE test in the browser in watch mode using Vitest Browser mode with your default browser ([iife.browser.ts](./test/iife.browser.ts)) 115 | - `test:playground`: run ESM test with Vite in the browser in watch mode using Vitest Browser mode with Playwright ([vite.test.ts](./playground/test/vite.test.ts)) 116 | - `test:playground:headless`: run ESM test with Vite in the browser using Vitest Browser mode with Playwright with the headless mode ([vite.test.ts](./playground/test/vite.test.ts)) 117 | - `test`: run ESM test in Node in watch mode ([esm.test.ts](./test/esm.test.ts)) and IIFE test in the browser in watch mode using Vitest Browser mode with your default browser ([iife.browser.ts](./test/iife.browser.ts)) 118 | - `test:headless`: run ESM test in Node without the watch mode ([esm.test.ts](./test/esm.test.ts)), IIFE test in the browser using Vitest Browser mode with Playwright with the headless mode ([iife.browser.ts](./test/iife.browser.ts)) and ESM test with Vite in the browser using Vitest Browser mode with Playwright with the headless mode ([vite.test.ts](./playground/test/vite.test.ts)) 119 | 120 | The `test:ci` should be used only in CI environments, it will run the same tests in `test:headless`. 121 | 122 | ## License 123 | 124 | MIT License © 2020-PRESENT [Anthony Fu](https://github.com/antfu) 125 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | ignores: [ 5 | '**/dist/**', 6 | '**/node_modules/**', 7 | // dont change vite layout: pnpm create vite > Vanilla > TypesScript 8 | '**/playground/tsconfig.json', 9 | './src/wasm.js', 10 | ], 11 | }, { 12 | rules: { 13 | 'no-restricted-globals': 'off', 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SVG Pack Demo 8 | 9 | 10 | 11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-packer", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "packageManager": "pnpm@10.11.0", 6 | "description": "Pack SVGs to Icon Fonts - In Browser!", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/antfu/svg-packer", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antfu/svg-packer" 14 | }, 15 | "bugs": "https://github.com/antfu/svg-packer/issues", 16 | "exports": { 17 | ".": "./dist/index.mjs", 18 | "./vite": "./dist/vite.mjs" 19 | }, 20 | "main": "dist/index.mjs", 21 | "module": "dist/index.mjs", 22 | "unpkg": "dist/index.browser.js", 23 | "jsdelivr": "dist/index.browser.js", 24 | "types": "dist/index.d.mts", 25 | "typesVersions": { 26 | "*": { 27 | "vite": [ 28 | "dist/vite.d.mts" 29 | ] 30 | } 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "scripts": { 36 | "lint": "eslint .", 37 | "lint:fix": "nr lint --fix", 38 | "build": "tsdown", 39 | "prepack": "nr build", 40 | "prepare": "node scripts/pack-wasm.js", 41 | "dev:serve": "nr prepack && pnpx serve .", 42 | "play": "nr -C playground dev", 43 | "release": "bumpp && pnpm publish", 44 | "test:node": "vitest --project=node", 45 | "test:node:run": "vitest --project=node run", 46 | "test:node:ui": "vitest --project=node --open --ui", 47 | "test:browser": "vitest --config=vitest.browser.config.ts", 48 | "test:browser:preview": "vitest --project=browser", 49 | "test:browser:headless": "CI=true vitest --config=vitest.browser.config.ts", 50 | "test:playground": "pnpm -C playground run test", 51 | "test:playground:ci": "pnpm -C playground run test:ci", 52 | "test:playground:headless": "pnpm -C playground run test:headless", 53 | "test": "tsc --noEmit && vitest", 54 | "test:ci": "tsc --noEmit && nr test:node:run && nr test:browser && nr test:playground:ci", 55 | "test:headless": "tsc --noEmit && nr test:node:run && nr test:browser:headless && nr test:playground:headless" 56 | }, 57 | "dependencies": { 58 | "client-zip": "2.5.0", 59 | "node-stdlib-browser": "^1.3.1", 60 | "readable-stream": "latest", 61 | "svg2ttf": "6.0.3", 62 | "ttf2eot": "3.1.0", 63 | "ttf2woff": "3.0.0", 64 | "ttf2woff2": "6.0.1", 65 | "vite-plugin-node-polyfills": "^0.23.0" 66 | }, 67 | "devDependencies": { 68 | "@antfu/eslint-config": "^4.13.2", 69 | "@antfu/ni": "^24.4.0", 70 | "@vitest/browser": "^3.1.4", 71 | "@vitest/ui": "^3.1.4", 72 | "bumpp": "^10.1.1", 73 | "esbuild": "^0.25.4", 74 | "eslint": "^9.27.0", 75 | "playwright": "^1.52.0", 76 | "std-env": "^3.9.0", 77 | "svgicons2svgfont": "15.0.1", 78 | "tsdown": "^0.12.3", 79 | "typescript": "^5.8.3", 80 | "vite": "^6.3.5", 81 | "vitest": "3.1.4" 82 | }, 83 | "stackblitz": { 84 | "installDependencies": true, 85 | "startCommand": "nr prepack && pnpm -C playground run dev" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SVG Pack Demo 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "test": "vitest", 11 | "test:headless": "CI=true vitest run", 12 | "test:ci": "vitest run" 13 | }, 14 | "dependencies": { 15 | "svg-packer": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "vite-plugin-node-polyfills": "^0.23.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /playground/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { options } from '../../test/shared.ts' 2 | 3 | import('svg-packer').then(async ({ SvgPacker }) => { 4 | const result = await SvgPacker(options) 5 | 6 | const a = document.createElement('a') 7 | a.href = result.zip.url! 8 | a.download = result.zip.name 9 | a.textContent = `Download ${result.zip.name}` 10 | document.body.append(a) 11 | 12 | // eslint-disable-next-line no-console 13 | console.log(result) 14 | if (import.meta.env.MODE === 'test') { 15 | // @ts-expect-error I'm lazy to augment this 16 | window.SvgPackerResult = result 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/test/vite.test.ts: -------------------------------------------------------------------------------- 1 | import type { SvgPackerResult } from '../../src' 2 | import { page } from '@vitest/browser/context' 3 | import { expect, it } from 'vitest' 4 | import { appendIconsToTheDomBody, expectedFontName, options, replaceCssFontUrls } from '../../test/shared' 5 | 6 | import '../src/main' 7 | 8 | it('svg-packer in Vite', async () => { 9 | await expect.poll(() => 'SvgPacker' in window).toBeTruthy() 10 | 11 | const download = page.getByRole('link', { hasText: 'Download' }) 12 | 13 | await expect.element(download).toBeTruthy() 14 | await expect.element(download).toHaveTextContent('Download maf.zip') 15 | await expect.element(download).toHaveAttribute( 16 | 'href', 17 | expect.stringContaining('blob:http:'), 18 | ) 19 | const href = download.element().getAttribute('href') 20 | const data = await fetch(href).then(r => r.blob()) 21 | expect(data.size).toBeGreaterThan(0) 22 | await expect.poll(() => 'SvgPackerResult' in window).toBeTruthy() 23 | if ('SvgPackerResult' in window) { 24 | const result = window.SvgPackerResult as SvgPackerResult 25 | expect(result.files).toBeTruthy() 26 | expect(Array.from(Object.values(result.files)).map(m => m.name)).toMatchInlineSnapshot(` 27 | [ 28 | "maf.svg", 29 | "maf.ttf", 30 | "maf.eot", 31 | "maf.woff", 32 | "maf.woff2", 33 | "maf.css", 34 | "_demo.html", 35 | ] 36 | `) 37 | const css = result.files.css 38 | expect(css).toBeTruthy() 39 | expect(css.blob).toBeTruthy() 40 | expect(css.blob.size).toBeGreaterThan(0) 41 | let cssContent = await css.blob.text() 42 | expect(cssContent).toMatchInlineSnapshot(` 43 | " 44 | @font-face { 45 | font-family: "My Awesome Font"; 46 | src: url("./maf.eot"); 47 | src: url("./maf.eot") format("embedded-opentype"), 48 | url("./maf.ttf") format("truetype"), 49 | url("./maf.woff") format("woff"), 50 | url("./maf.woff2") format("woff2"), 51 | url("./maf.svg") format("svg"); 52 | font-weight: normal; 53 | font-style: normal; 54 | } 55 | 56 | .i { 57 | font-family: "My Awesome Font" !important; 58 | font-size: 1em; 59 | font-style: normal; 60 | -webkit-font-smoothing: antialiased; 61 | -moz-osx-font-smoothing: grayscale; 62 | } 63 | 64 | 65 | .i.mdi\\:account-edit:before { 66 | content: "\\e001"; 67 | } 68 | 69 | .i.mdi\\:account-tie:before { 70 | content: "\\e002"; 71 | } 72 | 73 | " 74 | `) 75 | cssContent = replaceCssFontUrls(cssContent, result) 76 | const style = globalThis.document.createElement('style') 77 | style.textContent = `${cssContent} 78 | i { 79 | padding: 5px; 80 | color: #717171; 81 | display: inline-block; 82 | font-size: 1.2em; 83 | } 84 | ` 85 | globalThis.document.head.append(style) 86 | await new Promise(resolve => setTimeout(resolve, 100)) 87 | const iconDeclarations = await appendIconsToTheDomBody() 88 | const fontName = expectedFontName(options) 89 | for (const { css } of iconDeclarations) { 90 | expect(css).toBeTruthy() 91 | expect(css.content).toBeTruthy() 92 | expect(css.fontFamily).toBe(fontName) 93 | expect(css.fontStyle).toBe('normal') 94 | } 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { isCI } from 'std-env' 3 | import { SvgPackerVitePlugin } from 'svg-packer/vite' 4 | import { defineConfig } from 'vite' 5 | 6 | export default defineConfig({ 7 | server: { 8 | fs: { 9 | allow: [ 10 | resolve(__dirname, '..'), 11 | ], 12 | }, 13 | }, 14 | plugins: [SvgPackerVitePlugin()], 15 | test: { 16 | browser: { 17 | enabled: true, 18 | headless: isCI, 19 | provider: 'playwright', 20 | instances: [ 21 | { browser: 'chromium' }, 22 | ], 23 | }, 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | onlyBuiltDependencies: 4 | - esbuild 5 | - ttf2woff2 6 | - unrs-resolver 7 | -------------------------------------------------------------------------------- /scripts/pack-wasm.js: -------------------------------------------------------------------------------- 1 | import * as Buffer from 'node:buffer' 2 | import * as fsp from 'node:fs/promises' 3 | import { resolve } from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | const root = fileURLToPath(new URL('..', import.meta.url)) 7 | 8 | packWasm() 9 | 10 | async function packWasm() { 11 | const file = await fsp.readFile(resolve(root, `./node_modules/ttf2woff2/jssrc/ttf2woff2.wasm`)) 12 | await fsp.writeFile(resolve(root, './src/ttf2woff2.wasm.js'), `export const wasmUrl = 'data:application/wasm;base64,${Buffer.Buffer.from(file).toString('base64')}';`, 'utf8') 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { BufferLike, InputWithSizeMeta } from 'client-zip' 2 | import type { FileExtension } from './utils' 3 | import * as buffer from 'node:buffer' 4 | import * as process from 'node:process' 5 | import { PassThrough } from 'node:stream' 6 | import { StringDecoder } from 'node:string_decoder' 7 | import { downloadZip } from 'client-zip' 8 | import svg2ttf from 'svg2ttf' 9 | import { SVGIcons2SVGFontStream } from 'svgicons2svgfont' 10 | import ttf2eot from 'ttf2eot' 11 | import ttf2woff from 'ttf2woff' 12 | 13 | export type { FileExtension } from './utils' 14 | export { FileExtensions } from './utils' 15 | 16 | export interface SvgPackerOptions { 17 | /** 18 | * @default 'iconfont' 19 | */ 20 | fontName?: string 21 | /** 22 | * @default 'iconfont' 23 | */ 24 | cssPrefix?: string 25 | /** 26 | * @default 'iconfont' 27 | */ 28 | fileName?: string 29 | /** 30 | * @default 'iconfont' 31 | */ 32 | startCodepoint?: number 33 | /** 34 | * @default 1000 35 | */ 36 | fontHeight?: number 37 | /** 38 | * @default 0 39 | */ 40 | descent?: number 41 | /** 42 | * @default false 43 | */ 44 | fixedWidth?: boolean 45 | icons: { 46 | svg: string 47 | name: string 48 | unicode?: number 49 | }[] 50 | } 51 | 52 | export interface SvgPackerResult { 53 | files: Record 58 | zip: { 59 | name: string 60 | blob: Blob 61 | url?: string 62 | } 63 | } 64 | 65 | export async function SvgPacker({ icons, ...options }: SvgPackerOptions): Promise { 66 | const { 67 | fontName = 'iconfont', 68 | cssPrefix = 'iconfont', 69 | fileName = 'iconfont', 70 | startCodepoint = 0xE001, 71 | fontHeight = 1000, 72 | descent = 0, 73 | fixedWidth = false, 74 | } = options ?? {} 75 | 76 | const parsedIcons = icons.map(({ svg, name, unicode }, i) => { 77 | return { 78 | svg, 79 | name, 80 | unicode: unicode ?? (startCodepoint + i), 81 | } 82 | }) 83 | 84 | const iconStreams = parsedIcons.map(({ svg, unicode, name }) => { 85 | const iconStream = new PassThrough() 86 | iconStream.write(svg, 'utf8') 87 | iconStream.end() 88 | iconStream.metadata = { 89 | unicode: [String.fromCharCode(unicode)], 90 | name, 91 | } 92 | return iconStream 93 | }) 94 | 95 | const files = {} as SvgPackerResult['files'] 96 | 97 | const zip = await downloadZip(generateZipEntries( 98 | iconStreams, 99 | { 100 | fontName, 101 | cssPrefix, 102 | fileName, 103 | startCodepoint, 104 | fontHeight, 105 | descent, 106 | fixedWidth, 107 | icons: parsedIcons, 108 | }, 109 | files, 110 | )).blob() 111 | 112 | return { 113 | files, 114 | zip: { 115 | name: `${options.fileName}.zip`, 116 | blob: zip, 117 | url: makeUrl(zip), 118 | }, 119 | } 120 | } 121 | 122 | function addFile( 123 | files: SvgPackerResult['files'], 124 | filename: string, 125 | ext: FileExtension, 126 | data: BufferLike, 127 | mime = 'text/plain', 128 | ) { 129 | const blob = new Blob([data], { type: mime }) 130 | files[ext] = { 131 | name: filename, 132 | blob, 133 | url: makeUrl(blob), 134 | } 135 | return { 136 | name: filename, 137 | input: blob, 138 | url: makeUrl(blob), 139 | } satisfies InputWithSizeMeta & { url?: string } 140 | } 141 | 142 | function makeUrl(blob: Blob | MediaSource) { 143 | if (typeof window === 'undefined' || !window.URL || !window.URL.createObjectURL) 144 | return null 145 | return window.URL.createObjectURL(blob) 146 | } 147 | 148 | function makeSVG(iconStreams: PassThrough[], options: Omit) { 149 | return new Promise((resolve) => { 150 | const fontStream = new SVGIcons2SVGFontStream(options) 151 | const parts = [] 152 | const decoder = new StringDecoder('utf8') 153 | fontStream.on('data', (chunk) => { 154 | parts.push(decoder.write(chunk)) 155 | }) 156 | fontStream.on('finish', () => { 157 | resolve(parts.join('')) 158 | }) 159 | iconStreams.forEach(fontStream.write.bind(fontStream)) 160 | fontStream.end() 161 | }) 162 | } 163 | 164 | function makeTTF(svgFont: BufferLike) { 165 | const ttfFontBuffer: Uint8Array = svg2ttf(svgFont).buffer 166 | 167 | return ttfFontBuffer 168 | } 169 | 170 | function makeEOT(ttfFontBuffer: Uint8Array) { 171 | const eotFontBuffer = ttf2eot(ttfFontBuffer).buffer 172 | 173 | return eotFontBuffer 174 | } 175 | 176 | function makeWOFF(ttfFontBuffer: Uint8Array) { 177 | const woffFontBuffer = ttf2woff(new Uint8Array(ttfFontBuffer.buffer)).buffer 178 | return woffFontBuffer 179 | } 180 | 181 | async function browserPromise() { 182 | const [{ wasmUrl }, { initWasmBrowser, ttf2woff2 }] = await Promise.all([ 183 | import('./ttf2woff2.wasm'), 184 | import('./wasm'), 185 | ]) 186 | 187 | await initWasmBrowser(wasmUrl) 188 | return ttf2woff2 189 | } 190 | 191 | async function preloadWasm() { 192 | const isNode: boolean 193 | = typeof process < 'u' 194 | && typeof process.stdout < 'u' 195 | && !process.versions?.deno 196 | && !globalThis.window 197 | 198 | return isNode 199 | ? await import('ttf2woff2/jssrc/index.js').then((m) => { 200 | return m.default || m 201 | }).catch((e) => { 202 | console.error('ERROR', e) 203 | Promise.reject(e) 204 | }) 205 | : await browserPromise() 206 | } 207 | 208 | async function makeWOFF2(ttfFontBuffer: Uint8Array) { 209 | ttfFontBuffer = new Uint8Array(ttfFontBuffer) 210 | let buf = buffer.Buffer.alloc(ttfFontBuffer.length) 211 | for (let i = 0, j = ttfFontBuffer.length; i < j; i++) 212 | buf.writeUInt8(ttfFontBuffer[i], i) 213 | 214 | const ttf2woff2 = await preloadWasm() 215 | 216 | if (!ttf2woff2 || !(typeof ttf2woff2 === 'function')) 217 | throw new Error('ttf2woff2 not found') 218 | 219 | if (ttf2woff2 instanceof Error) 220 | throw ttf2woff2 221 | 222 | buf = ttf2woff2(buf) as buffer.Buffer 223 | const woff2FontBuffer = new Uint8Array(buf.length) 224 | for (let i = 0, j = buf.length; i < j; i++) 225 | woff2FontBuffer[i] = buf.readUInt8(i) 226 | 227 | return woff2FontBuffer 228 | } 229 | 230 | function makeCSS({ icons, fontName, fileName, cssPrefix }: SvgPackerOptions) { 231 | const css = ` 232 | @font-face { 233 | font-family: "${fontName}"; 234 | src: url("./${fileName}.eot"); 235 | src: url("./${fileName}.eot") format("embedded-opentype"), 236 | url("./${fileName}.ttf") format("truetype"), 237 | url("./${fileName}.woff") format("woff"), 238 | url("./${fileName}.woff2") format("woff2"), 239 | url("./${fileName}.svg") format("svg"); 240 | font-weight: normal; 241 | font-style: normal; 242 | } 243 | 244 | .${cssPrefix} { 245 | font-family: "${fontName}" !important; 246 | font-size: 1em; 247 | font-style: normal; 248 | -webkit-font-smoothing: antialiased; 249 | -moz-osx-font-smoothing: grayscale; 250 | } 251 | 252 | ${icons.map(({ name, unicode }) => ` 253 | .${cssPrefix}.${name.replace(/:/g, '\\:')}:before { 254 | content: "\\${unicode.toString(16)}"; 255 | } 256 | `).join('')} 257 | ` 258 | 259 | return css 260 | } 261 | 262 | function makeDemoHTML({ icons, fontName, fileName, cssPrefix }: SvgPackerOptions) { 263 | return ` 264 | 265 | 266 | 267 | 268 | ${fontName} Demo 269 | 270 | 280 | 281 | 282 | ${icons.map(({ name }) => ``).join('')} 283 | 284 | 285 | ` 286 | } 287 | 288 | async function* generateZipEntries( 289 | iconStreams: PassThrough[], 290 | options: SvgPackerOptions, 291 | files: SvgPackerResult['files'], 292 | ): AsyncGenerator<{ 293 | name: string 294 | input: Blob 295 | url?: string 296 | }> { 297 | const content = await makeSVG(iconStreams, options) as BufferLike 298 | yield addFile(files, `${options.fileName}.svg`, 'svg', content, 'image/svg+xml') 299 | const ttfFontBuffer = makeTTF(content) 300 | yield addFile(files, `${options.fileName}.ttf`, 'ttf', ttfFontBuffer, 'application/octet-stream') 301 | yield addFile(files, `${options.fileName}.eot`, 'eot', makeEOT(ttfFontBuffer), 'application/octet-stream') 302 | yield addFile(files, `${options.fileName}.woff`, 'woff', makeWOFF(ttfFontBuffer), 'application/octet-stream') 303 | yield addFile(files, `${options.fileName}.woff2`, 'woff2', await makeWOFF2(ttfFontBuffer), 'application/octet-stream') 304 | yield addFile(files, `${options.fileName}.css`, 'css', makeCSS(options), 'text/css') 305 | yield addFile(files, '_demo.html', 'demoHTML', makeDemoHTML(options), 'text/html') 306 | } 307 | 308 | if (typeof window !== 'undefined') { 309 | window.SvgPacker = SvgPacker 310 | } 311 | if (typeof self !== 'undefined') { 312 | self.SvgPacker = SvgPacker 313 | } 314 | if (typeof globalThis !== 'undefined') { 315 | globalThis.SvgPacker = SvgPacker 316 | } 317 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | // not included in the pack 2 | import type { SvgPacker } from './index' 3 | 4 | declare module 'node:stream' { 5 | interface PassThrough { 6 | metadata?: { 7 | unicode: string[] 8 | name: string 9 | } 10 | } 11 | } 12 | 13 | declare global { 14 | interface Window { 15 | SvgPacker: SvgPacker 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // don't move this to index.ts, we need to import both in the shared test package and will break the browser test 2 | export const FileExtensions = ['eot', 'ttf', 'woff', 'woff2', 'svg', 'css', 'demoHTML'] as const 3 | export type FileExtension = typeof FileExtensions[number] 4 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite' 2 | import { nodePolyfills } from 'vite-plugin-node-polyfills' 3 | 4 | export function SvgPackerVitePlugin(): PluginOption { 5 | return [{ 6 | name: 'vite-svg-packer-optimize-deps-plugin', 7 | config() { 8 | return { 9 | optimizeDeps: { 10 | include: [ 11 | 'sax', 12 | 'svg-pathdata', 13 | 'transformation-matrix', 14 | 'yerror', 15 | 'debug', 16 | 'stream', 17 | 'vite-plugin-node-polyfills/shims/buffer', 18 | 'vite-plugin-node-polyfills/shims/global', 19 | 'vite-plugin-node-polyfills/shims/process', 20 | 'node:fs', 21 | 'node:path', 22 | 'node:stream', 23 | 'node:string_decoder', 24 | 'client-zip', 25 | 'svg2ttf', 26 | 'ttf2eot', 27 | 'ttf2woff', 28 | 'ttf2woff2/jssrc/index.js', 29 | ], 30 | }, 31 | } 32 | }, 33 | }, nodePolyfills({ 34 | include: [ 35 | 'buffer', 36 | 'fs', 37 | 'path', 38 | 'stream', 39 | 'string_decoder', 40 | ], 41 | protocolImports: true, 42 | })] 43 | } 44 | -------------------------------------------------------------------------------- /src/wasm.js: -------------------------------------------------------------------------------- 1 | var Module = typeof Module != 'undefined' ? Module : {} 2 | 3 | let wasmMemory; 4 | let ABORT = false; 5 | let HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; 6 | 7 | function updateMemoryViews() { 8 | const b = wasmMemory.buffer; 9 | Module.HEAP8 = HEAP8 = new Int8Array(b); 10 | Module.HEAP16 = HEAP16 = new Int16Array(b); 11 | Module.HEAPU8 = HEAPU8 = new Uint8Array(b); 12 | Module.HEAPU16 = HEAPU16 = new Uint16Array(b); 13 | Module.HEAP32 = HEAP32 = new Int32Array(b); 14 | Module.HEAPU32 = HEAPU32 = new Uint32Array(b); 15 | Module.HEAPF32 = HEAPF32 = new Float32Array(b); 16 | Module.HEAPF64 = HEAPF64 = new Float64Array(b) 17 | } 18 | 19 | const __ATPRERUN__ = []; 20 | const __ATINIT__ = []; 21 | const __ATPOSTRUN__ = []; 22 | 23 | function preRun() { 24 | if (Module.preRun) { 25 | if (typeof Module.preRun == 'function') 26 | Module.preRun = [Module.preRun]; 27 | while (Module.preRun.length) { 28 | addOnPreRun(Module.preRun.shift()) 29 | } 30 | } 31 | callRuntimeCallbacks(__ATPRERUN__) 32 | } 33 | 34 | function initRuntime() { 35 | callRuntimeCallbacks(__ATINIT__) 36 | } 37 | 38 | function postRun() { 39 | if (Module.postRun) { 40 | if (typeof Module.postRun == 'function') 41 | Module.postRun = [Module.postRun]; 42 | while (Module.postRun.length) { 43 | addOnPostRun(Module.postRun.shift()) 44 | } 45 | } 46 | callRuntimeCallbacks(__ATPOSTRUN__) 47 | } 48 | 49 | function addOnPreRun(cb) { 50 | __ATPRERUN__.unshift(cb) 51 | } 52 | 53 | function addOnInit(cb) { 54 | __ATINIT__.unshift(cb) 55 | } 56 | 57 | function addOnPostRun(cb) { 58 | __ATPOSTRUN__.unshift(cb) 59 | } 60 | 61 | let runDependencies = 0; 62 | let runDependencyWatcher = null; 63 | let dependenciesFulfilled = null; 64 | 65 | function addRunDependency(id) { 66 | runDependencies++; 67 | Module.monitorRunDependencies?.(runDependencies) 68 | } 69 | 70 | function removeRunDependency(id) { 71 | runDependencies--; 72 | Module.monitorRunDependencies?.(runDependencies); 73 | if (runDependencies == 0) { 74 | if (runDependencyWatcher !== null) { 75 | clearInterval(runDependencyWatcher); 76 | runDependencyWatcher = null 77 | } 78 | if (dependenciesFulfilled) { 79 | const callback = dependenciesFulfilled; 80 | dependenciesFulfilled = null; 81 | callback() 82 | } 83 | } 84 | } 85 | 86 | function abort(what) { 87 | Module.onAbort?.(what); 88 | what = `Aborted(${what})`; 89 | err(what); 90 | ABORT = true; 91 | what += '. Build with -sASSERTIONS for more info.'; 92 | const e = new WebAssembly.RuntimeError(what); 93 | throw e 94 | } 95 | 96 | function getWasmImports() { 97 | return { 98 | env: wasmImports, 99 | wasi_snapshot_preview1: wasmImports, 100 | memory: new WebAssembly.Memory({ initial: 10, maximum: 100 }) 101 | } 102 | } 103 | 104 | let wasmUrl; 105 | const wasmPromise = () => fetch(wasmUrl?.default || wasmUrl) 106 | async function loadWasmModule(info) { 107 | if (Module.wasmExports) { 108 | return [Module.wasmExports, undefined]; 109 | } 110 | try { 111 | const response = await wasmPromise(); 112 | 113 | if (!response) { 114 | throw new Error('no response when fetching WASM file'); 115 | } 116 | 117 | if (!response.ok) { 118 | throw new Error(`error fetching WASM file: ${response.status} ${response.statusText}`); 119 | } 120 | const { instance, module } = await WebAssembly.instantiateStreaming(response, info); 121 | 122 | console.log("WebAssembly module successfully loaded and instantiated."); 123 | console.log("WASM Instance:", instance); 124 | return [instance, module]; 125 | 126 | } catch (error) { 127 | console.error("Error loading or instantiating the WebAssembly module:", error); 128 | throw error; 129 | } 130 | } 131 | 132 | async function createWasm() { 133 | const info = getWasmImports(); 134 | 135 | function receiveInstance(instance, module) { 136 | wasmExports = instance.exports; 137 | wasmMemory = wasmExports.memory; 138 | updateMemoryViews(); 139 | wasmTable = wasmExports.__indirect_function_table; 140 | addOnInit(wasmExports.__wasm_call_ctors); 141 | removeRunDependency('wasm-instantiate'); 142 | return wasmExports 143 | } 144 | 145 | addRunDependency('wasm-instantiate'); 146 | const result = await loadWasmModule(info); 147 | return receiveInstance(result[0]) 148 | } 149 | 150 | var callRuntimeCallbacks = (callbacks) => { 151 | while (callbacks.length > 0) { 152 | callbacks.shift()(Module) 153 | } 154 | }; 155 | 156 | function getValue(ptr, type = 'i8') { 157 | if (type.endsWith('*')) 158 | type = '*'; 159 | switch (type) { 160 | case 'i1': 161 | return HEAP8[ptr]; 162 | case 'i8': 163 | return HEAP8[ptr]; 164 | case 'i16': 165 | return HEAP16[ptr >> 1]; 166 | case 'i32': 167 | return HEAP32[ptr >> 2]; 168 | case 'i64': 169 | abort('to do getValue(i64) use WASM_BIGINT'); 170 | case 'float': 171 | return HEAPF32[ptr >> 2]; 172 | case 'double': 173 | return HEAPF64[ptr >> 3]; 174 | case '*': 175 | return HEAPU32[ptr >> 2]; 176 | default: 177 | abort(`invalid type for getValue: ${type}`) 178 | } 179 | } 180 | 181 | const noExitRuntime = Module.noExitRuntime || true; 182 | const UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder() : undefined; 183 | 184 | function UTF8ArrayToString(heapOrArray, idx, maxBytesToRead) { 185 | const endIdx = idx + maxBytesToRead; 186 | let endPtr = idx; 187 | while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; 188 | if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { 189 | return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)) 190 | } 191 | let str = ''; 192 | while (idx < endPtr) { 193 | let u0 = heapOrArray[idx++]; 194 | if (!(u0 & 128)) { 195 | str += String.fromCharCode(u0); 196 | continue 197 | } 198 | const u1 = heapOrArray[idx++] & 63; 199 | if ((u0 & 224) == 192) { 200 | str += String.fromCharCode((u0 & 31) << 6 | u1); 201 | continue 202 | } 203 | const u2 = heapOrArray[idx++] & 63; 204 | if ((u0 & 240) == 224) { 205 | u0 = (u0 & 15) << 12 | u1 << 6 | u2 206 | } else { 207 | u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63 208 | } 209 | if (u0 < 65536) { 210 | str += String.fromCharCode(u0) 211 | } else { 212 | const ch = u0 - 65536; 213 | str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023) 214 | } 215 | } 216 | return str 217 | } 218 | 219 | const UTF8ToString = (ptr, maxBytesToRead) => ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; 220 | 221 | function ___assert_fail(condition, filename, line, func) { 222 | abort(`Assertion failed: ${UTF8ToString(condition)}, at: ${[filename ? UTF8ToString(filename) : 'unknown filename', line, func ? UTF8ToString(func) : 'unknown function']}`) 223 | } 224 | 225 | class ExceptionInfo { 226 | constructor(excPtr) { 227 | this.excPtr = excPtr; 228 | this.ptr = excPtr - 24 229 | } 230 | 231 | set_type(type) { 232 | HEAPU32[this.ptr + 4 >> 2] = type 233 | } 234 | 235 | get_type() { 236 | return HEAPU32[this.ptr + 4 >> 2] 237 | } 238 | 239 | set_destructor(destructor) { 240 | HEAPU32[this.ptr + 8 >> 2] = destructor 241 | } 242 | 243 | get_destructor() { 244 | return HEAPU32[this.ptr + 8 >> 2] 245 | } 246 | 247 | set_caught(caught) { 248 | caught = caught ? 1 : 0; 249 | HEAP8[this.ptr + 12] = caught 250 | } 251 | 252 | get_caught() { 253 | return HEAP8[this.ptr + 12] != 0 254 | } 255 | 256 | set_rethrown(rethrown) { 257 | rethrown = rethrown ? 1 : 0; 258 | HEAP8[this.ptr + 13] = rethrown 259 | } 260 | 261 | get_rethrown() { 262 | return HEAP8[this.ptr + 13] != 0 263 | } 264 | 265 | init(type, destructor) { 266 | this.set_adjusted_ptr(0); 267 | this.set_type(type); 268 | this.set_destructor(destructor) 269 | } 270 | 271 | set_adjusted_ptr(adjustedPtr) { 272 | HEAPU32[this.ptr + 16 >> 2] = adjustedPtr 273 | } 274 | 275 | get_adjusted_ptr() { 276 | return HEAPU32[this.ptr + 16 >> 2] 277 | } 278 | 279 | get_exception_ptr() { 280 | const isPointer = ___cxa_is_pointer_type(this.get_type()); 281 | if (isPointer) { 282 | return HEAPU32[this.excPtr >> 2] 283 | } 284 | const adjusted = this.get_adjusted_ptr(); 285 | if (adjusted !== 0) 286 | return adjusted; 287 | return this.excPtr 288 | } 289 | } 290 | 291 | let exceptionLast = 0; 292 | let uncaughtExceptionCount = 0; 293 | 294 | function ___cxa_throw(ptr, type, destructor) { 295 | const info = new ExceptionInfo(ptr); 296 | info.init(type, destructor); 297 | exceptionLast = ptr; 298 | uncaughtExceptionCount++; 299 | throw exceptionLast 300 | } 301 | 302 | function __abort_js() { 303 | abort('') 304 | } 305 | 306 | function __embind_register_bigint(primitiveType, name, size, minRange, maxRange) { 307 | } 308 | 309 | function embind_init_charCodes() { 310 | const codes = Array.from({length: 256}); 311 | for (let i = 0; i < 256; ++i) { 312 | codes[i] = String.fromCharCode(i) 313 | } 314 | embind_charCodes = codes 315 | } 316 | 317 | let embind_charCodes; 318 | 319 | function readLatin1String(ptr) { 320 | let ret = ''; 321 | let c = ptr; 322 | while (HEAPU8[c]) { 323 | ret += embind_charCodes[HEAPU8[c++]] 324 | } 325 | return ret 326 | } 327 | 328 | const awaitingDependencies = {}; 329 | const registeredTypes = {}; 330 | const typeDependencies = {}; 331 | let BindingError; 332 | 333 | function throwBindingError(message) { 334 | throw new BindingError(message) 335 | } 336 | 337 | let InternalError; 338 | 339 | function throwInternalError(message) { 340 | throw new InternalError(message) 341 | } 342 | 343 | function whenDependentTypesAreResolved(myTypes, dependentTypes, getTypeConverters) { 344 | myTypes.forEach((type) => { 345 | typeDependencies[type] = dependentTypes 346 | }); 347 | 348 | function onComplete(typeConverters) { 349 | const myTypeConverters = getTypeConverters(typeConverters); 350 | if (myTypeConverters.length !== myTypes.length) { 351 | throwInternalError('Mismatched type converter count') 352 | } 353 | for (let i = 0; i < myTypes.length; ++i) { 354 | registerType(myTypes[i], myTypeConverters[i]) 355 | } 356 | } 357 | 358 | const typeConverters = Array.from({length: dependentTypes.length}); 359 | const unregisteredTypes = []; 360 | let registered = 0; 361 | dependentTypes.forEach((dt, i) => { 362 | if (registeredTypes.hasOwnProperty(dt)) { 363 | typeConverters[i] = registeredTypes[dt] 364 | } else { 365 | unregisteredTypes.push(dt); 366 | if (!awaitingDependencies.hasOwnProperty(dt)) { 367 | awaitingDependencies[dt] = [] 368 | } 369 | awaitingDependencies[dt].push(() => { 370 | typeConverters[i] = registeredTypes[dt]; 371 | ++registered; 372 | if (registered === unregisteredTypes.length) { 373 | onComplete(typeConverters) 374 | } 375 | }) 376 | } 377 | }); 378 | if (unregisteredTypes.length === 0) { 379 | onComplete(typeConverters) 380 | } 381 | } 382 | 383 | function sharedRegisterType(rawType, registeredInstance, options = {}) { 384 | const name = registeredInstance.name; 385 | if (!rawType) { 386 | throwBindingError(`type "${name}" must have a positive integer typeid pointer`) 387 | } 388 | if (registeredTypes.hasOwnProperty(rawType)) { 389 | if (options.ignoreDuplicateRegistrations) { 390 | return 391 | } else { 392 | throwBindingError(`Cannot register type '${name}' twice`) 393 | } 394 | } 395 | registeredTypes[rawType] = registeredInstance; 396 | delete typeDependencies[rawType]; 397 | if (awaitingDependencies.hasOwnProperty(rawType)) { 398 | const callbacks = awaitingDependencies[rawType]; 399 | delete awaitingDependencies[rawType]; 400 | callbacks.forEach(cb => cb()) 401 | } 402 | } 403 | 404 | function registerType(rawType, registeredInstance, options = {}) { 405 | if (!('argPackAdvance' in registeredInstance)) { 406 | throw new TypeError('registerType registeredInstance requires argPackAdvance') 407 | } 408 | return sharedRegisterType(rawType, registeredInstance, options) 409 | } 410 | 411 | const GenericWireTypeSize = 8; 412 | 413 | function __embind_register_bool(rawType, name, trueValue, falseValue) { 414 | name = readLatin1String(name); 415 | registerType(rawType, { 416 | name, fromWireType(wt) { 417 | return !!wt 418 | }, toWireType(destructors, o) { 419 | return o ? trueValue : falseValue 420 | }, argPackAdvance: GenericWireTypeSize, readValueFromPointer(pointer) { 421 | return this.fromWireType(HEAPU8[pointer]) 422 | }, destructorFunction: null 423 | }) 424 | } 425 | 426 | const emval_freelist = []; 427 | const emval_handles = []; 428 | 429 | function __emval_decref(handle) { 430 | if (handle > 9 && --emval_handles[handle + 1] === 0) { 431 | emval_handles[handle] = undefined; 432 | emval_freelist.push(handle) 433 | } 434 | } 435 | 436 | const count_emval_handles = () => emval_handles.length / 2 - 5 - emval_freelist.length; 437 | 438 | function init_emval() { 439 | emval_handles.push(0, 1, undefined, 1, null, 1, true, 1, false, 1); 440 | Module.count_emval_handles = count_emval_handles 441 | } 442 | 443 | const Emval = { 444 | toValue: (handle) => { 445 | if (!handle) { 446 | throwBindingError(`Cannot use deleted val. handle = ${handle}`) 447 | } 448 | return emval_handles[handle] 449 | }, toHandle: (value) => { 450 | switch (value) { 451 | case undefined: 452 | return 2; 453 | case null: 454 | return 4; 455 | case true: 456 | return 6; 457 | case false: 458 | return 8; 459 | default: { 460 | const handle = emval_freelist.pop() || emval_handles.length; 461 | emval_handles[handle] = value; 462 | emval_handles[handle + 1] = 1; 463 | return handle 464 | } 465 | } 466 | } 467 | }; 468 | 469 | function readPointer(pointer) { 470 | return this.fromWireType(HEAPU32[pointer >> 2]) 471 | } 472 | 473 | const EmValType = { 474 | name: 'emscripten::val', 475 | fromWireType: (handle) => { 476 | const rv = Emval.toValue(handle); 477 | __emval_decref(handle); 478 | return rv 479 | }, 480 | toWireType: (destructors, value) => Emval.toHandle(value), 481 | argPackAdvance: GenericWireTypeSize, 482 | readValueFromPointer: readPointer, 483 | destructorFunction: null 484 | }; 485 | const __embind_register_emval = rawType => registerType(rawType, EmValType); 486 | 487 | function floatReadValueFromPointer(name, width) { 488 | switch (width) { 489 | case 4: 490 | return function (pointer) { 491 | return this.fromWireType(HEAPF32[pointer >> 2]) 492 | }; 493 | case 8: 494 | return function (pointer) { 495 | return this.fromWireType(HEAPF64[pointer >> 3]) 496 | }; 497 | default: 498 | throw new TypeError(`invalid float width (${width}): ${name}`) 499 | } 500 | } 501 | 502 | function __embind_register_float(rawType, name, size) { 503 | name = readLatin1String(name); 504 | registerType(rawType, { 505 | name, 506 | fromWireType: value => value, 507 | toWireType: (destructors, value) => value, 508 | argPackAdvance: GenericWireTypeSize, 509 | readValueFromPointer: floatReadValueFromPointer(name, size), 510 | destructorFunction: null 511 | }) 512 | } 513 | 514 | const createNamedFunction = (name, body) => Object.defineProperty(body, 'name', {value: name}); 515 | 516 | function runDestructors(destructors) { 517 | while (destructors.length) { 518 | const ptr = destructors.pop(); 519 | const del = destructors.pop(); 520 | del(ptr) 521 | } 522 | } 523 | 524 | function usesDestructorStack(argTypes) { 525 | for (let i = 1; i < argTypes.length; ++i) { 526 | if (argTypes[i] !== null && argTypes[i].destructorFunction === undefined) { 527 | return true 528 | } 529 | } 530 | return false 531 | } 532 | 533 | function newFunc(constructor, argumentList) { 534 | if (!(typeof constructor === 'function')) { 535 | throw new TypeError(`new_ called with constructor type ${typeof constructor} which is not a function`) 536 | } 537 | const dummy = createNamedFunction(constructor.name || 'unknownFunctionName', function () { 538 | }); 539 | dummy.prototype = constructor.prototype; 540 | const obj = new dummy(); 541 | const r = constructor.apply(obj, argumentList); 542 | return r instanceof Object ? r : obj 543 | } 544 | 545 | function createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync) { 546 | const needsDestructorStack = usesDestructorStack(argTypes); 547 | const argCount = argTypes.length; 548 | let argsList = ''; 549 | let argsListWired = ''; 550 | for (var i = 0; i < argCount - 2; ++i) { 551 | argsList += `${i !== 0 ? ', ' : ''}arg${i}`; 552 | argsListWired += `${i !== 0 ? ', ' : ''}arg${i}Wired` 553 | } 554 | let invokerFnBody = `\n return function (${argsList}) {\n if (arguments.length !== ${argCount - 2}) {\n throwBindingError('function ' + humanName + ' called with ' + arguments.length + ' arguments, expected ${argCount - 2}');\n }`; 555 | if (needsDestructorStack) { 556 | invokerFnBody += 'var destructors = [];\n' 557 | } 558 | const dtorStack = needsDestructorStack ? 'destructors' : 'null'; 559 | const args1 = ['humanName', 'throwBindingError', 'invoker', 'fn', 'runDestructors', 'retType', 'classParam']; 560 | if (isClassMethodFunc) { 561 | invokerFnBody += `var thisWired = classParam['toWireType'](${dtorStack}, this);\n` 562 | } 563 | for (var i = 0; i < argCount - 2; ++i) { 564 | invokerFnBody += `var arg${i}Wired = argType${i}['toWireType'](${dtorStack}, arg${i});\n`; 565 | args1.push(`argType${i}`) 566 | } 567 | if (isClassMethodFunc) { 568 | argsListWired = `thisWired${argsListWired.length > 0 ? ', ' : ''}${argsListWired}` 569 | } 570 | invokerFnBody += `${returns || isAsync ? 'var rv = ' : ''}invoker(fn${argsListWired.length > 0 ? ', ' : ''}${argsListWired});\n`; 571 | if (needsDestructorStack) { 572 | invokerFnBody += 'runDestructors(destructors);\n' 573 | } else { 574 | for (var i = isClassMethodFunc ? 1 : 2; i < argTypes.length; ++i) { 575 | const paramName = i === 1 ? 'thisWired' : `arg${i - 2}Wired`; 576 | if (argTypes[i].destructorFunction !== null) { 577 | invokerFnBody += `${paramName}_dtor(${paramName});\n`; 578 | args1.push(`${paramName}_dtor`) 579 | } 580 | } 581 | } 582 | if (returns) { 583 | invokerFnBody += 'var ret = retType[\'fromWireType\'](rv);\n' + 'return ret;\n' 584 | } else { 585 | } 586 | invokerFnBody += '}\n'; 587 | return [args1, invokerFnBody] 588 | } 589 | 590 | function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc, isAsync) { 591 | const argCount = argTypes.length; 592 | if (argCount < 2) { 593 | throwBindingError('argTypes array size mismatch! Must at least get return value and \'this\' types!') 594 | } 595 | const isClassMethodFunc = argTypes[1] !== null && classType !== null; 596 | const needsDestructorStack = usesDestructorStack(argTypes); 597 | const returns = argTypes[0].name !== 'void'; 598 | const closureArgs = [humanName, throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]]; 599 | for (var i = 0; i < argCount - 2; ++i) { 600 | closureArgs.push(argTypes[i + 2]) 601 | } 602 | if (!needsDestructorStack) { 603 | for (var i = isClassMethodFunc ? 1 : 2; i < argTypes.length; ++i) { 604 | if (argTypes[i].destructorFunction !== null) { 605 | closureArgs.push(argTypes[i].destructorFunction) 606 | } 607 | } 608 | } 609 | const [args, invokerFnBody] = createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync); 610 | args.push(invokerFnBody); 611 | const invokerFn = newFunc(Function, args)(...closureArgs); 612 | return createNamedFunction(humanName, invokerFn) 613 | } 614 | 615 | function ensureOverloadTable(proto, methodName, humanName) { 616 | if (undefined === proto[methodName].overloadTable) { 617 | const prevFunc = proto[methodName]; 618 | proto[methodName] = function (...args) { 619 | if (!proto[methodName].overloadTable.hasOwnProperty(args.length)) { 620 | throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`) 621 | } 622 | return proto[methodName].overloadTable[args.length].apply(this, args) 623 | }; 624 | proto[methodName].overloadTable = []; 625 | proto[methodName].overloadTable[prevFunc.argCount] = prevFunc 626 | } 627 | } 628 | 629 | function exposePublicSymbol(name, value, numArguments) { 630 | if (Module.hasOwnProperty(name)) { 631 | if (undefined === numArguments || undefined !== Module[name].overloadTable && undefined !== Module[name].overloadTable[numArguments]) { 632 | throwBindingError(`Cannot register public name '${name}' twice`) 633 | } 634 | ensureOverloadTable(Module, name, name); 635 | if (Module.hasOwnProperty(numArguments)) { 636 | throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`) 637 | } 638 | Module[name].overloadTable[numArguments] = value 639 | } else { 640 | Module[name] = value; 641 | if (undefined !== numArguments) { 642 | Module[name].numArguments = numArguments 643 | } 644 | } 645 | } 646 | 647 | function heap32VectorToArray(count, firstElement) { 648 | const array = []; 649 | for (let i = 0; i < count; i++) { 650 | array.push(HEAPU32[firstElement + i * 4 >> 2]) 651 | } 652 | return array 653 | } 654 | 655 | function replacePublicSymbol(name, value, numArguments) { 656 | if (!Module.hasOwnProperty(name)) { 657 | throwInternalError('Replacing nonexistent public symbol') 658 | } 659 | if (undefined !== Module[name].overloadTable && undefined !== numArguments) { 660 | Module[name].overloadTable[numArguments] = value 661 | } else { 662 | Module[name] = value; 663 | Module[name].argCount = numArguments 664 | } 665 | } 666 | 667 | function dynCallLegacy(sig, ptr, args) { 668 | sig = sig.replace(/p/g, 'i'); 669 | const f = Module[`dynCall_${sig}`]; 670 | return f(ptr, ...args) 671 | } 672 | 673 | const wasmTableMirror = []; 674 | let wasmTable; 675 | 676 | function getWasmTableEntry(funcPtr) { 677 | let func = wasmTableMirror[funcPtr]; 678 | if (!func) { 679 | if (funcPtr >= wasmTableMirror.length) 680 | wasmTableMirror.length = funcPtr + 1; 681 | wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr) 682 | } 683 | return func 684 | } 685 | 686 | function dynCall(sig, ptr, args = []) { 687 | if (sig.includes('j')) { 688 | return dynCallLegacy(sig, ptr, args) 689 | } 690 | const rtn = getWasmTableEntry(ptr)(...args); 691 | return rtn 692 | } 693 | 694 | const getDynCaller = (sig, ptr) => (...args) => dynCall(sig, ptr, args); 695 | 696 | function embind__requireFunction(signature, rawFunction) { 697 | signature = readLatin1String(signature); 698 | 699 | function makeDynCaller() { 700 | if (signature.includes('j')) { 701 | return getDynCaller(signature, rawFunction) 702 | } 703 | return getWasmTableEntry(rawFunction) 704 | } 705 | 706 | const fp = makeDynCaller(); 707 | if (typeof fp != 'function') { 708 | throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`) 709 | } 710 | return fp 711 | } 712 | 713 | function extendError(baseErrorType, errorName) { 714 | const errorClass = createNamedFunction(errorName, function (message) { 715 | this.name = errorName; 716 | this.message = message; 717 | const stack = new Error(message).stack; 718 | if (stack !== undefined) { 719 | this.stack = `${this.toString()}\n${stack.replace(/^Error(:[^\n]*)?\n/, '')}` 720 | } 721 | }); 722 | errorClass.prototype = Object.create(baseErrorType.prototype); 723 | errorClass.prototype.constructor = errorClass; 724 | errorClass.prototype.toString = function () { 725 | if (this.message === undefined) { 726 | return this.name 727 | } else { 728 | return `${this.name}: ${this.message}` 729 | } 730 | }; 731 | return errorClass 732 | } 733 | 734 | let UnboundTypeError; 735 | 736 | function getTypeName(type) { 737 | const ptr = ___getTypeName(type); 738 | const rv = readLatin1String(ptr); 739 | _free(ptr); 740 | return rv 741 | } 742 | 743 | function throwUnboundTypeError(message, types) { 744 | const unboundTypes = []; 745 | const seen = {}; 746 | 747 | function visit(type) { 748 | if (seen[type]) { 749 | return 750 | } 751 | if (registeredTypes[type]) { 752 | return 753 | } 754 | if (typeDependencies[type]) { 755 | typeDependencies[type].forEach(visit); 756 | return 757 | } 758 | unboundTypes.push(type); 759 | seen[type] = true 760 | } 761 | 762 | types.forEach(visit); 763 | throw new UnboundTypeError(`${message}: ${unboundTypes.map(getTypeName).join([', '])}`) 764 | } 765 | 766 | function getFunctionName(signature) { 767 | signature = signature.trim(); 768 | const argsIndex = signature.indexOf('('); 769 | if (argsIndex !== -1) { 770 | return signature.substr(0, argsIndex) 771 | } else { 772 | return signature 773 | } 774 | } 775 | 776 | function __embind_register_function(name, argCount, rawArgTypesAddr, signature, rawInvoker, fn, isAsync) { 777 | const argTypes = heap32VectorToArray(argCount, rawArgTypesAddr); 778 | name = readLatin1String(name); 779 | name = getFunctionName(name); 780 | rawInvoker = embind__requireFunction(signature, rawInvoker); 781 | exposePublicSymbol(name, () => { 782 | throwUnboundTypeError(`Cannot call ${name} due to unbound types`, argTypes) 783 | }, argCount - 1); 784 | whenDependentTypesAreResolved([], argTypes, (argTypes) => { 785 | const invokerArgsArray = [argTypes[0], null].concat(argTypes.slice(1)); 786 | replacePublicSymbol(name, craftInvokerFunction(name, invokerArgsArray, null, rawInvoker, fn, isAsync), argCount - 1); 787 | return [] 788 | }) 789 | } 790 | 791 | function integerReadValueFromPointer(name, width, signed) { 792 | switch (width) { 793 | case 1: 794 | return signed ? pointer => HEAP8[pointer] : pointer => HEAPU8[pointer]; 795 | case 2: 796 | return signed ? pointer => HEAP16[pointer >> 1] : pointer => HEAPU16[pointer >> 1]; 797 | case 4: 798 | return signed ? pointer => HEAP32[pointer >> 2] : pointer => HEAPU32[pointer >> 2]; 799 | default: 800 | throw new TypeError(`invalid integer width (${width}): ${name}`) 801 | } 802 | } 803 | 804 | function __embind_register_integer(primitiveType, name, size, minRange, maxRange) { 805 | name = readLatin1String(name); 806 | if (maxRange === -1) { 807 | maxRange = 4294967295 808 | } 809 | let fromWireType = value => value; 810 | if (minRange === 0) { 811 | const bitshift = 32 - 8 * size; 812 | fromWireType = value => value<>>bitshift; 813 | } 814 | const isUnsignedType = name.includes('unsigned'); 815 | const checkAssertions = (value, toTypeName) => { 816 | }; 817 | let toWireType; 818 | if (isUnsignedType) { 819 | toWireType = function (destructors, value) { 820 | checkAssertions(value, this.name); 821 | return value >>> 0 822 | } 823 | } else { 824 | toWireType = function (destructors, value) { 825 | checkAssertions(value, this.name); 826 | return value 827 | } 828 | } 829 | registerType(primitiveType, { 830 | name, 831 | fromWireType, 832 | toWireType, 833 | argPackAdvance: GenericWireTypeSize, 834 | readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0), 835 | destructorFunction: null 836 | }) 837 | } 838 | 839 | function __embind_register_memory_view(rawType, dataTypeIndex, name) { 840 | const typeMapping = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; 841 | const TA = typeMapping[dataTypeIndex]; 842 | 843 | function decodeMemoryView(handle) { 844 | const size = HEAPU32[handle >> 2]; 845 | const data = HEAPU32[handle + 4 >> 2]; 846 | return new TA(HEAP8.buffer, data, size) 847 | } 848 | 849 | name = readLatin1String(name); 850 | registerType(rawType, { 851 | name, 852 | fromWireType: decodeMemoryView, 853 | argPackAdvance: GenericWireTypeSize, 854 | readValueFromPointer: decodeMemoryView 855 | }, {ignoreDuplicateRegistrations: true}) 856 | } 857 | 858 | function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) { 859 | if (!(maxBytesToWrite > 0)) 860 | return 0; 861 | const startIdx = outIdx; 862 | const endIdx = outIdx + maxBytesToWrite - 1; 863 | for (let i = 0; i < str.length; ++i) { 864 | let u = str.charCodeAt(i); 865 | if (u >= 55296 && u <= 57343) { 866 | const u1 = str.charCodeAt(++i); 867 | u = 65536 + ((u & 1023) << 10) | u1 & 1023 868 | } 869 | if (u <= 127) { 870 | if (outIdx >= endIdx) 871 | break; 872 | heap[outIdx++] = u 873 | } else if (u <= 2047) { 874 | if (outIdx + 1 >= endIdx) 875 | break; 876 | heap[outIdx++] = 192 | u >> 6; 877 | heap[outIdx++] = 128 | u & 63 878 | } else if (u <= 65535) { 879 | if (outIdx + 2 >= endIdx) 880 | break; 881 | heap[outIdx++] = 224 | u >> 12; 882 | heap[outIdx++] = 128 | u >> 6 & 63; 883 | heap[outIdx++] = 128 | u & 63 884 | } else { 885 | if (outIdx + 3 >= endIdx) 886 | break; 887 | heap[outIdx++] = 240 | u >> 18; 888 | heap[outIdx++] = 128 | u >> 12 & 63; 889 | heap[outIdx++] = 128 | u >> 6 & 63; 890 | heap[outIdx++] = 128 | u & 63 891 | } 892 | } 893 | heap[outIdx] = 0; 894 | return outIdx - startIdx 895 | } 896 | 897 | const stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); 898 | 899 | function lengthBytesUTF8(str) { 900 | let len = 0; 901 | for (let i = 0; i < str.length; ++i) { 902 | const c = str.charCodeAt(i); 903 | if (c <= 127) { 904 | len++ 905 | } else if (c <= 2047) { 906 | len += 2 907 | } else if (c >= 55296 && c <= 57343) { 908 | len += 4; 909 | ++i 910 | } else { 911 | len += 3 912 | } 913 | } 914 | return len 915 | } 916 | 917 | function __embind_register_std_string(rawType, name) { 918 | name = readLatin1String(name); 919 | const stdStringIsUTF8 = name === 'std::string'; 920 | registerType(rawType, { 921 | name, fromWireType(value) { 922 | const length = HEAPU32[value >> 2]; 923 | const payload = value + 4; 924 | let str; 925 | if (stdStringIsUTF8) { 926 | let decodeStartPtr = payload; 927 | for (var i = 0; i <= length; ++i) { 928 | const currentBytePtr = payload + i; 929 | if (i == length || HEAPU8[currentBytePtr] == 0) { 930 | const maxRead = currentBytePtr - decodeStartPtr; 931 | const stringSegment = UTF8ToString(decodeStartPtr, maxRead); 932 | if (str === undefined) { 933 | str = stringSegment 934 | } else { 935 | str += String.fromCharCode(0); 936 | str += stringSegment 937 | } 938 | decodeStartPtr = currentBytePtr + 1 939 | } 940 | } 941 | } else { 942 | const a = new Array(length); 943 | for (var i = 0; i < length; ++i) { 944 | a[i] = String.fromCharCode(HEAPU8[payload + i]) 945 | } 946 | str = a.join('') 947 | } 948 | _free(value); 949 | return str 950 | }, toWireType(destructors, value) { 951 | if (value instanceof ArrayBuffer) { 952 | value = new Uint8Array(value) 953 | } 954 | let length; 955 | const valueIsOfTypeString = typeof value == 'string'; 956 | if (!(valueIsOfTypeString || value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Int8Array)) { 957 | throwBindingError('Cannot pass non-string to std::string') 958 | } 959 | if (stdStringIsUTF8 && valueIsOfTypeString) { 960 | length = lengthBytesUTF8(value) 961 | } else { 962 | length = value.length 963 | } 964 | const base = _malloc(4 + length + 1); 965 | const ptr = base + 4; 966 | HEAPU32[base >> 2] = length; 967 | if (stdStringIsUTF8 && valueIsOfTypeString) { 968 | stringToUTF8(value, ptr, length + 1) 969 | } else { 970 | if (valueIsOfTypeString) { 971 | for (var i = 0; i < length; ++i) { 972 | const charCode = value.charCodeAt(i); 973 | if (charCode > 255) { 974 | _free(ptr); 975 | throwBindingError('String has UTF-16 code units that do not fit in 8 bits') 976 | } 977 | HEAPU8[ptr + i] = charCode 978 | } 979 | } else { 980 | for (var i = 0; i < length; ++i) { 981 | HEAPU8[ptr + i] = value[i] 982 | } 983 | } 984 | } 985 | if (destructors !== null) { 986 | destructors.push(_free, base) 987 | } 988 | return base 989 | }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction(ptr) { 990 | _free(ptr) 991 | } 992 | }) 993 | } 994 | 995 | const UTF16Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder('utf-16le') : undefined; 996 | 997 | function UTF16ToString(ptr, maxBytesToRead) { 998 | let endPtr = ptr; 999 | let idx = endPtr >> 1; 1000 | const maxIdx = idx + maxBytesToRead / 2; 1001 | while (!(idx >= maxIdx) && HEAPU16[idx]) ++idx; 1002 | endPtr = idx << 1; 1003 | if (endPtr - ptr > 32 && UTF16Decoder) 1004 | return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); 1005 | let str = ''; 1006 | for (let i = 0; !(i >= maxBytesToRead / 2); ++i) { 1007 | const codeUnit = HEAP16[ptr + i * 2 >> 1]; 1008 | if (codeUnit == 0) 1009 | break; 1010 | str += String.fromCharCode(codeUnit) 1011 | } 1012 | return str 1013 | } 1014 | 1015 | function stringToUTF16(str, outPtr, maxBytesToWrite) { 1016 | maxBytesToWrite ??= 2147483647; 1017 | if (maxBytesToWrite < 2) 1018 | return 0; 1019 | maxBytesToWrite -= 2; 1020 | const startPtr = outPtr; 1021 | const numCharsToWrite = maxBytesToWrite < str.length * 2 ? maxBytesToWrite / 2 : str.length; 1022 | for (let i = 0; i < numCharsToWrite; ++i) { 1023 | const codeUnit = str.charCodeAt(i); 1024 | HEAP16[outPtr >> 1] = codeUnit; 1025 | outPtr += 2 1026 | } 1027 | HEAP16[outPtr >> 1] = 0; 1028 | return outPtr - startPtr 1029 | } 1030 | 1031 | const lengthBytesUTF16 = str => str.length * 2; 1032 | 1033 | function UTF32ToString(ptr, maxBytesToRead) { 1034 | let i = 0; 1035 | let str = ''; 1036 | while (!(i >= maxBytesToRead / 4)) { 1037 | const utf32 = HEAP32[ptr + i * 4 >> 2]; 1038 | if (utf32 == 0) 1039 | break; 1040 | ++i; 1041 | if (utf32 >= 65536) { 1042 | const ch = utf32 - 65536; 1043 | str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023) 1044 | } else { 1045 | str += String.fromCharCode(utf32) 1046 | } 1047 | } 1048 | return str 1049 | } 1050 | 1051 | function stringToUTF32(str, outPtr, maxBytesToWrite) { 1052 | maxBytesToWrite ??= 2147483647; 1053 | if (maxBytesToWrite < 4) 1054 | return 0; 1055 | const startPtr = outPtr; 1056 | const endPtr = startPtr + maxBytesToWrite - 4; 1057 | for (let i = 0; i < str.length; ++i) { 1058 | let codeUnit = str.charCodeAt(i); 1059 | if (codeUnit >= 55296 && codeUnit <= 57343) { 1060 | const trailSurrogate = str.charCodeAt(++i); 1061 | codeUnit = 65536 + ((codeUnit & 1023) << 10) | trailSurrogate & 1023 1062 | } 1063 | HEAP32[outPtr >> 2] = codeUnit; 1064 | outPtr += 4; 1065 | if (outPtr + 4 > endPtr) 1066 | break 1067 | } 1068 | HEAP32[outPtr >> 2] = 0; 1069 | return outPtr - startPtr 1070 | } 1071 | 1072 | function lengthBytesUTF32(str) { 1073 | let len = 0; 1074 | for (let i = 0; i < str.length; ++i) { 1075 | const codeUnit = str.charCodeAt(i); 1076 | if (codeUnit >= 55296 && codeUnit <= 57343) 1077 | ++i; 1078 | len += 4 1079 | } 1080 | return len 1081 | } 1082 | 1083 | function __embind_register_std_wstring(rawType, charSize, name) { 1084 | name = readLatin1String(name); 1085 | let decodeString, encodeString, readCharAt, lengthBytesUTF; 1086 | if (charSize === 2) { 1087 | decodeString = UTF16ToString; 1088 | encodeString = stringToUTF16; 1089 | lengthBytesUTF = lengthBytesUTF16; 1090 | readCharAt = pointer => HEAPU16[pointer >> 1] 1091 | } else if (charSize === 4) { 1092 | decodeString = UTF32ToString; 1093 | encodeString = stringToUTF32; 1094 | lengthBytesUTF = lengthBytesUTF32; 1095 | readCharAt = pointer => HEAPU32[pointer >> 2] 1096 | } 1097 | registerType(rawType, { 1098 | name, fromWireType: (value) => { 1099 | const length = HEAPU32[value >> 2]; 1100 | let str; 1101 | let decodeStartPtr = value + 4; 1102 | for (let i = 0; i <= length; ++i) { 1103 | const currentBytePtr = value + 4 + i * charSize; 1104 | if (i == length || readCharAt(currentBytePtr) == 0) { 1105 | const maxReadBytes = currentBytePtr - decodeStartPtr; 1106 | const stringSegment = decodeString(decodeStartPtr, maxReadBytes); 1107 | if (str === undefined) { 1108 | str = stringSegment 1109 | } else { 1110 | str += String.fromCharCode(0); 1111 | str += stringSegment 1112 | } 1113 | decodeStartPtr = currentBytePtr + charSize 1114 | } 1115 | } 1116 | _free(value); 1117 | return str 1118 | }, toWireType: (destructors, value) => { 1119 | if (!(typeof value == 'string')) { 1120 | throwBindingError(`Cannot pass non-string to C++ string type ${name}`) 1121 | } 1122 | const length = lengthBytesUTF(value); 1123 | const ptr = _malloc(4 + length + charSize); 1124 | HEAPU32[ptr >> 2] = length / charSize; 1125 | encodeString(value, ptr + 4, length + charSize); 1126 | if (destructors !== null) { 1127 | destructors.push(_free, ptr) 1128 | } 1129 | return ptr 1130 | }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction(ptr) { 1131 | _free(ptr) 1132 | } 1133 | }) 1134 | } 1135 | 1136 | function __embind_register_void(rawType, name) { 1137 | name = readLatin1String(name); 1138 | registerType(rawType, { 1139 | isVoid: true, 1140 | name, 1141 | argPackAdvance: 0, 1142 | fromWireType: () => undefined, 1143 | toWireType: (destructors, o) => undefined 1144 | }) 1145 | } 1146 | 1147 | const __emscripten_memcpy_js = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num); 1148 | const getHeapMax = () => 2147483648; 1149 | 1150 | function growMemory(size) { 1151 | const b = wasmMemory.buffer; 1152 | const pages = (size - b.byteLength + 65535) / 65536; 1153 | try { 1154 | wasmMemory.grow(pages); 1155 | updateMemoryViews(); 1156 | return 1 1157 | } catch (e) { 1158 | } 1159 | } 1160 | 1161 | function _emscripten_resize_heap(requestedSize) { 1162 | const oldSize = HEAPU8.length; 1163 | requestedSize >>>= 0; 1164 | const maxHeapSize = getHeapMax(); 1165 | if (requestedSize > maxHeapSize) { 1166 | return false 1167 | } 1168 | const alignUp = (x, multiple) => x + (multiple - x % multiple) % multiple; 1169 | for (let cutDown = 1; cutDown <= 4; cutDown *= 2) { 1170 | let overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); 1171 | overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); 1172 | const newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536)); 1173 | const replacement = growMemory(newSize); 1174 | if (replacement) { 1175 | return true 1176 | } 1177 | } 1178 | return false 1179 | } 1180 | 1181 | const _fd_close = fd => 52; 1182 | const convertI32PairToI53Checked = (lo, hi) => hi + 2097152 >>> 0 < 4194305 - !!lo ? (lo >>> 0) + hi * 4294967296 : Number.NaN; 1183 | 1184 | function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { 1185 | const offset = convertI32PairToI53Checked(offset_low, offset_high); 1186 | return 70 1187 | } 1188 | 1189 | const printCharBuffers = [null, [], []]; 1190 | 1191 | function printChar(stream, curr) { 1192 | const buffer = printCharBuffers[stream]; 1193 | if (curr === 0 || curr === 10) { 1194 | (stream === 1 ? out : err)(UTF8ArrayToString(buffer, 0)); 1195 | buffer.length = 0 1196 | } else { 1197 | buffer.push(curr) 1198 | } 1199 | } 1200 | 1201 | function _fd_write(fd, iov, iovcnt, pnum) { 1202 | let num = 0; 1203 | for (let i = 0; i < iovcnt; i++) { 1204 | const ptr = HEAPU32[iov >> 2]; 1205 | const len = HEAPU32[iov + 4 >> 2]; 1206 | iov += 8; 1207 | for (let j = 0; j < len; j++) { 1208 | printChar(fd, HEAPU8[ptr + j]) 1209 | } 1210 | num += len 1211 | } 1212 | HEAPU32[pnum >> 2] = num; 1213 | return 0 1214 | } 1215 | 1216 | function writeArrayToMemory(array, buffer) { 1217 | HEAP8.set(array, buffer) 1218 | } 1219 | 1220 | embind_init_charCodes(); 1221 | BindingError = Module.BindingError = class BindingError extends Error { 1222 | constructor(message) { 1223 | super(message); 1224 | this.name = 'BindingError' 1225 | } 1226 | }; 1227 | InternalError = Module.InternalError = class InternalError extends Error { 1228 | constructor(message) { 1229 | super(message); 1230 | this.name = 'InternalError' 1231 | } 1232 | }; 1233 | init_emval(); 1234 | UnboundTypeError = Module.UnboundTypeError = extendError(Error, 'UnboundTypeError'); 1235 | var wasmImports = { 1236 | __assert_fail: ___assert_fail, 1237 | __cxa_throw: ___cxa_throw, 1238 | _abort_js: __abort_js, 1239 | _embind_register_bigint: __embind_register_bigint, 1240 | _embind_register_bool: __embind_register_bool, 1241 | _embind_register_emval: __embind_register_emval, 1242 | _embind_register_float: __embind_register_float, 1243 | _embind_register_function: __embind_register_function, 1244 | _embind_register_integer: __embind_register_integer, 1245 | _embind_register_memory_view: __embind_register_memory_view, 1246 | _embind_register_std_string: __embind_register_std_string, 1247 | _embind_register_std_wstring: __embind_register_std_wstring, 1248 | _embind_register_void: __embind_register_void, 1249 | _emscripten_memcpy_js: __emscripten_memcpy_js, 1250 | emscripten_resize_heap: _emscripten_resize_heap, 1251 | fd_close: _fd_close, 1252 | fd_seek: _fd_seek, 1253 | fd_write: _fd_write 1254 | }; 1255 | 1256 | var wasmExports; 1257 | var ___wasm_call_ctors; 1258 | var ___getTypeName; 1259 | var _free; 1260 | var _malloc; 1261 | var __emscripten_stack_restore; 1262 | var __emscripten_stack_alloc; 1263 | var _emscripten_stack_get_current; 1264 | var ___cxa_is_pointer_type; 1265 | var dynCall_jiji; 1266 | 1267 | Module.getValue = getValue; 1268 | Module.writeArrayToMemory = writeArrayToMemory; 1269 | 1270 | let calledRun; 1271 | 1272 | async function run() { 1273 | wasmExports = await createWasm(); 1274 | ___wasm_call_ctors = wasmExports.__wasm_call_ctors; 1275 | ___getTypeName = wasmExports.__getTypeName; 1276 | _free = wasmExports.free; 1277 | _malloc = Module._malloc = wasmExports.malloc; 1278 | __emscripten_stack_restore = wasmExports._emscripten_stack_restore; 1279 | __emscripten_stack_alloc = wasmExports._emscripten_stack_alloc; 1280 | _emscripten_stack_get_current = wasmExports.emscripten_stack_get_current; 1281 | ___cxa_is_pointer_type = wasmExports.__cxa_is_pointer_type; 1282 | dynCall_jiji = Module.dynCall_jiji = wasmExports.dynCall_jiji; 1283 | if (runDependencies > 0) { 1284 | return 1285 | } 1286 | preRun(); 1287 | 1288 | if (runDependencies > 0) { 1289 | return 1290 | } 1291 | 1292 | function doRun() { 1293 | if (calledRun) 1294 | return; 1295 | calledRun = true; 1296 | Module.calledRun = true; 1297 | if (ABORT) 1298 | return; 1299 | initRuntime(); 1300 | postRun() 1301 | } 1302 | 1303 | doRun() 1304 | } 1305 | 1306 | Module.inspect = function() { 1307 | return '[Module]' 1308 | }; 1309 | 1310 | export async function initWasmBrowser(url) { 1311 | wasmUrl = url; 1312 | await run(); 1313 | } 1314 | 1315 | export function ttf2woff2(inputContent) { 1316 | // Prepare input 1317 | const inputBuffer = Module._malloc(inputContent.length + 1); 1318 | const outputSizePtr = Module._malloc(4); // eslint-disable-line 1319 | let outputBufferPtr; 1320 | let outputSize; 1321 | let outputContent; 1322 | 1323 | Module.writeArrayToMemory(inputContent, inputBuffer); 1324 | 1325 | // Run 1326 | outputBufferPtr = Module.convert( 1327 | inputBuffer, 1328 | inputContent.length, 1329 | outputSizePtr, 1330 | ); 1331 | 1332 | // Retrieve output 1333 | outputSize = Module.getValue(outputSizePtr, 'i32'); 1334 | outputContent = Buffer.alloc(outputSize); 1335 | 1336 | for (let i = 0; i < outputSize; i++) { 1337 | outputContent[i] = Module.getValue(outputBufferPtr + i, 'i8'); 1338 | } 1339 | 1340 | Module.freePtrs(outputBufferPtr, outputSizePtr); 1341 | 1342 | return outputContent; 1343 | } 1344 | -------------------------------------------------------------------------------- /test/esm.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { SvgPacker } from '../src' 3 | import { options } from './shared' 4 | 5 | it('svg-packer in Node', async () => { 6 | const result = await SvgPacker(options) 7 | expect(result.files).toBeTruthy() 8 | expect(Array.from(Object.values(result.files)).map(m => m.name)).toMatchInlineSnapshot(` 9 | [ 10 | "maf.svg", 11 | "maf.ttf", 12 | "maf.eot", 13 | "maf.woff", 14 | "maf.woff2", 15 | "maf.css", 16 | "_demo.html", 17 | ] 18 | `) 19 | const css = result.files.css 20 | expect(css).toBeTruthy() 21 | expect(css.blob).toBeTruthy() 22 | expect(css.blob.size).toBeGreaterThan(0) 23 | const cssContent = await css.blob.text() 24 | expect(cssContent).toMatchInlineSnapshot(` 25 | " 26 | @font-face { 27 | font-family: "My Awesome Font"; 28 | src: url("./maf.eot"); 29 | src: url("./maf.eot") format("embedded-opentype"), 30 | url("./maf.ttf") format("truetype"), 31 | url("./maf.woff") format("woff"), 32 | url("./maf.woff2") format("woff2"), 33 | url("./maf.svg") format("svg"); 34 | font-weight: normal; 35 | font-style: normal; 36 | } 37 | 38 | .i { 39 | font-family: "My Awesome Font" !important; 40 | font-size: 1em; 41 | font-style: normal; 42 | -webkit-font-smoothing: antialiased; 43 | -moz-osx-font-smoothing: grayscale; 44 | } 45 | 46 | 47 | .i.mdi\\:account-edit:before { 48 | content: "\\e001"; 49 | } 50 | 51 | .i.mdi\\:account-tie:before { 52 | content: "\\e002"; 53 | } 54 | 55 | " 56 | `) 57 | }) 58 | -------------------------------------------------------------------------------- /test/iife.browser.ts: -------------------------------------------------------------------------------- 1 | import type { SvgPackerResult } from '../src' 2 | import { server } from '@vitest/browser/context' 3 | import { isCI } from 'std-env' 4 | import { expect, it } from 'vitest' 5 | import { appendIconsToTheDomBody, expectedFontName, options, replaceCssFontUrls } from './shared' 6 | 7 | // this test shouldn't run when using the preview provider and running the test in the CI 8 | it.skipIf(isCI && server.provider === 'preview')('svg-packer in the browser', async () => { 9 | expect('SvgPacker' in globalThis).toBeTruthy() 10 | const result: SvgPackerResult = await globalThis.SvgPacker(options) 11 | expect(result.files).toBeTruthy() 12 | expect(Array.from(Object.values(result.files)).map(m => m.name)).toMatchInlineSnapshot(` 13 | [ 14 | "maf.svg", 15 | "maf.ttf", 16 | "maf.eot", 17 | "maf.woff", 18 | "maf.woff2", 19 | "maf.css", 20 | "_demo.html", 21 | ] 22 | `) 23 | const css = result.files.css 24 | expect(css).toBeTruthy() 25 | expect(css.blob).toBeTruthy() 26 | expect(css.blob.size).toBeGreaterThan(0) 27 | let cssContent = await css.blob.text() 28 | expect(cssContent).toMatchInlineSnapshot(` 29 | " 30 | @font-face { 31 | font-family: "My Awesome Font"; 32 | src: url("./maf.eot"); 33 | src: url("./maf.eot") format("embedded-opentype"), 34 | url("./maf.ttf") format("truetype"), 35 | url("./maf.woff") format("woff"), 36 | url("./maf.woff2") format("woff2"), 37 | url("./maf.svg") format("svg"); 38 | font-weight: normal; 39 | font-style: normal; 40 | } 41 | 42 | .i { 43 | font-family: "My Awesome Font" !important; 44 | font-size: 1em; 45 | font-style: normal; 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | } 49 | 50 | 51 | .i.mdi\\:account-edit:before { 52 | content: "\\e001"; 53 | } 54 | 55 | .i.mdi\\:account-tie:before { 56 | content: "\\e002"; 57 | } 58 | 59 | " 60 | `) 61 | cssContent = replaceCssFontUrls(cssContent, result) 62 | const style = globalThis.document.createElement('style') 63 | style.textContent = `${cssContent} 64 | i { 65 | padding: 5px; 66 | color: #717171; 67 | display: inline-block; 68 | font-size: 1.2em; 69 | } 70 | ` 71 | globalThis.document.head.append(style) 72 | await new Promise(resolve => setTimeout(resolve, 100)) 73 | const iconDeclarations = await appendIconsToTheDomBody() 74 | const fontName = expectedFontName(options) 75 | for (const { css } of iconDeclarations) { 76 | expect(css).toBeTruthy() 77 | expect(css.content).toBeTruthy() 78 | expect(css.fontFamily).toBe(fontName) 79 | expect(css.fontStyle).toBe('normal') 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /test/shared.ts: -------------------------------------------------------------------------------- 1 | import type { SvgPackerOptions, SvgPackerResult } from '../src' 2 | import type { FileExtension } from '../src/utils' 3 | import { FileExtensions } from '../src/utils' 4 | 5 | export const options = { 6 | fontName: 'My Awesome Font', 7 | fileName: 'maf', 8 | cssPrefix: 'i', 9 | icons: [ 10 | { 11 | svg: '', 12 | name: 'mdi:account-edit', 13 | }, 14 | { 15 | svg: '', 16 | name: 'mdi:account-tie', 17 | }, 18 | ], 19 | } satisfies SvgPackerOptions 20 | 21 | const fonts = FileExtensions.reduce((acc, ext) => { 22 | acc[ext] = new RegExp(`"./${options.fileName}.${ext}"`, 'g') 23 | return acc 24 | }, {} as Record) 25 | 26 | export function replaceCssFontUrls(css: string, result: SvgPackerResult): string { 27 | return css 28 | .replace(fonts.eot, result.files.eot.url) 29 | .replace(fonts.ttf, result.files.ttf.url) 30 | .replace(fonts.woff, result.files.woff.url) 31 | .replace(fonts.woff2, result.files.woff2.url) 32 | .replace(fonts.svg, result.files.svg.url) 33 | } 34 | 35 | export function expectedFontName({ fontName }: SvgPackerOptions) { 36 | return fontName ? (fontName[0] === '"' ? fontName : `"${fontName}"`) : undefined 37 | } 38 | 39 | export interface IconDeclaration { 40 | icon: string 41 | css: CSSStyleDeclaration 42 | } 43 | 44 | export async function appendIconsToTheDomBody(): Promise { 45 | const { icons, cssPrefix } = options 46 | const iconsDeclarations: IconDeclaration[] = [] 47 | for (const icon of icons) { 48 | const el = globalThis.document.createElement('i') 49 | el.className = `${cssPrefix} ${icon.name}` 50 | globalThis.document.body.append(el) 51 | await new Promise(resolve => setTimeout(resolve, 100)) 52 | const before = globalThis.getComputedStyle(el, ':before') 53 | iconsDeclarations.push({ 54 | icon: icon.name, 55 | css: before, 56 | }) 57 | } 58 | 59 | return iconsDeclarations 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext", "DOM"], 5 | "moduleDetection": "force", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "types": ["@vitest/browser/providers/playwright"], 9 | "allowJs": true, 10 | "noEmit": true, 11 | "esModuleInterop": false, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["src/wasm.js"] 16 | } 17 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import * as fsp from 'node:fs/promises' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { build } from 'esbuild' 5 | import stdLibBrowser from 'node-stdlib-browser' 6 | import plugin from 'node-stdlib-browser/helpers/esbuild/plugin' 7 | import { defineConfig } from 'tsdown/config' 8 | 9 | const root = fileURLToPath(new URL('.', import.meta.url)) 10 | 11 | export default defineConfig({ 12 | entry: ['src/index.ts', 'src/vite.ts'], 13 | platform: 'browser', 14 | format: 'esm', 15 | target: 'node16', 16 | external: ['sax'], 17 | dts: true, 18 | outExtensions() { 19 | return { 20 | js: '.mjs', 21 | dts: '.mts', 22 | } 23 | }, 24 | hooks: { 25 | 'build:done': async function () { 26 | await build({ 27 | entryPoints: [path.resolve(root, './dist/index.mjs')], 28 | outfile: path.resolve(root, './dist/index.browser.js'), 29 | bundle: true, 30 | format: 'iife', 31 | // beware of the globalName, it won't work since it will be exposed as SvgPacker.SvgPacker instead of SvgPacker 32 | globalName: 'SvgPacker', 33 | inject: [path.resolve('node_modules/node-stdlib-browser/helpers/esbuild/shim')], 34 | define: { 35 | global: 'global', 36 | process: 'process', 37 | Buffer: 'Buffer', 38 | }, 39 | plugins: [plugin(stdLibBrowser)], 40 | }) 41 | // will require the user to use SvgPacker.SvgPacker({...}) instead SvgPacker({...}) 42 | // this code will patch the export to use SvgPacker.SvgPacker 43 | await editFile(path.resolve(root, './dist/index.browser.js'), (content) => { 44 | return content.replace( 45 | 'return __toCommonJS(index_exports);', 46 | 'return __toCommonJS(index_exports).SvgPacker;', 47 | ) 48 | }) 49 | }, 50 | }, 51 | }) 52 | 53 | async function editFile(filePath: string, edit: (content: string) => string) { 54 | const content = await fsp.readFile(filePath, 'utf8') 55 | await fsp.writeFile(filePath, edit(content), 'utf8') 56 | } 57 | -------------------------------------------------------------------------------- /vitest.browser.config.ts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'std-env' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: './dist/index.browser.js', 7 | include: [ 8 | 'test/*.browser.ts', 9 | ], 10 | name: 'browser', 11 | browser: { 12 | enabled: true, 13 | headless: isCI, 14 | provider: 'playwright', 15 | instances: [ 16 | { browser: 'chromium' }, 17 | ], 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'std-env' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | workspace: [ 7 | { 8 | test: { 9 | include: [ 10 | 'test/*.test.ts', 11 | ], 12 | name: 'node', 13 | environment: 'node', 14 | }, 15 | }, 16 | // this test is about testing the iife with the preview provider 17 | // won't run in the CI, and the test will be skipped 18 | { 19 | test: { 20 | setupFiles: './dist/index.browser.js', 21 | include: [ 22 | 'test/*.browser.ts', 23 | ], 24 | name: 'browser', 25 | browser: { 26 | enabled: !isCI, 27 | provider: 'preview', 28 | instances: [ 29 | { browser: 'chromium' }, 30 | ], 31 | }, 32 | }, 33 | }, 34 | ], 35 | }, 36 | }) 37 | --------------------------------------------------------------------------------