Convert Figma project to Vite + React (#7453)

A Figma plugin is just a single javascript file which means no out of the box way to split files, import helper libraries and scale the project. It's also a vanilla web site for making the UI.

The PR updates the project to use Vite. It allows normal project spliting for typescript and css files. It provides a simpler way for the plugin and backend code to communicate. React is used to then build the plugin interface.
This commit is contained in:
Nigel Breslaw 2025-01-27 12:54:09 +02:00 committed by GitHub
parent c324a37a20
commit facd460037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 2163 additions and 1424 deletions

View file

@ -127,6 +127,7 @@ path = [
"tools/slintpad/styles/**.css",
"tools/figma-inspector/**.json",
"tools/figma-inspector/**.html",
"tools/figma-inspector/**.css",
]
precedence = "aggregate"
SPDX-FileCopyrightText = "Copyright © SixtyFPS GmbH <info@slint.dev>"

2763
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1 +1,4 @@
*.js
*.js
dist
.tmp
!*.d.ts

View file

@ -27,3 +27,123 @@ First install the dependencies with `pnpm i`.
Then build the project with `pnpm build`.
Then in Figma select Plugins > Plugins & Widgets > Import from manifest... and then chose the manifest.json from this folder.
## Sending Messages between the Frontend and Backend
Bolt Figma makes messaging between the frontend UI and backend code layers simple and type-safe. This can be done with `listenTS()` and `dispatchTS()`.
Using this method accounts for:
- Setting up a scoped event listener in the listening context
- Removing the listener when the event is called (if `once` is set to true)
- Ensuring End-to-End Type-Safety for the event
### 1. Declare the Event Type in EventTS in shared/universals.ts
```js
export type EventTS = {
myCustomEvent: {
oneValue: string,
anotherValue: number,
},
// [... other events]
};
```
### 2a. Send a Message from the Frontend to the Backend
**Backend Listener:** `src-code/code.ts`
```js
import { listenTS } from "./utils/code-utils";
listenTS("myCustomEvent", (data) => {
console.log("oneValue is", data.oneValue);
console.log("anotherValue is", data.anotherValue);
});
```
**Frontend Dispatcher:** `index.svelte` or `index.tsx` or `index.vue`
```js
import { dispatchTS } from "./utils/utils";
dispatchTS("myCustomEvent", { oneValue: "name", anotherValue: 20 });
```
### 2b. Send a Message from the Backend to the Frontend
**Frontend Listener:** `index.svelte` or `index.tsx` or `index.vue`
```js
import { listenTS } from "./utils/utils";
listenTS(
"myCustomEvent",
(data) => {
console.log("oneValue is", data.oneValue);
console.log("anotherValue is", data.anotherValue);
},
true,
);
```
_Note: `true` is passed as the 3rd argument which means the listener will only listen once and then be removed. Set this to true to avoid duplicate events if you only intend to recieve one reponse per function._
**Backend Dispatcher:** `src-code/code.ts`
```js
import { dispatchTS } from "./utils/code-utils";
dispatchTS("myCustomEvent", { oneValue: "name", anotherValue: 20 });
```
---
### Info on Build Process
Frontend code is built to the `.tmp` directory temporarily and then copied to the `dist` folder for final. This is done to avoid Figma throwing plugin errors with editing files directly in the `dist` folder.
The frontend code (JS, CSS, HTML) is bundled into a single `index.html` file and all assets are inlined.
The backend code is bundled into a single `code.js` file.
Finally the `manifest.json` is generated from the `figma.config.ts` file with type-safety. This is configured when running `yarn create bolt-figma`, but you can make additional modifications to the `figma.config.ts` file after initialization.
### Read if Dev or Production Mode
Use the built-in Vite env var MODE to determine this:
```js
const mode = import.meta.env.MODE; // 'dev' or 'production'
```
### Troubleshooting Assets
Figma requires the entire frontend code to be wrapped into a single HTML file. For this reason, bundling external images, svgs, and other assets is not possible.
The solution to this is to inline all assets. Vite is already setup to inline most asset types it understands such as JPG, PNG, SVG, and more, however if the file type you're trying to inline doesn't work, you may need to add it to the assetsInclude array in the vite config:
More Info: https://vitejs.dev/config/shared-options.html#assetsinclude
Additionally, you may be able to import the file as a raw string, and then use that data inline in your component using the `?raw` suffix.
For example:
```ts
import icon from "./assets/icon.svg?raw";
```
and then use that data inline in your component:
```js
// Svelte
{@html icon}
// React
<div dangerouslySetInnerHTML={{ __html: icon }}></div>
// Vue
<div v-html="icon"></div>
```

View file

@ -0,0 +1,28 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import {
getStore,
setStore,
listenTS,
dispatchTS,
getStatus,
updateUI,
} from "./utils/code-utils";
figma.showUI(__html__, {
themeColors: true,
width: 400,
height: 320,
});
listenTS("copyToClipboard", () => {
figma.notify("Copied!");
});
figma.on("selectionchange", () => {
updateUI();
});
// init
updateUI();

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"typeRoots": [
"../../../node_modules/@types",
"../../../node_modules/@figma",
"../src/globals.d.ts",
"../shared/universals.d.ts"
]
},
"include": ["./**/*.ts"]
}

View file

@ -1,7 +1,50 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
// SPDX-License-Identifier: MIT
function getStatus(selectionCount: number) {
import type { Message, PluginMessageEvent } from "../../src/globals";
import type { EventTS } from "../../shared/universals";
export const dispatch = (data: any, origin = "*") => {
figma.ui.postMessage(data, {
origin,
});
};
export const dispatchTS = <Key extends keyof EventTS>(
event: Key,
data: EventTS[Key],
origin = "*",
) => {
dispatch({ event, data }, origin);
};
export const listenTS = <Key extends keyof EventTS>(
eventName: Key,
callback: (data: EventTS[Key]) => any,
listenOnce = false,
) => {
const func = (event: any) => {
if (event.event === eventName) {
callback(event);
if (listenOnce) {
figma.ui?.off("message", func); // Remove Listener so we only listen once
}
}
};
figma.ui.on("message", func);
};
export const getStore = async (key: string) => {
const value = await figma.clientStorage.getAsync(key);
return value;
};
export const setStore = async (key: string, value: string) => {
await figma.clientStorage.setAsync(key, value);
};
export function getStatus(selectionCount: number) {
if (selectionCount === 0) {
return "Please select a layer";
}
@ -11,10 +54,6 @@ function getStatus(selectionCount: number) {
return "Slint properties:";
}
type StyleObject = {
[key: string]: string;
};
const itemsToKeep = [
"color",
"font-family",
@ -30,6 +69,10 @@ const itemsToKeep = [
"stroke",
];
type StyleObject = {
[key: string]: string;
};
function transformStyle(styleObj: StyleObject): string {
const filteredEntries = Object.entries(styleObj)
.filter(([key]) => itemsToKeep.includes(key))
@ -62,7 +105,7 @@ function transformStyle(styleObj: StyleObject): string {
return filteredEntries.length > 0 ? `${filteredEntries.join(";\n")};` : "";
}
async function updateUI() {
export async function updateUI() {
const title = getStatus(figma.currentPage.selection.length);
let slintProperties = "";
@ -70,25 +113,7 @@ async function updateUI() {
const cssProperties =
await figma.currentPage.selection[0].getCSSAsync();
slintProperties = transformStyle(cssProperties);
console.log(cssProperties);
}
figma.ui.postMessage({ title, slintProperties });
dispatchTS("updatePropertiesCallback", { title, slintProperties });
}
// This shows the HTML page in "ui.html".
figma.showUI(__html__, { width: 400, height: 320, themeColors: true });
// init
updateUI();
figma.on("selectionchange", () => {
updateUI();
});
// Logic to react to UI events
figma.ui.onmessage = async (msg: { type: string; count: number }) => {
if (msg.type === "copy") {
figma.notify("Copied to clipboard");
}
};

View file

@ -0,0 +1,29 @@
// Copyright © Hyper Brew LLC
// SPDX-License-Identifier: MIT
import type { FigmaConfig, PluginManifest } from "vite-figma-plugin/lib/types";
import { version } from "./package.json";
export const manifest: PluginManifest = {
name: "Figma to Slint",
id: "slint.figma.plugin",
api: "1.0.0",
main: "code.js",
ui: "index.html",
editorType: ["figma", "dev"],
documentAccess: "dynamic-page",
networkAccess: {
allowedDomains: ["*"],
reasoning: "For accessing remote assets",
},
};
const extraPrefs = {
copyZipAssets: ["public-zip/*"],
};
export const config: FigmaConfig = {
manifest,
version,
...extraPrefs,
};

View file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index-react.tsx"></script>
</body>
</html>

View file

@ -1,18 +0,0 @@
{
"name": "Figma to Slint",
"id": "1464256128189783424",
"api": "1.0.0",
"main": "code.js",
"capabilities": [],
"enableProposedApi": false,
"documentAccess": "dynamic-page",
"editorType": [
"figma"
],
"ui": "ui.html",
"networkAccess": {
"allowedDomains": [
"none"
]
}
}

View file

@ -1,47 +1,36 @@
{
"name": "Figma to Slint (Beta)",
"name": "slint.figma.plugin",
"private": true,
"version": "1.10.0",
"description": "Slint plugin for Figma",
"main": "code.js",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint --ext .ts,.tsx --ignore-pattern node_modules .",
"lint:fix": "eslint --ext .ts,.tsx --ignore-pattern node_modules --fix .",
"dev": "vite build --watch --mode dev",
"devcode": "vite build --watch --mode dev --config vite.config.code.ts",
"build": "vite build",
"buildcode": "vite build --config vite.config.code.ts",
"preview": "vite preview",
"hmr": "vite",
"zip": "cross-env MODE=zip vite build",
"check": "biome check",
"lint": "biome lint",
"lint:fix": "biome lint --fix",
"format": "biome format",
"format:fix": "biome format --write",
"watch": "pnpm build --watch"
"format:fix": "biome format --write"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0"
},
"author": "",
"license": "",
"devDependencies": {
"@figma/eslint-plugin-figma-plugins": "0.15.0",
"@figma/plugin-typings": "*",
"@typescript-eslint/eslint-plugin": "6.12.0",
"@typescript-eslint/parser": "6.12.0",
"eslint": "8.54.0",
"typescript": "5.3.2"
},
"eslintConfig": {
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@figma/figma-plugins/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"root": true,
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@vitejs/plugin-react": "4.3.4",
"@figma/plugin-typings": "1.106.0",
"@types/node": "20.16.10",
"cross-env": "7.0.3",
"typescript": "5.7.3",
"vite": "6.0.11",
"vite-figma-plugin": "0.0.24",
"vite-plugin-singlefile": "2.1.0"
}
}

View file

@ -0,0 +1,4 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: MIT
This is a sample readme that gets copied to the zip

View file

@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export interface EventTS {
updatePropertiesCallback: {
title: string;
slintProperties: string;
};
copyToClipboard: {
result: boolean;
};
}

View file

@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.6632 55.828L48.7333 37.0309C48.7333 37.0309 50 36.3278 50 35.2182C50 33.7406 48.3981 33.2599 48.3981 33.2599L32.9557 27.355C32.4047 27.1462 31.6464 27.7312 32.3564 28.4728L37.4689 33.4165C37.4689 33.4165 38.889 34.765 38.889 35.6494C38.889 36.5338 38.017 37.322 38.017 37.322L19.4135 54.6909C18.7517 55.3089 19.6464 56.4294 20.6632 55.828Z" fill="#2379F4"/>
<path d="M43.3368 8.17339L15.2667 26.9677C15.2667 26.9677 14 27.6708 14 28.7804C14 30.258 15.6019 30.7387 15.6019 30.7387L31.0443 36.6464C31.5953 36.8524 32.3565 36.2674 31.6436 35.5286L26.5311 30.5684C26.5311 30.5684 25.111 29.2226 25.111 28.3355C25.111 27.4483 25.983 26.6628 25.983 26.6628L44.5752 9.30769C45.2483 8.68973 44.3565 7.56916 43.3368 8.17339Z" fill="#2379F4"/>
</svg>

After

Width:  |  Height:  |  Size: 850 B

18
tools/figma-inspector/src/globals.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export type Message = {
event: string;
data?: any;
callback?: string;
};
export interface PluginMessageEvent {
pluginMessage: Message;
pluginId?: string;
}
declare module "*.png";
declare module "*.gif";
declare module "*.jpg";
declare module "*.svg";

View file

@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./main";
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

@ -0,0 +1,47 @@
body {
margin: 0;
padding: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
}
.container {
width: 100%;
height: 100%;
background-color: var(--figma-color-bg-secondary);
border-radius: 4px;
overflow: hidden;
}
.title {
height: 30px;
font-size: 12px;
color: var(--figma-color-text-secondary);
border-bottom: 1px solid var(--figma-color-border);
display: flex;
align-items: center;
padding-left: 16px;
padding-right: 16px;
justify-content: space-between;
user-select: none;
}
.copy-icon {
cursor: pointer;
font-size: 14px;
}
.copy-icon:hover {
opacity: 0.7;
}
.content {
padding: 16px;
font-size: 12px;
color: var(--figma-color-text-secondary);
white-space: pre-wrap;
}
.hidden {
display: none;
}

View file

@ -0,0 +1,40 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import React, { useEffect, useState } from "react";
import { listenTS } from "./utils/bolt-utils";
import "./main.css";
import { copyToClipboard } from "./utils/utils.js";
export const App = () => {
const [title, setTitle] = useState("");
const [slintProperties, setSlintProperties] = useState("");
listenTS(
"updatePropertiesCallback",
(res) => {
setTitle(res.title);
setSlintProperties(res.slintProperties);
},
true,
);
return (
<div className="container">
<div className="title">
{title}
{slintProperties !== "" && (
<span
id="copy-icon"
onClick={() => copyToClipboard(slintProperties)}
onKeyDown={() => copyToClipboard(slintProperties)}
className="copy-icon"
>
📋
</span>
)}
</div>
<div className="content">{slintProperties}</div>
</div>
);
};

View file

@ -0,0 +1,69 @@
// Copyright © Hyper Brew LLC
// SPDX-License-Identifier: MIT
import { manifest } from "../../figma.config";
import type { Message, PluginMessageEvent } from "../globals";
import type { EventTS } from "../../shared/universals";
export const dispatch = (msg: Message, global = false, origin = "*") => {
const data: PluginMessageEvent = { pluginMessage: msg };
if (!global) {
data.pluginId = manifest.id;
}
parent.postMessage(data, origin);
};
export const dispatchTS = <Key extends keyof EventTS>(
event: Key,
data: EventTS[Key],
global = false,
origin = "*",
) => {
dispatch({ event, ...data }, global, origin);
};
export const listenTS = <Key extends keyof EventTS>(
eventName: Key,
callback: (data: EventTS[Key]) => any,
listenOnce = false,
) => {
const func = (event: MessageEvent<any>) => {
if (event.data.pluginMessage.event === eventName) {
callback(event.data.pluginMessage.data);
if (listenOnce) {
window.removeEventListener("message", func); // Remove Listener so we only listen once
}
}
};
window.addEventListener("message", func);
};
export const getColorTheme = () => {
if (window?.matchMedia) {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
return "light";
}
}
return "light";
};
export const subscribeColorTheme = (
callback: (mode: "light" | "dark") => void,
) => {
if (window?.matchMedia) {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", ({ matches }) => {
if (matches) {
console.log("change to dark mode!");
callback("dark");
} else {
console.log("change to light mode!");
callback("light");
}
});
}
};

View file

@ -0,0 +1,35 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { dispatchTS } from "./bolt-utils.js";
export function writeTextToClipboard(str: string) {
const prevActive = document.activeElement;
const textArea = document.createElement("textarea");
textArea.value = str;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return new Promise<void>((res, rej) => {
document.execCommand("copy") ? res() : rej();
textArea.remove();
if (prevActive && prevActive instanceof HTMLElement) {
prevActive.focus();
}
});
}
export function copyToClipboard(slintProperties: string) {
writeTextToClipboard(slintProperties);
dispatchTS("copyToClipboard", {
result: true,
});
}

View file

@ -0,0 +1,6 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
/// <reference types="svelte" />
/// <reference types="vite/client" />
/// <reference types="figma" />

View file

@ -1,11 +1,35 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es2017"],
"useDefineForClassFields": true,
"module": "es6",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"jsx": "react-jsx",
"checkJs": true,
"isolatedModules": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@figma"
"../../node_modules/@types",
"../../node_modules/@figma",
"./src/globals.d.ts",
"shared/universals.d.ts"
]
}
},
"include": [
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/*.js",
"src/**/*.svelte",
"src-code/**/*.ts",
"shared/universals.d.ts"
]
}

View file

@ -1,108 +0,0 @@
<style>
body {
margin: 0;
padding: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.container {
width: 100%;
height: 100%;
background-color: var(--figma-color-bg-secondary);
border-radius: 4px;
overflow: hidden
}
.title {
height: 30px;
font-size: 12px;
color: var(--figma-color-text-secondary);
border-bottom: 1px solid var(--figma-color-border);
display: flex;
align-items: center;
padding-left: 16px;
padding-right: 16px;
justify-content: space-between;
user-select: none;
}
.copy-icon {
cursor: pointer;
font-size: 14px;
}
.copy-icon:hover {
opacity: 0.7;
}
.content {
padding: 16px;
font-size: 12px;
color: var(--figma-color-text-secondary);
}
.hidden {
display: none;
}
</style>
<div class="container">
<div class="title">
<span id="copy-icon" class="copy-icon">📋</span>
</div>
<div class="content">
</div>
</div>
<script>
// Yes paste is achieved in a Figma plugin by temp creating a textarea
// hidden off screen and then using the deprecated execCommand API to
// copy the text to the clipboard.
function writeTextToClipboard(str) {
const prevActive = document.activeElement;
const textArea = document.createElement('textarea');
textArea.value = str;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return new Promise((res, rej) => {
document.execCommand('copy') ? res() : rej();
textArea.remove();
prevActive.focus();
});
}
document.getElementById("copy-icon").addEventListener('click', async () => {
const content = document.querySelector('.content').innerText;
writeTextToClipboard(content);
console.log(content);
parent.postMessage({ pluginMessage: { type: 'copy' } }, '*');
});
onmessage = (event) => {
if (event.data.pluginMessage && event.data.pluginMessage.title) {
document.querySelector('.title').firstChild.textContent = event.data.pluginMessage.title;
const content = event.data.pluginMessage.slintProperties;
const copyIcon = document.getElementById('copy-icon');
copyIcon.classList.toggle('hidden', !content);
if (content !== "") {
document.querySelector('.content').innerText = event.data.pluginMessage.slintProperties;
copyIcon.classList.toggle('hidden', false);
} else {
copyIcon.classList.toggle('hidden', true);
}
}
}
</script>

View file

@ -0,0 +1,22 @@
// Copyright © Hyper Brew LLC
// SPDX-License-Identifier: MIT
import { defineConfig } from "vite";
import { figmaCodePlugin } from "vite-figma-plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [figmaCodePlugin()],
build: {
emptyOutDir: false,
outDir: ".tmp",
target: "chrome58",
rollupOptions: {
output: {
manualChunks: {},
entryFileNames: "code.js",
},
input: "./backend/code.ts",
},
},
});

View file

@ -0,0 +1,33 @@
// Copyright © Hyper Brew LLC
// SPDX-License-Identifier: MIT
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
import { figmaPlugin, figmaPluginInit, runAction } from "vite-figma-plugin";
import react from "@vitejs/plugin-react";
import { config } from "./figma.config";
const action = process.env.ACTION;
const mode = process.env.MODE;
if (action) {
runAction(
{},
// config,
action,
);
}
figmaPluginInit();
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), viteSingleFile(), figmaPlugin(config, mode)],
build: {
assetsInlineLimit: Number.POSITIVE_INFINITY,
emptyOutDir: false,
outDir: ".tmp",
},
});

View file

@ -147,7 +147,7 @@ impl<'a> SourceFileWithTags<'a> {
&self.source[tag_loc.start..tag_loc.end]
}
fn tag_matches(&self, expected_tag: &LicenseHeader, license: &str) -> bool {
fn has_license_header(&self, expected_tag: &LicenseHeader) -> bool {
let tag_loc = match &self.tag_location {
Some(loc) => loc,
None => return false,
@ -158,8 +158,10 @@ impl<'a> SourceFileWithTags<'a> {
.trim_end_matches(self.tag_style.overall_end);
let mut tag_entries = found_tag.split(self.tag_style.line_break);
let Some(_copyright_entry) = tag_entries.next() else { return false };
let Some(license_entry) = tag_entries.next() else { return false };
expected_tag.to_string(self.tag_style, license) == license_entry
// Require _some_ license ...
let Some(_) = tag_entries.next() else { return false };
// ... as well as the SPDX license line at the start
expected_tag.0 == SPDX_LICENSE_LINE
}
fn replace_tag(&self, replacement: &LicenseHeader, license: &str) -> String {
@ -522,6 +524,7 @@ lazy_static! {
("\\.tmPreferences$", LicenseLocation::NoLicense),
("\\.toml$", LicenseLocation::NoLicense),
("\\.ts$", LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())),
("\\.tsx$", LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())),
("\\.ttf$", LicenseLocation::NoLicense),
("\\.txt$", LicenseLocation::NoLicense),
("\\.ui$", LicenseLocation::NoLicense),
@ -866,7 +869,7 @@ impl LicenseHeaderCheck {
} else {
Err(anyhow!("Missing tag"))
}
} else if source.tag_matches(&EXPECTED_HEADER, license) {
} else if source.has_license_header(&EXPECTED_HEADER) {
Ok(())
} else if self.fix_it {
eprintln!("Fixing up {path:?} as instructed. It has a wrong license header.");