chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
const { readdirSync, writeFileSync } = require("fs");
|
||||
const files = readdirSync(`${__dirname}/../packages/excalidraw/locales`);
|
||||
|
||||
const flatten = (object = {}, result = {}, extraKey = "") => {
|
||||
for (const key in object) {
|
||||
if (typeof object[key] !== "object") {
|
||||
result[extraKey + key] = object[key];
|
||||
} else {
|
||||
flatten(object[key], result, `${extraKey}${key}.`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const locales = files.filter(
|
||||
(file) => file !== "README.md" && file !== "percentages.json",
|
||||
);
|
||||
|
||||
const percentages = {};
|
||||
|
||||
for (let index = 0; index < locales.length; index++) {
|
||||
const currentLocale = locales[index];
|
||||
const data = flatten(
|
||||
require(`${__dirname}/../packages/excalidraw/locales/${currentLocale}`),
|
||||
);
|
||||
|
||||
const allKeys = Object.keys(data);
|
||||
const translatedKeys = allKeys.filter((item) => data[item] !== "");
|
||||
const percentage = Math.floor((100 * translatedKeys.length) / allKeys.length);
|
||||
percentages[currentLocale.replace(".json", "")] = percentage;
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
`${__dirname}/../packages/excalidraw/locales/percentages.json`,
|
||||
`${JSON.stringify(percentages, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// In order to use this, you need to install Cairo on your machine. See
|
||||
// instructions here: https://github.com/Automattic/node-canvas#compiling
|
||||
|
||||
// In order to run:
|
||||
// npm install canvas # please do not check it in
|
||||
// yarn build-node
|
||||
// node build/static/js/build-node.js
|
||||
// open test.png
|
||||
|
||||
const rewire = require("rewire");
|
||||
const defaults = rewire("react-scripts/scripts/build.js");
|
||||
const config = defaults.__get__("config");
|
||||
|
||||
// Disable multiple chunks
|
||||
config.optimization.runtimeChunk = false;
|
||||
config.optimization.splitChunks = {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
// Set the filename to be deterministic
|
||||
config.output.filename = "static/js/build-node.js";
|
||||
// Don't choke on node-specific requires
|
||||
config.target = "node";
|
||||
// Set the node entrypoint
|
||||
config.entry = "../packages/excalidraw/index-node";
|
||||
// By default, webpack is going to replace the require of the canvas.node file
|
||||
// to just a string with the path of the canvas.node file. We need to tell
|
||||
// webpack to avoid rewriting that dependency.
|
||||
config.externals = (context, request, callback) => {
|
||||
if (/\.node$/.test(request)) {
|
||||
return callback(
|
||||
null,
|
||||
"commonjs ../../../node_modules/canvas/build/Release/canvas.node",
|
||||
);
|
||||
}
|
||||
callback();
|
||||
};
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const versionFile = path.join("build", "version.json");
|
||||
const indexFile = path.join("build", "index.html");
|
||||
|
||||
const versionDate = (date) => date.toISOString().replace(".000", "");
|
||||
|
||||
const commitHash = () => {
|
||||
try {
|
||||
return require("child_process")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
return "none";
|
||||
}
|
||||
};
|
||||
|
||||
const commitDate = (hash) => {
|
||||
try {
|
||||
const unix = require("child_process")
|
||||
.execSync(`git show -s --format=%ct ${hash}`)
|
||||
.toString()
|
||||
.trim();
|
||||
const date = new Date(parseInt(unix) * 1000);
|
||||
return versionDate(date);
|
||||
} catch {
|
||||
return versionDate(new Date());
|
||||
}
|
||||
};
|
||||
|
||||
const getFullVersion = () => {
|
||||
const hash = commitHash();
|
||||
return `${commitDate(hash)}-${hash}`;
|
||||
};
|
||||
|
||||
const data = JSON.stringify(
|
||||
{
|
||||
version: getFullVersion(),
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.writeFileSync(versionFile, data);
|
||||
|
||||
// https://stackoverflow.com/a/14181136/8418
|
||||
fs.readFile(indexFile, "utf8", (error, data) => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
const result = data.replace(/{version}/g, getFullVersion());
|
||||
|
||||
fs.writeFile(indexFile, result, "utf8", (error) => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
const path = require("path");
|
||||
|
||||
const { build } = require("esbuild");
|
||||
|
||||
// contains all dependencies bundled inside
|
||||
const getConfig = (outdir) => ({
|
||||
outdir,
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
entryPoints: ["src/index.ts"],
|
||||
entryNames: "[name]",
|
||||
assetNames: "[dir]/[name]",
|
||||
alias: {
|
||||
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
|
||||
},
|
||||
external: [
|
||||
"@excalidraw/common",
|
||||
"@excalidraw/element",
|
||||
"@excalidraw/math",
|
||||
"@excalidraw/fractional-indexing",
|
||||
],
|
||||
});
|
||||
|
||||
function buildDev(config) {
|
||||
return build({
|
||||
...config,
|
||||
sourcemap: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildProd(config) {
|
||||
return build({
|
||||
...config,
|
||||
minify: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createESMRawBuild = async () => {
|
||||
// development unminified build with source maps
|
||||
await buildDev(getConfig("dist/dev"));
|
||||
|
||||
// production minified build without sourcemaps
|
||||
await buildProd(getConfig("dist/prod"));
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await createESMRawBuild();
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
const { exec } = require("child_process");
|
||||
|
||||
// get files changed between prev and head commit
|
||||
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||
if (error || stderr) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
const changedFiles = stdout.trim().split("\n");
|
||||
|
||||
const docFiles = changedFiles.filter((file) => {
|
||||
return file.indexOf("docs") >= 0;
|
||||
});
|
||||
|
||||
if (!docFiles.length) {
|
||||
console.info("Skipping building docs as no valid diff found");
|
||||
process.exit(0);
|
||||
}
|
||||
// Exit code 1 to build the docs in ignoredBuildStep
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const { build } = require("esbuild");
|
||||
const { sassPlugin } = require("esbuild-sass-plugin");
|
||||
|
||||
const { parseEnvVariables } = require("../packages/excalidraw/env.cjs");
|
||||
|
||||
const ENV_VARS = {
|
||||
development: {
|
||||
...parseEnvVariables(`${__dirname}/../.env.development`),
|
||||
DEV: true,
|
||||
},
|
||||
production: {
|
||||
...parseEnvVariables(`${__dirname}/../.env.production`),
|
||||
PROD: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Resolve a relative path from the source file's directory
|
||||
const resolveRelativePath = (importPath, sourceFile) => {
|
||||
const sourceDir = path.dirname(sourceFile);
|
||||
const extensions = [".scss", ".css", ""];
|
||||
|
||||
for (const ext of extensions) {
|
||||
const fullPath = path.resolve(sourceDir, importPath + ext);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
// Try with underscore prefix for partials
|
||||
const partialPath = path.join(
|
||||
path.dirname(fullPath),
|
||||
`_${path.basename(fullPath)}`,
|
||||
);
|
||||
if (fs.existsSync(partialPath)) {
|
||||
return partialPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Precompile function to convert relative paths to absolute paths
|
||||
const precompile = (source, sourcePath) => {
|
||||
// Match @use and @forward statements with relative paths
|
||||
const importRegex = /(@use|@forward)\s+["'](\.[^"']+)["']/g;
|
||||
|
||||
return source.replace(importRegex, (match, directive, importPath) => {
|
||||
const resolvedPath = resolveRelativePath(importPath, sourcePath);
|
||||
if (resolvedPath) {
|
||||
// Convert to file:// URL format for sass
|
||||
const fileUrl = pathToFileURL(resolvedPath).href;
|
||||
return `${directive} "${fileUrl}"`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
// excludes all external dependencies and bundles only the source code
|
||||
const getConfig = (outdir) => ({
|
||||
outdir,
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
packages: "external",
|
||||
plugins: [
|
||||
sassPlugin({
|
||||
precompile,
|
||||
}),
|
||||
],
|
||||
target: "es2020",
|
||||
assetNames: "[dir]/[name]",
|
||||
chunkNames: "[dir]/[name]-[hash]",
|
||||
alias: {
|
||||
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
|
||||
},
|
||||
external: [
|
||||
"@excalidraw/common",
|
||||
"@excalidraw/element",
|
||||
"@excalidraw/math",
|
||||
"@excalidraw/fractional-indexing",
|
||||
],
|
||||
loader: {
|
||||
".woff2": "file",
|
||||
},
|
||||
});
|
||||
|
||||
function buildDev(config) {
|
||||
return build({
|
||||
...config,
|
||||
sourcemap: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify(ENV_VARS.development),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildProd(config) {
|
||||
return build({
|
||||
...config,
|
||||
minify: true,
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify(ENV_VARS.production),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createESMRawBuild = async () => {
|
||||
const chunksConfig = {
|
||||
entryPoints: ["index.tsx", "**/*.chunk.ts"],
|
||||
entryNames: "[name]",
|
||||
};
|
||||
|
||||
// development unminified build with source maps
|
||||
await buildDev({
|
||||
...getConfig("dist/dev"),
|
||||
...chunksConfig,
|
||||
});
|
||||
|
||||
// production minified buld without sourcemaps
|
||||
await buildProd({
|
||||
...getConfig("dist/prod"),
|
||||
...chunksConfig,
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await createESMRawBuild();
|
||||
})();
|
||||
@@ -0,0 +1,66 @@
|
||||
const path = require("path");
|
||||
|
||||
const { build } = require("esbuild");
|
||||
const { sassPlugin } = require("esbuild-sass-plugin");
|
||||
|
||||
const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins");
|
||||
|
||||
// contains all dependencies bundled inside
|
||||
const getConfig = (outdir) => ({
|
||||
outdir,
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
entryPoints: ["src/index.ts"],
|
||||
entryNames: "[name]",
|
||||
assetNames: "[dir]/[name]",
|
||||
alias: {
|
||||
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
|
||||
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
|
||||
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
|
||||
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
|
||||
"@excalidraw/fractional-indexing": path.resolve(
|
||||
__dirname,
|
||||
"../packages/fractional-indexing/src",
|
||||
),
|
||||
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
|
||||
},
|
||||
});
|
||||
|
||||
function buildDev(config) {
|
||||
return build({
|
||||
...config,
|
||||
sourcemap: true,
|
||||
plugins: [sassPlugin(), woff2ServerPlugin()],
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildProd(config) {
|
||||
return build({
|
||||
...config,
|
||||
minify: true,
|
||||
plugins: [
|
||||
sassPlugin(),
|
||||
woff2ServerPlugin({
|
||||
outdir: `${config.outdir}/assets`,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createESMRawBuild = async () => {
|
||||
// development unminified build with source maps
|
||||
await buildDev(getConfig("dist/dev"));
|
||||
|
||||
// production minified build without sourcemaps
|
||||
await buildProd(getConfig("dist/prod"));
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await createESMRawBuild();
|
||||
})();
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* This script is used to convert the wasm modules into js modules, with the binary converted into base64 encoded strings.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const wasmModules = [
|
||||
{
|
||||
pkg: `../node_modules/fonteditor-core`,
|
||||
src: `./wasm/woff2.wasm`,
|
||||
dest: `../packages/excalidraw/fonts/wasm/woff2-wasm.ts`,
|
||||
},
|
||||
{
|
||||
pkg: `../node_modules/harfbuzzjs`,
|
||||
src: `./wasm/hb-subset.wasm`,
|
||||
dest: `../packages/excalidraw/fonts/wasm/hb-subset-wasm.ts`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { pkg, src, dest } of wasmModules) {
|
||||
const packagePath = path.resolve(__dirname, pkg, "package.json");
|
||||
const licensePath = path.resolve(__dirname, pkg, "LICENSE");
|
||||
const sourcePath = path.resolve(__dirname, src);
|
||||
const destPath = path.resolve(__dirname, dest);
|
||||
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
license,
|
||||
authors,
|
||||
licenses,
|
||||
} = require(packagePath);
|
||||
|
||||
const licenseContent = fs.readFileSync(licensePath, "utf-8") || "";
|
||||
const base64 = fs.readFileSync(sourcePath, "base64");
|
||||
const content = `// GENERATED CODE -- DO NOT EDIT!
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/**
|
||||
* The following wasm module is generated with \`scripts/buildWasm.js\` and encoded as base64.
|
||||
*
|
||||
* The source of this content is taken from the package "${name}", which contains the following metadata:
|
||||
*
|
||||
* @author ${author || JSON.stringify(authors)}
|
||||
* @license ${license || JSON.stringify(licenses)}
|
||||
* @version ${version}
|
||||
|
||||
${licenseContent}
|
||||
*/
|
||||
|
||||
// faster atob alternative - https://github.com/evanw/esbuild/issues/1534#issuecomment-902738399
|
||||
const __toBinary = /* @__PURE__ */ (() => {
|
||||
const table = new Uint8Array(128);
|
||||
for (let i = 0; i < 64; i++)
|
||||
{table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;}
|
||||
return (base64) => {
|
||||
const n = base64.length; const bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0);
|
||||
for (let i2 = 0, j = 0; i2 < n; ) {
|
||||
const c0 = table[base64.charCodeAt(i2++)]; const c1 = table[base64.charCodeAt(i2++)];
|
||||
const c2 = table[base64.charCodeAt(i2++)]; const c3 = table[base64.charCodeAt(i2++)];
|
||||
bytes[j++] = c0 << 2 | c1 >> 4;
|
||||
bytes[j++] = c1 << 4 | c2 >> 2;
|
||||
bytes[j++] = c2 << 6 | c3;
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
})();
|
||||
|
||||
export default __toBinary(\`${base64}\`);
|
||||
`;
|
||||
|
||||
fs.writeFileSync(destPath, content);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
const fs = require("fs");
|
||||
|
||||
const THRESSHOLD = 85;
|
||||
|
||||
// we're using BCP 47 language tags as keys
|
||||
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
|
||||
|
||||
const crowdinMap = {
|
||||
"ar-SA": "en-ar",
|
||||
"bg-BG": "en-bg",
|
||||
"bn-BD": "en-bn",
|
||||
"ca-ES": "en-ca",
|
||||
"da-DK": "en-da",
|
||||
"de-DE": "en-de",
|
||||
"el-GR": "en-el",
|
||||
"es-ES": "en-es",
|
||||
"eu-ES": "en-eu",
|
||||
"fa-IR": "en-fa",
|
||||
"fi-FI": "en-fi",
|
||||
"fr-FR": "en-fr",
|
||||
"gl-ES": "en-gl",
|
||||
"he-IL": "en-he",
|
||||
"hi-IN": "en-hi",
|
||||
"hu-HU": "en-hu",
|
||||
"id-ID": "en-id",
|
||||
"it-IT": "en-it",
|
||||
"ja-JP": "en-ja",
|
||||
"kab-KAB": "en-kab",
|
||||
"ko-KR": "en-ko",
|
||||
"ku-TR": "en-ku",
|
||||
"my-MM": "en-my",
|
||||
"nb-NO": "en-nb",
|
||||
"nl-NL": "en-nl",
|
||||
"nn-NO": "en-nnno",
|
||||
"oc-FR": "en-oc",
|
||||
"pa-IN": "en-pain",
|
||||
"pl-PL": "en-pl",
|
||||
"pt-BR": "en-ptbr",
|
||||
"pt-PT": "en-pt",
|
||||
"ro-RO": "en-ro",
|
||||
"ru-RU": "en-ru",
|
||||
"si-LK": "en-silk",
|
||||
"sk-SK": "en-sk",
|
||||
"sl-SI": "en-sl",
|
||||
"sv-SE": "en-sv",
|
||||
"ta-IN": "en-ta",
|
||||
"tr-TR": "en-tr",
|
||||
"uk-UA": "en-uk",
|
||||
"zh-CN": "en-zhcn",
|
||||
"zh-HK": "en-zhhk",
|
||||
"zh-TW": "en-zhtw",
|
||||
"lt-LT": "en-lt",
|
||||
"lv-LV": "en-lv",
|
||||
"cs-CZ": "en-cs",
|
||||
"kk-KZ": "en-kk",
|
||||
"vi-VN": "en-vi",
|
||||
"mr-IN": "en-mr",
|
||||
"th-TH": "en-th",
|
||||
};
|
||||
|
||||
const flags = {
|
||||
"ar-SA": "🇸🇦",
|
||||
"bg-BG": "🇧🇬",
|
||||
"bn-BD": "🇧🇩",
|
||||
"ca-ES": "🏳",
|
||||
"cs-CZ": "🇨🇿",
|
||||
"da-DK": "🇩🇰",
|
||||
"de-DE": "🇩🇪",
|
||||
"el-GR": "🇬🇷",
|
||||
"es-ES": "🇪🇸",
|
||||
"fa-IR": "🇮🇷",
|
||||
"fi-FI": "🇫🇮",
|
||||
"fr-FR": "🇫🇷",
|
||||
"gl-ES": "🇪🇸",
|
||||
"he-IL": "🇮🇱",
|
||||
"hi-IN": "🇮🇳",
|
||||
"hu-HU": "🇭🇺",
|
||||
"id-ID": "🇮🇩",
|
||||
"it-IT": "🇮🇹",
|
||||
"ja-JP": "🇯🇵",
|
||||
"kab-KAB": "🏳",
|
||||
"kk-KZ": "🇰🇿",
|
||||
"ko-KR": "🇰🇷",
|
||||
"ku-TR": "🏳",
|
||||
"lt-LT": "🇱🇹",
|
||||
"lv-LV": "🇱🇻",
|
||||
"my-MM": "🇲🇲",
|
||||
"nb-NO": "🇳🇴",
|
||||
"nl-NL": "🇳🇱",
|
||||
"nn-NO": "🇳🇴",
|
||||
"oc-FR": "🏳",
|
||||
"pa-IN": "🇮🇳",
|
||||
"pl-PL": "🇵🇱",
|
||||
"pt-BR": "🇧🇷",
|
||||
"pt-PT": "🇵🇹",
|
||||
"ro-RO": "🇷🇴",
|
||||
"ru-RU": "🇷🇺",
|
||||
"si-LK": "🇱🇰",
|
||||
"sk-SK": "🇸🇰",
|
||||
"sl-SI": "🇸🇮",
|
||||
"sv-SE": "🇸🇪",
|
||||
"ta-IN": "🇮🇳",
|
||||
"tr-TR": "🇹🇷",
|
||||
"uk-UA": "🇺🇦",
|
||||
"zh-CN": "🇨🇳",
|
||||
"zh-HK": "🇭🇰",
|
||||
"zh-TW": "🇹🇼",
|
||||
"eu-ES": "🇪🇦",
|
||||
"vi-VN": "🇻🇳",
|
||||
"mr-IN": "🇮🇳",
|
||||
"th-TH": "🇹🇭",
|
||||
};
|
||||
|
||||
const languages = {
|
||||
"ar-SA": "العربية",
|
||||
"bg-BG": "Български",
|
||||
"bn-BD": "Bengali",
|
||||
"ca-ES": "Català",
|
||||
"cs-CZ": "Česky",
|
||||
"da-DK": "Dansk",
|
||||
"de-DE": "Deutsch",
|
||||
"el-GR": "Ελληνικά",
|
||||
"es-ES": "Español",
|
||||
"eu-ES": "Euskara",
|
||||
"fa-IR": "فارسی",
|
||||
"fi-FI": "Suomi",
|
||||
"fr-FR": "Français",
|
||||
"gl-ES": "Galego",
|
||||
"he-IL": "עברית",
|
||||
"hi-IN": "हिन्दी",
|
||||
"hu-HU": "Magyar",
|
||||
"id-ID": "Bahasa Indonesia",
|
||||
"it-IT": "Italiano",
|
||||
"ja-JP": "日本語",
|
||||
"kab-KAB": "Taqbaylit",
|
||||
"kk-KZ": "Қазақ тілі",
|
||||
"ko-KR": "한국어",
|
||||
"ku-TR": "Kurdî",
|
||||
"lt-LT": "Lietuvių",
|
||||
"lv-LV": "Latviešu",
|
||||
"my-MM": "Burmese",
|
||||
"nb-NO": "Norsk bokmål",
|
||||
"nl-NL": "Nederlands",
|
||||
"nn-NO": "Norsk nynorsk",
|
||||
"oc-FR": "Occitan",
|
||||
"pa-IN": "ਪੰਜਾਬੀ",
|
||||
"pl-PL": "Polski",
|
||||
"pt-BR": "Português Brasileiro",
|
||||
"pt-PT": "Português",
|
||||
"ro-RO": "Română",
|
||||
"ru-RU": "Русский",
|
||||
"si-LK": "සිංහල",
|
||||
"sk-SK": "Slovenčina",
|
||||
"sl-SI": "Slovenščina",
|
||||
"sv-SE": "Svenska",
|
||||
"ta-IN": "Tamil",
|
||||
"tr-TR": "Türkçe",
|
||||
"uk-UA": "Українська",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-HK": "繁體中文 (香港)",
|
||||
"zh-TW": "繁體中文",
|
||||
"vi-VN": "Tiếng Việt",
|
||||
"mr-IN": "मराठी",
|
||||
"th-TH": "ภาษาไทย",
|
||||
};
|
||||
|
||||
const percentages = fs.readFileSync(
|
||||
`${__dirname}/../packages/excalidraw/locales/percentages.json`,
|
||||
);
|
||||
const rowData = JSON.parse(percentages);
|
||||
|
||||
const coverages = Object.entries(rowData)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {});
|
||||
|
||||
const boldIf = (text, condition) => (condition ? `**${text}**` : text);
|
||||
|
||||
const printHeader = () => {
|
||||
let result = "| | Flag | Locale | % |\n";
|
||||
result += "| :--: | :--: | -- | :--: |";
|
||||
return result;
|
||||
};
|
||||
|
||||
const printRow = (id, locale, coverage) => {
|
||||
const isOver = coverage >= THRESSHOLD;
|
||||
let result = `| ${isOver ? id : "..."} | `;
|
||||
result += `${locale in flags ? flags[locale] : ""} | `;
|
||||
const language = locale in languages ? languages[locale] : locale;
|
||||
if (locale in crowdinMap && crowdinMap[locale]) {
|
||||
result += `[${boldIf(
|
||||
language,
|
||||
isOver,
|
||||
)}](https://crowdin.com/translate/excalidraw/10/${crowdinMap[locale]}) | `;
|
||||
} else {
|
||||
result += `${boldIf(language, isOver)} | `;
|
||||
}
|
||||
result += `${coverage === 100 ? "💯" : boldIf(coverage, isOver)} |`;
|
||||
return result;
|
||||
};
|
||||
|
||||
console.info(
|
||||
`Each language must be at least **${THRESSHOLD}%** translated in order to appear on Excalidraw. Join us on [Crowdin](https://crowdin.com/project/excalidraw) and help us translate your own language. **Can't find yours yet?** Open an [issue](https://github.com/excalidraw/excalidraw/issues/new) and we'll add it to the list.`,
|
||||
);
|
||||
console.info("\n\r");
|
||||
console.info(printHeader());
|
||||
let index = 1;
|
||||
for (const coverage in coverages) {
|
||||
if (coverage === "en") {
|
||||
continue;
|
||||
}
|
||||
console.info(printRow(index, coverage, coverages[coverage]));
|
||||
index++;
|
||||
}
|
||||
console.info("\n\r");
|
||||
console.info("\\* Languages in **bold** are going to appear on production.");
|
||||
@@ -0,0 +1,245 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const updateChangelog = require("./updateChangelog");
|
||||
|
||||
// skipping utils for now, as it has independent release process
|
||||
const PACKAGES = [
|
||||
"common",
|
||||
"fractional-indexing",
|
||||
"math",
|
||||
"element",
|
||||
"excalidraw",
|
||||
];
|
||||
const PACKAGES_DIR = path.resolve(__dirname, "../packages");
|
||||
|
||||
/**
|
||||
* Returns the arguments for the release script.
|
||||
*
|
||||
* Usage examples:
|
||||
* - yarn release --help -> prints this help message
|
||||
* - yarn release -> publishes `@excalidraw` packages with "test" tag and "-[hash]" version suffix
|
||||
* - yarn release --tag=test -> same as above
|
||||
* - yarn release --tag=next -> publishes `@excalidraw` packages with "next" tag and version "-[hash]" suffix
|
||||
* - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
|
||||
* - yarn release --tag=latest --version=0.19.0 -> publishes `@excalidraw` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
|
||||
*
|
||||
* @returns [tag, version, nonInteractive]
|
||||
*/
|
||||
const getArguments = () => {
|
||||
let tag = "test";
|
||||
let version = "";
|
||||
let nonInteractive = false;
|
||||
|
||||
for (const argument of process.argv.slice(2)) {
|
||||
if (/--help/.test(argument)) {
|
||||
console.info(`Available arguments:
|
||||
--tag=<tag> -> (optional) "test" (default), "next" for auto release, "latest" for stable release
|
||||
--version=<version> -> (optional) for "next" and "test", (required) for "latest" i.e. "0.19.0"
|
||||
--non-interactive -> (optional) disables interactive prompts`);
|
||||
|
||||
console.info(`\nUsage examples:
|
||||
- yarn release -> publishes \`@excalidraw\` packages with "test" tag and "-[hash]" version suffix
|
||||
- yarn release --tag=test -> same as above
|
||||
- yarn release --tag=next -> publishes \`@excalidraw\` packages with "next" tag and version "-[hash]" suffix
|
||||
- yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
|
||||
- yarn release --tag=latest --version=0.19.0 -> publishes \`@excalidraw\` packages with "latest" tag and version "0.19.0" & prepares changelog for the release`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (/--tag=/.test(argument)) {
|
||||
tag = argument.split("=")[1];
|
||||
}
|
||||
|
||||
if (/--version=/.test(argument)) {
|
||||
version = argument.split("=")[1];
|
||||
}
|
||||
|
||||
if (/--non-interactive/.test(argument)) {
|
||||
nonInteractive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag !== "latest" && tag !== "next" && tag !== "test") {
|
||||
console.error(`Unsupported tag "${tag}", use "latest", "next" or "test".`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (tag === "latest" && !version) {
|
||||
console.error("Pass the version to make the latest stable release!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
// set the next version based on the excalidraw package version + commit hash
|
||||
const excalidrawPackageVersion = require(getPackageJsonPath(
|
||||
"excalidraw",
|
||||
)).version;
|
||||
|
||||
const hash = getShortCommitHash();
|
||||
|
||||
if (!excalidrawPackageVersion.includes(hash)) {
|
||||
version = `${excalidrawPackageVersion}-${hash}`;
|
||||
} else {
|
||||
// ensuring idempotency
|
||||
version = excalidrawPackageVersion;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`Running with tag "${tag}" and version "${version}"...`);
|
||||
|
||||
return [tag, version, nonInteractive];
|
||||
};
|
||||
|
||||
const validatePackageName = (packageName) => {
|
||||
if (!PACKAGES.includes(packageName)) {
|
||||
console.error(`Package "${packageName}" not found!`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getPackageJsonPath = (packageName) => {
|
||||
validatePackageName(packageName);
|
||||
return path.resolve(PACKAGES_DIR, packageName, "package.json");
|
||||
};
|
||||
|
||||
const updatePackageJsons = (nextVersion) => {
|
||||
const packageJsons = new Map();
|
||||
|
||||
for (const packageName of PACKAGES) {
|
||||
const pkg = require(getPackageJsonPath(packageName));
|
||||
|
||||
pkg.version = nextVersion;
|
||||
|
||||
if (pkg.dependencies) {
|
||||
for (const dependencyName of PACKAGES) {
|
||||
if (!pkg.dependencies[`@excalidraw/${dependencyName}`]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pkg.dependencies[`@excalidraw/${dependencyName}`] = nextVersion;
|
||||
}
|
||||
}
|
||||
|
||||
packageJsons.set(packageName, `${JSON.stringify(pkg, null, 2)}\n`);
|
||||
}
|
||||
|
||||
// modify once, to avoid inconsistent state
|
||||
for (const packageName of PACKAGES) {
|
||||
const content = packageJsons.get(packageName);
|
||||
fs.writeFileSync(getPackageJsonPath(packageName), content, "utf-8");
|
||||
}
|
||||
};
|
||||
|
||||
const getShortCommitHash = () => {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
};
|
||||
|
||||
const askToCommit = (tag, nextVersion) => {
|
||||
if (tag !== "latest") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Would you like to commit these changes to git? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() === "y") {
|
||||
execSync(`git add -u`);
|
||||
execSync(
|
||||
`git commit -m "chore: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"Skipping commit. Don't forget to commit manually later!",
|
||||
);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const buildPackages = () => {
|
||||
console.info("Running yarn install...");
|
||||
execSync(`yarn --frozen-lockfile`, { stdio: "inherit" });
|
||||
|
||||
console.info("Removing existing build artifacts...");
|
||||
execSync(`yarn rm:build`, { stdio: "inherit" });
|
||||
|
||||
for (const packageName of PACKAGES) {
|
||||
console.info(`Building "@excalidraw/${packageName}"...`);
|
||||
execSync(`yarn run build:esm`, {
|
||||
cwd: path.resolve(PACKAGES_DIR, packageName),
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const askToPublish = (tag, version) => {
|
||||
return new Promise((resolve) => {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Would you like to publish these changes to npm? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() === "y") {
|
||||
publishPackages(tag, version);
|
||||
} else {
|
||||
console.info("Skipping publish.");
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const publishPackages = (tag, version) => {
|
||||
for (const packageName of PACKAGES) {
|
||||
execSync(`yarn publish --tag ${tag}`, {
|
||||
cwd: path.resolve(PACKAGES_DIR, packageName),
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Published "@excalidraw/${packageName}@${tag}" with version "${version}"! 🎉`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** main */
|
||||
(async () => {
|
||||
const [tag, version, nonInteractive] = getArguments();
|
||||
|
||||
buildPackages();
|
||||
|
||||
if (tag === "latest") {
|
||||
await updateChangelog(version);
|
||||
}
|
||||
|
||||
updatePackageJsons(version);
|
||||
|
||||
if (nonInteractive) {
|
||||
publishPackages(tag, version);
|
||||
} else {
|
||||
await askToCommit(tag, version);
|
||||
await askToPublish(tag, version);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,106 @@
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
|
||||
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
const pkg = require(excalidrawPackage);
|
||||
const lastVersion = pkg.version;
|
||||
const existingChangeLog = fs.readFileSync(
|
||||
`${excalidrawDir}/CHANGELOG.md`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const supportedTypes = ["feat", "fix", "style", "refactor", "perf", "build"];
|
||||
const headerForType = {
|
||||
feat: "Features",
|
||||
fix: "Fixes",
|
||||
style: "Styles",
|
||||
refactor: " Refactor",
|
||||
perf: "Performance",
|
||||
build: "Build",
|
||||
};
|
||||
|
||||
const badCommits = [];
|
||||
const getCommitHashForLastVersion = async () => {
|
||||
try {
|
||||
const commitMessage = `"release @excalidraw/excalidraw"`;
|
||||
const { stdout } = await exec(
|
||||
`git log --format=format:"%H" --grep=${commitMessage}`,
|
||||
);
|
||||
// take commit hash from latest release
|
||||
return stdout.split(/\r?\n/)[0];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getLibraryCommitsSinceLastRelease = async () => {
|
||||
const commitHash = await getCommitHashForLastVersion();
|
||||
const { stdout } = await exec(
|
||||
`git log --pretty=format:%s ${commitHash}...master`,
|
||||
);
|
||||
const commitsSinceLastRelease = stdout.split("\n");
|
||||
const commitList = {};
|
||||
supportedTypes.forEach((type) => {
|
||||
commitList[type] = [];
|
||||
});
|
||||
|
||||
commitsSinceLastRelease.forEach((commit) => {
|
||||
const indexOfColon = commit.indexOf(":");
|
||||
const type = commit.slice(0, indexOfColon);
|
||||
if (!supportedTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
|
||||
const messageWithCapitalizeFirst =
|
||||
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
|
||||
const prMatch = commit.match(/\(#([0-9]*)\)/);
|
||||
if (prMatch) {
|
||||
const prNumber = prMatch[1];
|
||||
|
||||
// return if the changelog already contains the pr number which would happen for package updates
|
||||
if (existingChangeLog.includes(prNumber)) {
|
||||
return;
|
||||
}
|
||||
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
|
||||
const messageWithPRLink = messageWithCapitalizeFirst.replace(
|
||||
/\(#[0-9]*\)/,
|
||||
prMarkdown,
|
||||
);
|
||||
commitList[type].push(messageWithPRLink);
|
||||
} else {
|
||||
badCommits.push(commit);
|
||||
commitList[type].push(messageWithCapitalizeFirst);
|
||||
}
|
||||
});
|
||||
console.info("Bad commits:", badCommits);
|
||||
return commitList;
|
||||
};
|
||||
|
||||
const updateChangelog = async (nextVersion) => {
|
||||
const commitList = await getLibraryCommitsSinceLastRelease();
|
||||
let changelogForLibrary =
|
||||
"## Excalidraw Library\n\n**_This section lists the updates made to the excalidraw library and will not affect the integration._**\n\n";
|
||||
supportedTypes.forEach((type) => {
|
||||
if (commitList[type].length) {
|
||||
changelogForLibrary += `### ${headerForType[type]}\n\n`;
|
||||
const commits = commitList[type];
|
||||
commits.forEach((commit) => {
|
||||
changelogForLibrary += `- ${commit}\n\n`;
|
||||
});
|
||||
}
|
||||
});
|
||||
changelogForLibrary += "---\n";
|
||||
const lastVersionIndex = existingChangeLog.indexOf(`## ${lastVersion}`);
|
||||
let updatedContent =
|
||||
existingChangeLog.slice(0, lastVersionIndex) +
|
||||
changelogForLibrary +
|
||||
existingChangeLog.slice(lastVersionIndex);
|
||||
const currentDate = new Date().toISOString().slice(0, 10);
|
||||
const newVersion = `## ${nextVersion} (${currentDate})`;
|
||||
updatedContent = updatedContent.replace(`## Unreleased`, newVersion);
|
||||
fs.writeFileSync(`${excalidrawDir}/CHANGELOG.md`, updatedContent, "utf8");
|
||||
};
|
||||
|
||||
module.exports = updateChangelog;
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,252 @@
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { Font } = require("fonteditor-core");
|
||||
const wawoff = require("wawoff2");
|
||||
const which = require("which");
|
||||
|
||||
/**
|
||||
* Custom esbuild plugin to:
|
||||
* 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
|
||||
* 2. convert all the imported fonts (including those from cdn) at build time into .ttf (since Resvg does not support woff2, neither inlined dataurls - https://github.com/RazrFalcon/resvg/issues/541)
|
||||
* - merging multiple woff2 into one ttf (for same families with different unicode ranges)
|
||||
* - deduplicating glyphs due to the merge process
|
||||
* - merging fallback font for each
|
||||
* - printing out font metrics
|
||||
*
|
||||
* @returns {import("esbuild").Plugin}
|
||||
*/
|
||||
module.exports.woff2ServerPlugin = (options = {}) => {
|
||||
return {
|
||||
name: "woff2ServerPlugin",
|
||||
setup(build) {
|
||||
const fonts = new Map();
|
||||
|
||||
build.onResolve({ filter: /\.woff2$/ }, (args) => {
|
||||
const resolvedPath = path.resolve(args.resolveDir, args.path);
|
||||
|
||||
return {
|
||||
path: resolvedPath,
|
||||
namespace: "woff2ServerPlugin",
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad(
|
||||
{ filter: /.*/, namespace: "woff2ServerPlugin" },
|
||||
async (args) => {
|
||||
let woff2Buffer;
|
||||
|
||||
if (path.isAbsolute(args.path)) {
|
||||
// read local woff2 as a buffer (WARN: `readFileSync` does not work!)
|
||||
woff2Buffer = await fs.promises.readFile(args.path);
|
||||
} else {
|
||||
throw new Error(`Font path has to be absolute! "${args.path}"`);
|
||||
}
|
||||
|
||||
// google's brotli decompression into snft
|
||||
const snftBuffer = new Uint8Array(
|
||||
await wawoff.decompress(woff2Buffer),
|
||||
).buffer;
|
||||
|
||||
// load font and store per fontfamily & subfamily cache
|
||||
let font;
|
||||
|
||||
try {
|
||||
font = Font.create(snftBuffer, {
|
||||
type: "ttf",
|
||||
hinting: true,
|
||||
kerning: true,
|
||||
});
|
||||
} catch {
|
||||
// if loading as ttf fails, try to load as otf
|
||||
font = Font.create(snftBuffer, {
|
||||
type: "otf",
|
||||
hinting: true,
|
||||
kerning: true,
|
||||
});
|
||||
}
|
||||
|
||||
const fontFamily = font.data.name.fontFamily;
|
||||
const subFamily = font.data.name.fontSubFamily;
|
||||
|
||||
if (!fonts.get(fontFamily)) {
|
||||
fonts.set(fontFamily, {});
|
||||
}
|
||||
|
||||
if (!fonts.get(fontFamily)[subFamily]) {
|
||||
fonts.get(fontFamily)[subFamily] = [];
|
||||
}
|
||||
|
||||
// store the snftbuffer per subfamily
|
||||
fonts.get(fontFamily)[subFamily].push(font);
|
||||
|
||||
// inline the woff2 as base64 for server-side use cases
|
||||
// NOTE: "file" loader is broken in commonjs and "dataurl" loader does not produce correct ur
|
||||
return {
|
||||
contents: `data:font/woff2;base64,${woff2Buffer.toString(
|
||||
"base64",
|
||||
)}`,
|
||||
loader: "text",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
build.onEnd(async () => {
|
||||
const { outdir } = options;
|
||||
|
||||
if (!outdir) {
|
||||
return;
|
||||
}
|
||||
const outputDir = path.resolve(outdir);
|
||||
|
||||
const isFontToolsInstalled = await which("fonttools", {
|
||||
nothrow: true,
|
||||
});
|
||||
if (!isFontToolsInstalled) {
|
||||
console.error(
|
||||
`Skipped TTF generation: install "fonttools" first in order to generate TTF fonts!\nhttps://github.com/fonttools/fonttools`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const xiaolaiPath = path.resolve(
|
||||
__dirname,
|
||||
"./assets/Xiaolai-Regular.ttf",
|
||||
);
|
||||
const emojiPath = path.resolve(
|
||||
__dirname,
|
||||
"./assets/NotoEmoji-Regular.ttf",
|
||||
);
|
||||
|
||||
// need to use the same em size as built-in fonts, otherwise pyftmerge throws (modified manually with font forge)
|
||||
const emojiPath_2048 = path.resolve(
|
||||
__dirname,
|
||||
"./assets/NotoEmoji-Regular-2048.ttf",
|
||||
);
|
||||
|
||||
const liberationPath = path.resolve(
|
||||
__dirname,
|
||||
"./assets/LiberationSans-Regular.ttf",
|
||||
);
|
||||
|
||||
// need to use the same em size as built-in fonts, otherwise pyftmerge throws (modified manually with font forge)
|
||||
const liberationPath_2048 = path.resolve(
|
||||
__dirname,
|
||||
"./assets/LiberationSans-Regular-2048.ttf",
|
||||
);
|
||||
|
||||
const xiaolaiFont = Font.create(fs.readFileSync(xiaolaiPath), {
|
||||
type: "ttf",
|
||||
});
|
||||
const emojiFont = Font.create(fs.readFileSync(emojiPath), {
|
||||
type: "ttf",
|
||||
});
|
||||
|
||||
const liberationFont = Font.create(fs.readFileSync(liberationPath), {
|
||||
type: "ttf",
|
||||
});
|
||||
|
||||
const sortedFonts = Array.from(fonts.entries()).sort(
|
||||
([family1], [family2]) => (family1 > family2 ? 1 : -1),
|
||||
);
|
||||
|
||||
// for now we are interested in the regular families only
|
||||
for (const [family, { Regular }] of sortedFonts) {
|
||||
if (family.includes("Xiaolai")) {
|
||||
// don't generate ttf for Xiaolai, as we have it hardcoded as one ttf
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseFont = Regular[0];
|
||||
const tempPaths = Regular.map((_, index) =>
|
||||
path.resolve(outputDir, `temp_${family}_${index}.ttf`),
|
||||
);
|
||||
|
||||
for (const [index, font] of Regular.entries()) {
|
||||
// tempFileNames
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// write down the buffer
|
||||
fs.writeFileSync(tempPaths[index], font.write({ type: "ttf" }));
|
||||
}
|
||||
|
||||
const mergedFontPath = path.resolve(outputDir, `${family}.ttf`);
|
||||
|
||||
const fallbackFontsPaths = [];
|
||||
const shouldIncludeXiaolaiFallback = family.includes("Excalifont");
|
||||
|
||||
if (shouldIncludeXiaolaiFallback) {
|
||||
fallbackFontsPaths.push(xiaolaiPath);
|
||||
}
|
||||
|
||||
// add liberation as fallback to all fonts, so that unknown characters are rendered similarly to how browser renders them (Helvetica, Arial, etc.)
|
||||
if (baseFont.data.head.unitsPerEm === 2048) {
|
||||
fallbackFontsPaths.push(emojiPath_2048, liberationPath_2048);
|
||||
} else {
|
||||
fallbackFontsPaths.push(emojiPath, liberationPath);
|
||||
}
|
||||
|
||||
// drop Vertical related metrics, otherwise it does not allow us to merge the fonts
|
||||
// vhea (Vertical Header Table)
|
||||
// vmtx (Vertical Metrics Table)
|
||||
execSync(
|
||||
`pyftmerge --drop-tables=vhea,vmtx --output-file="${mergedFontPath}" "${tempPaths.join(
|
||||
'" "',
|
||||
)}" "${fallbackFontsPaths.join('" "')}"`,
|
||||
);
|
||||
|
||||
// cleanup
|
||||
for (const path of tempPaths) {
|
||||
fs.rmSync(path);
|
||||
}
|
||||
|
||||
// yeah, we need to read the font again (:
|
||||
const mergedFont = Font.create(fs.readFileSync(mergedFontPath), {
|
||||
type: "ttf",
|
||||
kerning: true,
|
||||
hinting: true,
|
||||
});
|
||||
|
||||
const getNameField = (field) => {
|
||||
const base = baseFont.data.name[field];
|
||||
const xiaolai = xiaolaiFont.data.name[field];
|
||||
const emoji = emojiFont.data.name[field];
|
||||
const liberation = liberationFont.data.name[field];
|
||||
// liberation font
|
||||
|
||||
return shouldIncludeXiaolaiFallback
|
||||
? `${base} & ${xiaolai} & ${emoji} & ${liberation}`
|
||||
: `${base} & ${emoji} & ${liberation}`;
|
||||
};
|
||||
|
||||
mergedFont.set({
|
||||
...mergedFont.data,
|
||||
name: {
|
||||
...mergedFont.data.name,
|
||||
copyright: getNameField("copyright"),
|
||||
licence: getNameField("licence"),
|
||||
},
|
||||
});
|
||||
|
||||
fs.rmSync(mergedFontPath);
|
||||
fs.writeFileSync(mergedFontPath, mergedFont.write({ type: "ttf" }));
|
||||
|
||||
const { ascent, descent } = baseFont.data.hhea;
|
||||
console.info(`Generated "${family}"`);
|
||||
if (Regular.length > 1) {
|
||||
console.info(
|
||||
`- by merging ${Regular.length} woff2 fonts and related fallback fonts`,
|
||||
);
|
||||
}
|
||||
console.info(
|
||||
`- with metrics ${baseFont.data.head.unitsPerEm}, ${ascent}, ${descent}`,
|
||||
);
|
||||
console.info(``);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
// define `EXCALIDRAW_ASSET_PATH` as a SSOT
|
||||
const OSS_FONTS_CDN = "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/oss/";
|
||||
const OSS_FONTS_FALLBACK = "/";
|
||||
|
||||
/**
|
||||
* Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`.
|
||||
*
|
||||
* @returns {import("vite").PluginOption}
|
||||
*/
|
||||
module.exports.woff2BrowserPlugin = () => {
|
||||
let isDev;
|
||||
|
||||
return {
|
||||
name: "woff2BrowserPlugin",
|
||||
enforce: "pre",
|
||||
config(_, { command }) {
|
||||
isDev = command === "serve";
|
||||
},
|
||||
transform(code, id) {
|
||||
// using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically),
|
||||
// but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app`
|
||||
if (!isDev && id.endsWith("/excalidraw/fonts/fonts.css")) {
|
||||
return `/* WARN: The following content is generated during excalidraw-app build */
|
||||
|
||||
@font-face {
|
||||
font-family: "Assistant";
|
||||
src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Regular.woff2)
|
||||
format("woff2"),
|
||||
url(./Assistant-Regular.woff2) format("woff2");
|
||||
font-weight: 400;
|
||||
style: normal;
|
||||
display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Assistant";
|
||||
src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Medium.woff2)
|
||||
format("woff2"),
|
||||
url(./Assistant-Medium.woff2) format("woff2");
|
||||
font-weight: 500;
|
||||
style: normal;
|
||||
display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Assistant";
|
||||
src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-SemiBold.woff2)
|
||||
format("woff2"),
|
||||
url(./Assistant-SemiBold.woff2) format("woff2");
|
||||
font-weight: 600;
|
||||
style: normal;
|
||||
display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Assistant";
|
||||
src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Bold.woff2)
|
||||
format("woff2"),
|
||||
url(./Assistant-Bold.woff2) format("woff2");
|
||||
font-weight: 700;
|
||||
style: normal;
|
||||
display: swap;
|
||||
}`;
|
||||
}
|
||||
|
||||
if (!isDev && id.endsWith("excalidraw-app/index.html")) {
|
||||
return code.replace(
|
||||
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
|
||||
`<script>
|
||||
// point into our CDN in prod, fallback to root (excalidraw.com) domain in case of issues
|
||||
window.EXCALIDRAW_ASSET_PATH = [
|
||||
"${OSS_FONTS_CDN}",
|
||||
"${OSS_FONTS_FALLBACK}",
|
||||
];
|
||||
</script>
|
||||
|
||||
<!-- Preload all default fonts to avoid swap on init -->
|
||||
<link
|
||||
rel="preload"
|
||||
href="${OSS_FONTS_CDN}fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||
<link
|
||||
rel="preload"
|
||||
href="${OSS_FONTS_CDN}fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="${OSS_FONTS_CDN}fonts/Assistant/Assistant-SemiBold.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="${OSS_FONTS_CDN}fonts/ComicShanns/ComicShanns-Regular-279a7b317d12eb88de06167bd672b4b4.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user