mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Replace license generator web infra to use Vite
This commit is contained in:
parent
1f2dfcf372
commit
f4ec76f35e
12 changed files with 458 additions and 2663 deletions
|
@ -1,6 +1,11 @@
|
|||
{{!
|
||||
Be careful to prevent auto-formatting from breaking this file's indentation
|
||||
Be careful to prevent auto-formatting from breaking this file's indentation.
|
||||
Replace this file with JSON output once this is resolved: https://github.com/EmbarkStudios/cargo-about/issues/73
|
||||
|
||||
The `GENERATED_BY_CARGO_ABOUT` prefix is a JS labeled statement
|
||||
(<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label>)
|
||||
used so the reader of the generated file can verify the file does indeed start with that string,
|
||||
while remaining valid JS for subsequent parsing.
|
||||
}}
|
||||
GENERATED_BY_CARGO_ABOUT: [
|
||||
{{#each licenses}}
|
||||
|
|
19
about.toml
19
about.toml
|
@ -1,17 +1,18 @@
|
|||
# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`.
|
||||
accepted = [
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Apache-2.0",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSD-2-Clause",
|
||||
"Zlib",
|
||||
"Unicode-DFS-2016",
|
||||
"ISC",
|
||||
"MPL-2.0",
|
||||
"CC0-1.0",
|
||||
"OpenSSL",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT-0",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
ignore-build-dependencies = true
|
||||
ignore-dev-dependencies = true
|
||||
|
|
28
deny.toml
28
deny.toml
|
@ -70,20 +70,22 @@ unlicensed = "deny"
|
|||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
#
|
||||
# Keep this list in sync with those in `/about.toml` and `/frontend/vite.config.ts`.
|
||||
allow = [
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"BSD-2-Clause",
|
||||
"Zlib",
|
||||
"Unicode-DFS-2016",
|
||||
"ISC",
|
||||
"MPL-2.0",
|
||||
"CC0-1.0",
|
||||
"OpenSSL",
|
||||
"BSL-1.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT-0",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
# List of explicitly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
|
@ -135,7 +137,7 @@ expression = "MIT AND ISC AND OpenSSL"
|
|||
# depending on the rest of your configuration
|
||||
license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const webpackConfigPath = require.resolve("@webpack/webpack.config.js");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
@ -19,7 +18,6 @@ module.exports = {
|
|||
"import/resolver": {
|
||||
// `node` must be listed first!
|
||||
node: {},
|
||||
webpack: { config: webpackConfigPath },
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
|
|
|
@ -6,7 +6,7 @@ For lack of other options, the frontend is currently written as a web app. Maint
|
|||
|
||||
## Bundled assets: `assets/`
|
||||
|
||||
Icons and images that are used in components and embedded into the application bundle by the build system using [loaders](https://webpack.js.org/loaders/).
|
||||
Icons and images that are used in components and embedded into the application bundle by the build system.
|
||||
|
||||
## Public assets: `public/`
|
||||
|
||||
|
@ -26,7 +26,7 @@ Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for
|
|||
|
||||
## npm ecosystem packages: `package.json`
|
||||
|
||||
While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js and then run `cd frontend && npm install` to install these packages on your system. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Webpack, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process.
|
||||
While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js and then run `cd frontend && npm install` to install these packages on your system. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vite, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process.
|
||||
|
||||
## npm package installed versions: `package-lock.json`
|
||||
|
||||
|
@ -36,6 +36,6 @@ Specifies the exact versions of packages installed in the npm dependency tree. W
|
|||
|
||||
Basic configuration options for the TypeScript build tool to do its job in our repository.
|
||||
|
||||
## Webpack configurations: `webpack.config.js`
|
||||
## Vite configurations: `vite.config.js`
|
||||
|
||||
We use the [Webpack](https://webpack.js.org/) bundler/build system. This file is where we configure Webpack to set up plugins (like wasm-pack and license-checker) and loaders (like for Svelte and SVG files). Part of the license-checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code.
|
||||
We use the [Vite](https://vitejs.dev/) bundler/build system. This file is where we configure Vite to set up plugins (like the third-party license checker/generator). Part of the license checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code.
|
||||
|
|
2500
frontend/package-lock.json
generated
2500
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -6,17 +6,16 @@
|
|||
"browserslist": "> 1.5%, last 2 versions, not dead, not ie 11, not op_mini all, not ios_saf < 13",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "npm run build-wasm && concurrently -k vite \"npm run watch:wasm\" || (npm run print-building-help && exit 1)",
|
||||
"start-profiling": "npm run build-wasm-profiling && concurrently -k vite \"npm run watch:wasm-profiling\" || (npm run print-building-help && exit 1)",
|
||||
"build": "npm run build-wasm-prod && vite build && npm run build-licenses || (npm run print-building-help && exit 1)",
|
||||
"build-licenses": "webpack build",
|
||||
"tauri:dev": "echo 'Make sure you build the wasm binary for tauri using `npm run tauri:build-wasm`' && vite",
|
||||
"start": "npm run build-wasm && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run watch:wasm\" || (npm run print-building-help && exit 1)",
|
||||
"profiling": "npm run build-wasm-profiling && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run watch:wasm-profiling\" || (npm run print-building-help && exit 1)",
|
||||
"build": "npm run build-wasm-prod && vite build || (npm run print-building-help && exit 1)",
|
||||
"build-wasm": "wasm-pack build ./wasm --dev --target=web",
|
||||
"build-wasm-profiling": "wasm-pack build ./wasm --profiling --target=web",
|
||||
"build-wasm-prod": "wasm-pack build ./wasm --release --target=web",
|
||||
"tauri": "echo 'Make sure you build the wasm binary for tauri using `npm run tauri:build-wasm`' && vite",
|
||||
"tauri:build-wasm": "wasm-pack build ./wasm --release --target=web -- --features tauri",
|
||||
"watch:wasm": "cargo watch --postpone --watch-when-idle --workdir wasm --shell \"wasm-pack build . --dev --target=web -- --color always\"",
|
||||
"watch:wasm-profiling": "cargo watch --postpone --watch-when-idle --workdir wasm --shell \"wasm-pack build . --profiling --target=web -- --color always\"",
|
||||
"watch:wasm": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --color=always\"",
|
||||
"watch:wasm-profiling": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --profiling --target=web -- --color=always\"",
|
||||
"--------------------": "",
|
||||
"print-building-help": "echo 'Graphite project failed to build. Did you remember to `npm install` the dependencies in `/frontend`?'",
|
||||
"print-linting-help": "echo 'Graphite project had lint errors, or may have otherwise failed. In the latter case, did you remember to `npm install` the dependencies in `/frontend`?'"
|
||||
|
@ -29,22 +28,18 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"@types/license-checker-webpack-plugin": "^0.2.1",
|
||||
"@types/node": "^18.16.2",
|
||||
"@types/webpack": "^5.28.1",
|
||||
"buffer": "^5.7.1",
|
||||
"concurrently": "^8.0.1",
|
||||
"license-checker-webpack-plugin": "^0.2.1",
|
||||
"postcss": "^8.4.23",
|
||||
"process": "^0.11.10",
|
||||
"rollup-plugin-license": "^3.2.0",
|
||||
"sass": "^1.62.1",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte": "^3.58.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.4.5",
|
||||
"webpack": "^5.81.0",
|
||||
"webpack-cli": "^5.0.2"
|
||||
"vite": "^4.4.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wasm-pack": "0.12.1"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
// This is a dummy entry point for webpack
|
2
frontend/src/vite-env-override.d.ts
vendored
2
frontend/src/vite-env-override.d.ts
vendored
|
@ -1,6 +1,6 @@
|
|||
// Allow `import` statements to work with SVG files in the eyes of the TypeScript compiler.
|
||||
// This prevents red underlines from showing and lets it know the types of imported variables are strings.
|
||||
// The actual import is performed by Webpack when building, as configured in the module rules in `webpack.config.ts`.
|
||||
// The actual import is performed by Vite when building, as configured in the `resolve` aliases in `vite.config.ts`.
|
||||
declare module "*.svg" {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
|
|
@ -27,10 +27,6 @@
|
|||
"module": "commonjs",
|
||||
"useDefineForClassFields": false,
|
||||
"noImplicitOverride": true
|
||||
},
|
||||
"moduleTypes": {
|
||||
"webpack.config.ts": "cjs",
|
||||
"webpack-config-scripts/**/*": "cjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,28 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { spawnSync } from "child_process";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { resolve } from "path";
|
||||
import { sveltePreprocess } from "svelte-preprocess/dist/autoProcess";
|
||||
import path from "path";
|
||||
import rollupPluginLicense, { type Dependency } from "rollup-plugin-license";
|
||||
|
||||
const projectRootDir = resolve(__dirname);
|
||||
const projectRootDir = path.resolve(__dirname);
|
||||
|
||||
// Keep this list in sync with those in `/about.toml` and `/deny.toml`.
|
||||
const ALLOWED_LICENSES = [
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT-0",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
];
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
@ -14,20 +33,241 @@ export default defineConfig({
|
|||
const suppressed = ["vite-plugin-svelte-css-no-scopable-elements"];
|
||||
if (suppressed.includes(warning.code)) return;
|
||||
|
||||
defaultHandler(warning);
|
||||
defaultHandler?.(warning);
|
||||
}
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: /@graphite-frontend\/(.*\.svg)/, replacement: resolve(projectRootDir, "$1?raw") },
|
||||
{ find: /@graphite-frontend\/(.*\.svg)/, replacement: path.resolve(projectRootDir, "$1?raw") },
|
||||
{ find: "@graphite-frontend", replacement: projectRootDir },
|
||||
{ find: "@graphite/../assets", replacement: resolve(projectRootDir, "assets") },
|
||||
{ find: "@graphite/../public", replacement: resolve(projectRootDir, "public") },
|
||||
{ find: "@graphite", replacement: resolve(projectRootDir, "src") },
|
||||
{ find: "@graphite/../assets", replacement: path.resolve(projectRootDir, "assets") },
|
||||
{ find: "@graphite/../public", replacement: path.resolve(projectRootDir, "public") },
|
||||
{ find: "@graphite", replacement: path.resolve(projectRootDir, "src") },
|
||||
]
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
rollupPluginLicense({
|
||||
thirdParty: {
|
||||
allow: {
|
||||
test: `(${ALLOWED_LICENSES.join(" OR ")})`,
|
||||
failOnUnlicensed: true,
|
||||
failOnViolation: true,
|
||||
},
|
||||
output: {
|
||||
file: path.resolve(__dirname, "./dist/third-party-licenses.txt"),
|
||||
template: formatThirdPartyLicenses,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface LicenseInfo {
|
||||
licenseName: string;
|
||||
licenseText: string;
|
||||
packages: PackageInfo[];
|
||||
}
|
||||
|
||||
interface PackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
repository: string;
|
||||
}
|
||||
|
||||
function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
||||
// Generate the Rust license information.
|
||||
let licenses = generateRustLicenses() || [];
|
||||
|
||||
// Ensure we have license information to work with before proceeding.
|
||||
if (licenses.length === 0) {
|
||||
// This is probably caused by `cargo about` not being installed.
|
||||
console.error("Could not run \`cargo about\`, which is required to generate license information.");
|
||||
console.error("To install cargo-about on your system, you can run \`cargo install cargo-about\`.");
|
||||
console.error("License information is required in production builds. Aborting.");
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
if (jsLicenses.length === 0) {
|
||||
console.error("No JavaScript package licenses were found by `rollup-plugin-license`. Please investigate.");
|
||||
console.error("License information is required in production builds. Aborting.");
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Augment the imported Rust license list with the provided JS license list.
|
||||
jsLicenses.forEach((jsLicense) => {
|
||||
const name = jsLicense.name || "";
|
||||
const version = jsLicense.version || "";
|
||||
const author = jsLicense.author?.text() || "";
|
||||
const licenseText = trimBlankLines(jsLicense.licenseText ?? "");
|
||||
const licenseName = jsLicense.license || "";
|
||||
let repository = jsLicense.repository || "";
|
||||
if (repository && typeof repository === "object") repository = repository.url;
|
||||
|
||||
// Remove the `git+` or `git://` prefix and `.git` suffix.
|
||||
const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository;
|
||||
|
||||
const matchedLicense = licenses.find((license) => trimBlankLines(license.licenseText || "") === licenseText);
|
||||
|
||||
const packages: PackageInfo = { name, version, author, repository: repo };
|
||||
if (matchedLicense) matchedLicense.packages.push(packages);
|
||||
else licenses.push({ licenseName, licenseText, packages: [packages] });
|
||||
});
|
||||
|
||||
// De-duplicate any licenses with the same text by merging their lists of packages.
|
||||
licenses.forEach((license, licenseIndex) => {
|
||||
licenses.slice(0, licenseIndex).forEach((comparisonLicense) => {
|
||||
if (license.licenseText === comparisonLicense.licenseText) {
|
||||
license.packages.push(...comparisonLicense.packages);
|
||||
comparisonLicense.packages = [];
|
||||
// After emptying the packages, the redundant license with no packages will be removed in the next step's `filter()`.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Filter out the internal Graphite crates, which are not third-party.
|
||||
licenses = licenses.filter((license) => {
|
||||
license.packages = license.packages.filter((packageInfo) =>
|
||||
!(packageInfo.repository && packageInfo.repository.toLowerCase().includes("github.com/GraphiteEditor/Graphite".toLowerCase())) &&
|
||||
!(packageInfo.author && packageInfo.author.toLowerCase().includes("contact@graphite.rs"))
|
||||
);
|
||||
return license.packages.length > 0;
|
||||
});
|
||||
|
||||
// Sort the licenses, and the packages using each license, alphabetically.
|
||||
licenses.sort((a, b) => a.licenseName.localeCompare(b.licenseName));
|
||||
licenses.sort((a, b) => a.licenseText.localeCompare(b.licenseText));
|
||||
licenses.forEach((license) => {
|
||||
license.packages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Append a block for each license shared by multiple packages with identical license text.
|
||||
let formattedLicenseNotice = "GRAPHITE THIRD-PARTY SOFTWARE LICENSE NOTICES";
|
||||
licenses.forEach((license) => {
|
||||
let packagesWithSameLicense = "";
|
||||
license.packages.forEach((packageInfo) => {
|
||||
const { name, version, author, repository } = packageInfo;
|
||||
packagesWithSameLicense += `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}\n`;
|
||||
});
|
||||
packagesWithSameLicense = packagesWithSameLicense.trim();
|
||||
const packagesLineLength = Math.max(...packagesWithSameLicense.split("\n").map((line) => line.length));
|
||||
|
||||
formattedLicenseNotice += "\n\n--------------------------------------------------------------------------------\n\n";
|
||||
formattedLicenseNotice += `The following packages are licensed under the terms of the ${license.licenseName} license as printed beneath:\n`;
|
||||
formattedLicenseNotice += `${"_".repeat(packagesLineLength)}\n`;
|
||||
formattedLicenseNotice += `${packagesWithSameLicense}\n`;
|
||||
formattedLicenseNotice += `${"‾".repeat(packagesLineLength)}\n`;
|
||||
formattedLicenseNotice += `${license.licenseText}\n`;
|
||||
});
|
||||
return formattedLicenseNotice;
|
||||
}
|
||||
|
||||
function generateRustLicenses(): LicenseInfo[] | undefined {
|
||||
// Log the starting status to the build output.
|
||||
console.info("\n\nGenerating license information for Rust code\n");
|
||||
|
||||
try {
|
||||
// Call `cargo about` in the terminal to generate the license information for Rust crates.
|
||||
// The `about.hbs` file is written so it generates a valid JavaScript array expression which we evaluate below.
|
||||
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf8",
|
||||
timeout: 60000, // One minute
|
||||
shell: true,
|
||||
windowsHide: true, // Hide the terminal on Windows
|
||||
});
|
||||
|
||||
// If the command failed, print the error message and exit early.
|
||||
if (status !== 0) {
|
||||
// Cargo returns 101 when the subcommand (`about`) wasn't found, so we skip printing the below error message in that case.
|
||||
if (status !== 101) {
|
||||
console.error("cargo-about failed", status, stderr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Make sure the output starts with this expected label, which lets us know the file generated with expected output.
|
||||
// We don't want to eval an error message or something else, so we fail early if that happens.
|
||||
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
|
||||
console.error("Unexpected output from cargo-about", stdout);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Convert the array JS syntax string into an actual JS array in memory.
|
||||
// Security-wise, eval() isn't any worse than require(), but it's able to work without a temporary file.
|
||||
// We call eval indirectly to avoid a warning as explained here: <https://esbuild.github.io/content-types/#direct-eval>.
|
||||
const indirectEval = eval;
|
||||
const licensesArray = indirectEval(stdout) as LicenseInfo[];
|
||||
|
||||
// Remove the HTML character encoding caused by Handlebars.
|
||||
let rustLicenses = (licensesArray || []).map((rustLicense): LicenseInfo => ({
|
||||
licenseName: htmlDecode(rustLicense.licenseName),
|
||||
licenseText: trimBlankLines(htmlDecode(rustLicense.licenseText)),
|
||||
packages: rustLicense.packages.map((packageInfo): PackageInfo => ({
|
||||
name: htmlDecode(packageInfo.name),
|
||||
version: htmlDecode(packageInfo.version),
|
||||
author: htmlDecode(packageInfo.author).replace(/\[(.*), \]/, "$1").replace("[]", ""),
|
||||
repository: htmlDecode(packageInfo.repository),
|
||||
})),
|
||||
}));
|
||||
|
||||
return rustLicenses;
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function htmlDecode(input: string): string {
|
||||
if (!input) return input;
|
||||
|
||||
const htmlEntities = {
|
||||
nbsp: " ",
|
||||
copy: "©",
|
||||
reg: "®",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
amp: "&",
|
||||
apos: "'",
|
||||
quot: `"`,
|
||||
};
|
||||
|
||||
return input.replace(/&([^;]+);/g, (entity: string, entityCode: string) => {
|
||||
const maybeEntity = Object.keys(htmlEntities).find((key) => key === entityCode);
|
||||
if (maybeEntity) {
|
||||
return maybeEntity[1];
|
||||
}
|
||||
|
||||
let match;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#x([\da-fA-F]+)$/))) {
|
||||
return String.fromCharCode(parseInt(match[1], 16));
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#(\d+)$/))) {
|
||||
return String.fromCharCode(~~match[1]);
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
function trimBlankLines(input: string): string {
|
||||
let result = input.replace(/\r/g, "");
|
||||
|
||||
while (result.charAt(0) === "\r" || result.charAt(0) === "\n") {
|
||||
result = result.slice(1);
|
||||
}
|
||||
while (result.slice(-1) === "\r" || result.slice(-1) === "\n") {
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,273 +0,0 @@
|
|||
// TODO: Investigate replacing this with https://github.com/vitejs/vite/discussions/7722#discussioncomment-4007436
|
||||
|
||||
import * as path from "path";
|
||||
import fs from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
import * as webpack from "webpack";
|
||||
const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin");
|
||||
|
||||
const config: webpack.Configuration = {
|
||||
entry: {
|
||||
bundle: ["./src/main-webpack-licenses.ts"]
|
||||
},
|
||||
mode: "production",
|
||||
resolve: {
|
||||
alias: {
|
||||
// Note: Later in this config file, we'll automatically add paths from `tsconfig.compilerOptions.paths`
|
||||
svelte: path.resolve("node_modules", "svelte")
|
||||
},
|
||||
extensions: [".ts", ".js", ".svelte"],
|
||||
mainFields: ["svelte", "browser", "module", "main"]
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
publicPath: "/dist/",
|
||||
filename: "[name].js",
|
||||
chunkFilename: "[name].[id].js"
|
||||
},
|
||||
module: {
|
||||
rules: []
|
||||
},
|
||||
plugins: [
|
||||
// License Checker Webpack Plugin validates the license compatibility of all dependencies which are compiled into the webpack bundle
|
||||
// It also writes the third-party license notices to a file which is displayed in the application
|
||||
// https://github.com/microsoft/license-checker-webpack-plugin
|
||||
new LicenseCheckerWebpackPlugin({
|
||||
allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR 0BSD)",
|
||||
emitError: true,
|
||||
outputFilename: "../dist/third-party-licenses.txt",
|
||||
outputWriter: formatThirdPartyLicenses,
|
||||
// Workaround for failure caused in WebPack 5: https://github.com/microsoft/license-checker-webpack-plugin/issues/25#issuecomment-833325799
|
||||
filter: /(^.*[/\\]node_modules[/\\]((?:@[^/\\]+[/\\])?(?:[^@/\\][^/\\]*)))/,
|
||||
}),
|
||||
|
||||
// new SvelteCheckPlugin(),
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Load path aliases from the tsconfig.json file
|
||||
const tsconfigPath = path.resolve(__dirname, "tsconfig.json");
|
||||
const tsconfig = fs.existsSync(tsconfigPath) ? require(tsconfigPath) : {};
|
||||
|
||||
if ("compilerOptions" in tsconfig && "paths" in tsconfig.compilerOptions) {
|
||||
const aliases = tsconfig.compilerOptions.paths;
|
||||
|
||||
for (const alias in aliases) {
|
||||
const paths = aliases[alias].map((p: string) => path.resolve(__dirname, p));
|
||||
|
||||
// Our tsconfig uses glob path formats, whereas webpack just wants directories
|
||||
// We'll need to transform the glob format into a format acceptable to webpack
|
||||
|
||||
const wpAlias = alias.replace(/(\\|\/)\*$/, "");
|
||||
const wpPaths = paths.map((p: string) => p.replace(/(\\|\/)\*$/, ""));
|
||||
|
||||
if (config.resolve && config.resolve.alias) {
|
||||
if (!(wpAlias in config.resolve.alias) && wpPaths.length) {
|
||||
(config.resolve.alias as any)[wpAlias] = wpPaths.length > 1 ? wpPaths : wpPaths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
||||
interface LicenseInfo {
|
||||
licenseName: string;
|
||||
licenseText: string;
|
||||
packages: PackageInfo[]
|
||||
}
|
||||
|
||||
interface PackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
repository: string;
|
||||
}
|
||||
|
||||
interface Dependency extends PackageInfo {
|
||||
licenseName: string;
|
||||
licenseText?: string;
|
||||
}
|
||||
|
||||
function formatThirdPartyLicenses(jsLicenses: {dependencies: Dependency[]}): string {
|
||||
let rustLicenses: LicenseInfo[] | undefined;
|
||||
if (process.env.SKIP_CARGO_ABOUT === undefined) {
|
||||
try {
|
||||
rustLicenses = generateRustLicenses();
|
||||
} catch (err) {
|
||||
// Nothing to show. Error messages were printed above.
|
||||
}
|
||||
|
||||
if (rustLicenses === undefined) {
|
||||
// This is probably caused by cargo about not being installed
|
||||
console.error(
|
||||
`
|
||||
Could not run \`cargo about\`, which is required to generate license information.
|
||||
To install cargo-about on your system, you can run \`cargo install cargo-about\`.
|
||||
License information is required on production builds. Aborting.
|
||||
`
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.join("\n")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the HTML character encoding caused by Handlebars
|
||||
let licenses = (rustLicenses || []).map((rustLicense): LicenseInfo => ({
|
||||
licenseName: htmlDecode(rustLicense.licenseName),
|
||||
licenseText: trimBlankLines(htmlDecode(rustLicense.licenseText)),
|
||||
packages: rustLicense.packages.map((packageInfo): PackageInfo => ({
|
||||
name: htmlDecode(packageInfo.name),
|
||||
version: htmlDecode(packageInfo.version),
|
||||
author: htmlDecode(packageInfo.author).replace(/\[(.*), \]/, "$1"),
|
||||
repository: htmlDecode(packageInfo.repository),
|
||||
})),
|
||||
}));
|
||||
|
||||
// De-duplicate any licenses with the same text by merging their lists of packages
|
||||
licenses.forEach((license, licenseIndex) => {
|
||||
licenses.slice(0, licenseIndex).forEach((comparisonLicense) => {
|
||||
if (license.licenseText === comparisonLicense.licenseText) {
|
||||
license.packages.push(...comparisonLicense.packages);
|
||||
comparisonLicense.packages = [];
|
||||
// After emptying the packages, the redundant license with no packages will be removed in the next step's `filter()`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete the internal Graphite crates, which are not third-party and belong elsewhere
|
||||
licenses = licenses.filter((license) => {
|
||||
license.packages = license.packages.filter((packageInfo) => !(packageInfo.repository && packageInfo.repository.includes("github.com/GraphiteEditor/Graphite")));
|
||||
return license.packages.length > 0;
|
||||
});
|
||||
|
||||
// Augment the imported Rust license list with the provided JS license list
|
||||
jsLicenses.dependencies.forEach((jsLicense) => {
|
||||
const { name, version, author, repository, licenseName } = jsLicense;
|
||||
const licenseText = trimBlankLines(jsLicense.licenseText ?? "");
|
||||
|
||||
// Remove the `git+` or `git://` prefix and `.git` suffix
|
||||
const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository;
|
||||
|
||||
const matchedLicense = licenses.find((license) => trimBlankLines(license.licenseText) === licenseText);
|
||||
|
||||
const packages: PackageInfo = { name, version, author, repository: repo };
|
||||
if (matchedLicense) matchedLicense.packages.push(packages);
|
||||
else licenses.push({ licenseName, licenseText, packages: [packages] });
|
||||
});
|
||||
|
||||
// Sort the licenses, and the packages using each license, alphabetically
|
||||
licenses.sort((a, b) => a.licenseName.localeCompare(b.licenseName));
|
||||
licenses.sort((a, b) => a.licenseText.localeCompare(b.licenseText));
|
||||
licenses.forEach((license) => {
|
||||
license.packages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Generate the formatted text file
|
||||
let formattedLicenseNotice = "GRAPHITE THIRD-PARTY SOFTWARE LICENSE NOTICES\n\n";
|
||||
if (!rustLicenses) formattedLicenseNotice += "WARNING: Licenses for Rust packages are excluded in debug mode to improve performance — do not release without their inclusion!\n\n";
|
||||
|
||||
licenses.forEach((license) => {
|
||||
let packagesWithSameLicense = "";
|
||||
license.packages.forEach((packageInfo) => {
|
||||
const { name, version, author, repository } = packageInfo;
|
||||
packagesWithSameLicense += `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}\n`;
|
||||
});
|
||||
packagesWithSameLicense = packagesWithSameLicense.trim();
|
||||
const packagesLineLength = Math.max(...packagesWithSameLicense.split("\n").map((line) => line.length));
|
||||
|
||||
formattedLicenseNotice += `--------------------------------------------------------------------------------
|
||||
|
||||
The following packages are licensed under the terms of the ${license.licenseName} license as printed beneath:
|
||||
${"_".repeat(packagesLineLength)}
|
||||
${packagesWithSameLicense}
|
||||
${"‾".repeat(packagesLineLength)}
|
||||
${license.licenseText}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
return formattedLicenseNotice;
|
||||
}
|
||||
|
||||
function generateRustLicenses(): LicenseInfo[] | undefined {
|
||||
console.info("Generating license information for Rust code");
|
||||
// This `about.hbs` file is written so it generates a valid JavaScript array expression which we evaluate below
|
||||
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf8",
|
||||
timeout: 60000, // One minute
|
||||
shell: true,
|
||||
windowsHide: true, // Hide the terminal on Windows
|
||||
});
|
||||
|
||||
if (status !== 0) {
|
||||
if (status !== 101) {
|
||||
// Cargo returns 101 when the subcommand wasn't found
|
||||
console.error("cargo-about failed", status, stderr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Make sure the output starts with this expected label, we don't want to eval an error message.
|
||||
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
|
||||
console.error("Unexpected output from cargo-about", stdout);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Security-wise, eval() isn't any worse than require(), but it doesn't need a temporary file.
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(stdout) as LicenseInfo[];
|
||||
}
|
||||
|
||||
function htmlDecode(input: string): string {
|
||||
if (!input) return input;
|
||||
|
||||
const htmlEntities = {
|
||||
nbsp: " ",
|
||||
copy: "©",
|
||||
reg: "®",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
amp: "&",
|
||||
apos: "'",
|
||||
quot: `"`,
|
||||
};
|
||||
|
||||
return input.replace(/&([^;]+);/g, (entity: string, entityCode: string) => {
|
||||
let match;
|
||||
|
||||
const maybeEntity = Object.entries(htmlEntities).find(([key, _]) => key === entityCode);
|
||||
if (maybeEntity) {
|
||||
return maybeEntity[1];
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#x([\da-fA-F]+)$/))) {
|
||||
return String.fromCharCode(parseInt(match[1], 16));
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#(\d+)$/))) {
|
||||
return String.fromCharCode(~~match[1]);
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
function trimBlankLines(input: string): string {
|
||||
let result = input.replace(/\r/g, "");
|
||||
|
||||
while (result.charAt(0) === "\r" || result.charAt(0) === "\n") {
|
||||
result = result.slice(1);
|
||||
}
|
||||
while (result.slice(-1) === "\r" || result.slice(-1) === "\n") {
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue