mirror of
https://github.com/penpot/penpot.git
synced 2025-01-08 07:50:43 -05:00
607deb31dc
Mainly leave shadow-cljs for build cljs stuff and use esbuild for bundle all js dependencies, completly avoiding all possible incompatibility issues between js libraries and google closure compiler.
569 lines
14 KiB
JavaScript
569 lines
14 KiB
JavaScript
import proc from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import ph from "node:path";
|
|
import os from "node:os";
|
|
import url from "node:url";
|
|
|
|
import * as marked from "marked";
|
|
import SVGSpriter from "svg-sprite";
|
|
import Watcher from "watcher";
|
|
import gettext from "gettext-parser";
|
|
import l from "lodash";
|
|
import log from "fancy-log";
|
|
import mustache from "mustache";
|
|
import pLimit from "p-limit";
|
|
import ppt from "pretty-time";
|
|
import wpool from "workerpool";
|
|
|
|
function getCoreCount() {
|
|
return os.cpus().length;
|
|
}
|
|
|
|
export const dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
|
|
|
export function startWorker() {
|
|
return wpool.pool(dirname + "/_worker.js", {
|
|
maxWorkers: getCoreCount(),
|
|
});
|
|
}
|
|
|
|
export const isDebug = process.env.NODE_ENV !== "production";
|
|
|
|
async function findFiles(basePath, predicate, options = {}) {
|
|
predicate =
|
|
predicate ??
|
|
function () {
|
|
return true;
|
|
};
|
|
|
|
let files = await fs.readdir(basePath, {
|
|
recursive: options.recursive ?? false,
|
|
});
|
|
files = files.map((path) => ph.join(basePath, path));
|
|
|
|
return files;
|
|
}
|
|
|
|
function syncDirs(originPath, destPath) {
|
|
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
proc.exec(command, (cause, stdout) => {
|
|
if (cause) {
|
|
reject(cause);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
export function isSassFile(path) {
|
|
return path.endsWith(".scss");
|
|
}
|
|
|
|
export function isSvgFile(path) {
|
|
return path.endsWith(".svg");
|
|
}
|
|
|
|
export function isJsFile(path) {
|
|
return path.endsWith(".js");
|
|
}
|
|
|
|
export async function compileSass(worker, path, options) {
|
|
path = ph.resolve(path);
|
|
|
|
log.info("compile:", path);
|
|
return worker.exec("compileSass", [path, options]);
|
|
}
|
|
|
|
export async function compileSassDebug(worker) {
|
|
const result = await compileSass(worker, "resources/styles/debug.scss", {});
|
|
return `${result.css}\n`;
|
|
}
|
|
|
|
export async function compileSassStorybook(worker) {
|
|
const limitFn = pLimit(4);
|
|
const sourceDir = ph.join("src", "app", "main", "ui", "ds");
|
|
|
|
const dsFiles = (await fs.readdir(sourceDir, { recursive: true }))
|
|
.filter(isSassFile)
|
|
.map((filename) => ph.join(sourceDir, filename));
|
|
const procs = [compileSass(worker, "resources/styles/main-default.scss", {})];
|
|
|
|
for (let path of dsFiles) {
|
|
const proc = limitFn(() => compileSass(worker, path, { modules: true }));
|
|
procs.push(proc);
|
|
}
|
|
|
|
const result = await Promise.all(procs);
|
|
return result.reduce(
|
|
(acc, item) => {
|
|
acc.index[item.outputPath] = item.css;
|
|
acc.items.push(item.outputPath);
|
|
return acc;
|
|
},
|
|
{ index: {}, items: [] },
|
|
);
|
|
}
|
|
|
|
export async function compileSassAll(worker) {
|
|
const limitFn = pLimit(4);
|
|
const sourceDir = "src";
|
|
|
|
const isDesignSystemFile = (path) => {
|
|
return path.startsWith("app/main/ui/ds/");
|
|
};
|
|
|
|
const isOldComponentSystemFile = (path) => {
|
|
return path.startsWith("app/main/ui/components/");
|
|
};
|
|
|
|
let files = (await fs.readdir(sourceDir, { recursive: true })).filter(
|
|
isSassFile,
|
|
);
|
|
|
|
const appFiles = files
|
|
.filter((path) => !isDesignSystemFile(path))
|
|
.filter((path) => !isOldComponentSystemFile(path))
|
|
.map((path) => ph.join(sourceDir, path));
|
|
|
|
const dsFiles = files
|
|
.filter(isDesignSystemFile)
|
|
.map((path) => ph.join(sourceDir, path));
|
|
|
|
const oldComponentsFiles = files
|
|
.filter(isOldComponentSystemFile)
|
|
.map((path) => ph.join(sourceDir, path));
|
|
|
|
const procs = [compileSass(worker, "resources/styles/main-default.scss", {})];
|
|
|
|
for (let path of [...oldComponentsFiles, ...dsFiles, ...appFiles]) {
|
|
const proc = limitFn(() => compileSass(worker, path, { modules: true }));
|
|
procs.push(proc);
|
|
}
|
|
|
|
const result = await Promise.all(procs);
|
|
|
|
return result.reduce(
|
|
(acc, item) => {
|
|
acc.index[item.outputPath] = item.css;
|
|
acc.items.push(item.outputPath);
|
|
return acc;
|
|
},
|
|
{ index: {}, items: [] },
|
|
);
|
|
}
|
|
|
|
export function concatSass(data) {
|
|
const output = [];
|
|
|
|
for (let path of data.items) {
|
|
output.push(data.index[path]);
|
|
}
|
|
|
|
return output.join("\n");
|
|
}
|
|
|
|
export async function watch(baseDir, predicate, callback) {
|
|
predicate = predicate ?? (() => true);
|
|
|
|
const watcher = new Watcher(baseDir, {
|
|
persistent: true,
|
|
recursive: true,
|
|
});
|
|
|
|
watcher.on("change", (path) => {
|
|
if (predicate(path)) {
|
|
callback(path);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function readManifestFile(path) {
|
|
const manifestPath = "resources/public/js/manifest.json";
|
|
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
|
return JSON.parse(content);
|
|
}
|
|
|
|
async function readShadowManifest() {
|
|
const ts = Date.now();
|
|
try {
|
|
const content1 = await readManifestFile(
|
|
"resources/public/js/manifest.json",
|
|
);
|
|
const content2 = await readManifestFile(
|
|
"resources/public/js/worker/manifest.json",
|
|
);
|
|
|
|
const index = {
|
|
ts: ts,
|
|
config: "js/config.js?ts=" + ts,
|
|
polyfills: "js/polyfills.js?ts=" + ts,
|
|
};
|
|
|
|
for (let item of content1) {
|
|
index[item.name] = "js/" + item["output-name"];
|
|
}
|
|
|
|
for (let item of content2) {
|
|
index["worker_" + item.name] = "js/worker/" + item["output-name"];
|
|
}
|
|
|
|
return index;
|
|
} catch (cause) {
|
|
return {
|
|
ts: ts,
|
|
config: "js/config.js?ts=" + ts,
|
|
polyfills: "js/polyfills.js?ts=" + ts,
|
|
main: "js/main.js?ts=" + ts,
|
|
shared: "js/shared.js?ts=" + ts,
|
|
worker_main: "js/worker/main.js?ts=" + ts,
|
|
rasterizer: "js/rasterizer.js?ts=" + ts,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function renderTemplate(path, context = {}, partials = {}) {
|
|
const content = await fs.readFile(path, { encoding: "utf-8" });
|
|
|
|
const ts = Math.floor(new Date());
|
|
|
|
context = Object.assign({}, context, {
|
|
ts: ts,
|
|
isDebug,
|
|
});
|
|
|
|
return mustache.render(content, context, partials);
|
|
}
|
|
|
|
const renderer = {
|
|
link(href, title, text) {
|
|
return `<a href="${href}" target="_blank">${text}</a>`;
|
|
},
|
|
};
|
|
|
|
marked.use({ renderer });
|
|
|
|
async function readTranslations() {
|
|
const langs = [
|
|
"ar",
|
|
"ca",
|
|
"de",
|
|
"el",
|
|
"en",
|
|
"eu",
|
|
"it",
|
|
"es",
|
|
"fa",
|
|
"fr",
|
|
"he",
|
|
"nb_NO",
|
|
"pl",
|
|
"pt_BR",
|
|
"ro",
|
|
"id",
|
|
"ru",
|
|
"tr",
|
|
"zh_CN",
|
|
"zh_Hant",
|
|
"hr",
|
|
"gl",
|
|
"pt_PT",
|
|
"cs",
|
|
"fo",
|
|
"ko",
|
|
"lv",
|
|
"nl",
|
|
// this happens when file does not matches correct
|
|
// iso code for the language.
|
|
["ja_jp", "jpn_JP"],
|
|
["uk", "ukr_UA"],
|
|
"ha",
|
|
];
|
|
const result = {};
|
|
|
|
for (let lang of langs) {
|
|
let filename = `${lang}.po`;
|
|
if (l.isArray(lang)) {
|
|
filename = `${lang[1]}.po`;
|
|
lang = lang[0];
|
|
}
|
|
|
|
const content = await fs.readFile(`./translations/${filename}`, {
|
|
encoding: "utf-8",
|
|
});
|
|
|
|
lang = lang.toLowerCase();
|
|
|
|
const data = gettext.po.parse(content, "utf-8");
|
|
const trdata = data.translations[""];
|
|
|
|
for (let key of Object.keys(trdata)) {
|
|
if (key === "") continue;
|
|
const comments = trdata[key].comments || {};
|
|
|
|
if (l.isNil(result[key])) {
|
|
result[key] = {};
|
|
}
|
|
|
|
const isMarkdown = l.includes(comments.flag, "markdown");
|
|
|
|
const msgs = trdata[key].msgstr;
|
|
if (msgs.length === 1) {
|
|
let message = msgs[0];
|
|
if (isMarkdown) {
|
|
message = marked.parseInline(message);
|
|
}
|
|
|
|
result[key][lang] = message;
|
|
} else {
|
|
result[key][lang] = msgs.map((item) => {
|
|
if (isMarkdown) {
|
|
return marked.parseInline(item);
|
|
} else {
|
|
return item;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function filterTranslations(translations, langs = [], keyFilter) {
|
|
const filteredEntries = Object.entries(translations)
|
|
.filter(([translationKey, _]) => keyFilter(translationKey))
|
|
.map(([translationKey, value]) => {
|
|
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
|
langs.includes(lang),
|
|
);
|
|
return [translationKey, Object.fromEntries(langEntries)];
|
|
});
|
|
|
|
return Object.fromEntries(filteredEntries);
|
|
}
|
|
|
|
async function generateSvgSprite(files, prefix) {
|
|
const spriter = new SVGSpriter({
|
|
mode: {
|
|
symbol: { inline: true },
|
|
},
|
|
});
|
|
|
|
for (let path of files) {
|
|
const name = `${prefix}${ph.basename(path)}`;
|
|
const content = await fs.readFile(path, { encoding: "utf-8" });
|
|
spriter.add(name, name, content);
|
|
}
|
|
|
|
const { result } = await spriter.compileAsync();
|
|
const resource = result.symbol.sprite;
|
|
return resource.contents;
|
|
}
|
|
|
|
async function generateSvgSprites() {
|
|
await fs.mkdir("resources/public/images/sprites/symbol/", {
|
|
recursive: true,
|
|
});
|
|
|
|
const icons = await findFiles("resources/images/icons/", isSvgFile);
|
|
const iconsSprite = await generateSvgSprite(icons, "icon-");
|
|
await fs.writeFile(
|
|
"resources/public/images/sprites/symbol/icons.svg",
|
|
iconsSprite,
|
|
);
|
|
|
|
const cursors = await findFiles("resources/images/cursors/", isSvgFile);
|
|
const cursorsSprite = await generateSvgSprite(cursors, "cursor-");
|
|
await fs.writeFile(
|
|
"resources/public/images/sprites/symbol/cursors.svg",
|
|
cursorsSprite,
|
|
);
|
|
|
|
const assets = await findFiles("resources/images/assets/", isSvgFile);
|
|
const assetsSprite = await generateSvgSprite(assets, "asset-");
|
|
await fs.writeFile(
|
|
"resources/public/images/sprites/assets.svg",
|
|
assetsSprite,
|
|
);
|
|
}
|
|
|
|
async function generateTemplates() {
|
|
const isDebug = process.env.NODE_ENV !== "production";
|
|
await fs.mkdir("./resources/public/", { recursive: true });
|
|
|
|
let translations = await readTranslations();
|
|
const storybookTranslations = JSON.stringify(
|
|
filterTranslations(translations, ["en"], (key) =>
|
|
key.startsWith("labels."),
|
|
),
|
|
);
|
|
translations = JSON.stringify(translations);
|
|
|
|
const manifest = await readShadowManifest();
|
|
let content;
|
|
|
|
const iconsSprite = await fs.readFile(
|
|
"resources/public/images/sprites/symbol/icons.svg",
|
|
"utf8",
|
|
);
|
|
const cursorsSprite = await fs.readFile(
|
|
"resources/public/images/sprites/symbol/cursors.svg",
|
|
"utf8",
|
|
);
|
|
const assetsSprite = await fs.readFile(
|
|
"resources/public/images/sprites/assets.svg",
|
|
"utf-8",
|
|
);
|
|
const partials = {
|
|
"../public/images/sprites/symbol/icons.svg": iconsSprite,
|
|
"../public/images/sprites/symbol/cursors.svg": cursorsSprite,
|
|
"../public/images/sprites/assets.svg": assetsSprite,
|
|
};
|
|
|
|
const pluginRuntimeUri =
|
|
process.env.PENPOT_PLUGIN_DEV === "true"
|
|
? "http://localhost:4200/index.js?ts=" + manifest.ts
|
|
: "plugins-runtime/index.js?ts=" + manifest.ts;
|
|
|
|
content = await renderTemplate(
|
|
"resources/templates/index.mustache",
|
|
{
|
|
manifest: manifest,
|
|
translations: JSON.stringify(translations),
|
|
pluginRuntimeUri,
|
|
isDebug,
|
|
},
|
|
partials,
|
|
);
|
|
|
|
await fs.writeFile("./resources/public/index.html", content);
|
|
|
|
content = await renderTemplate(
|
|
"resources/templates/challenge.mustache",
|
|
{},
|
|
partials,
|
|
);
|
|
await fs.writeFile("./resources/public/challenge.html", content);
|
|
|
|
content = await renderTemplate(
|
|
"resources/templates/preview-body.mustache",
|
|
{
|
|
manifest: manifest,
|
|
},
|
|
partials,
|
|
);
|
|
await fs.writeFile("./.storybook/preview-body.html", content);
|
|
|
|
content = await renderTemplate(
|
|
"resources/templates/preview-head.mustache",
|
|
{
|
|
manifest: manifest,
|
|
translations: JSON.stringify(storybookTranslations),
|
|
},
|
|
partials,
|
|
);
|
|
await fs.writeFile("./.storybook/preview-head.html", content);
|
|
|
|
content = await renderTemplate("resources/templates/render.mustache", {
|
|
manifest: manifest,
|
|
translations: JSON.stringify(translations),
|
|
});
|
|
|
|
await fs.writeFile("./resources/public/render.html", content);
|
|
|
|
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
|
manifest: manifest,
|
|
translations: JSON.stringify(translations),
|
|
});
|
|
|
|
await fs.writeFile("./resources/public/rasterizer.html", content);
|
|
}
|
|
|
|
export async function compileStorybookStyles() {
|
|
const worker = startWorker();
|
|
const start = process.hrtime();
|
|
|
|
log.info("init: compile storybook styles");
|
|
let result = await compileSassStorybook(worker);
|
|
result = concatSass(result);
|
|
|
|
await fs.mkdir("./resources/public/css", { recursive: true });
|
|
await fs.writeFile("./resources/public/css/ds.css", result);
|
|
|
|
const end = process.hrtime(start);
|
|
log.info("done: compile storybook styles", `(${ppt(end)})`);
|
|
worker.terminate();
|
|
}
|
|
|
|
export async function compileStyles() {
|
|
const worker = startWorker();
|
|
const start = process.hrtime();
|
|
|
|
log.info("init: compile styles");
|
|
let result = await compileSassAll(worker);
|
|
result = concatSass(result);
|
|
|
|
await fs.mkdir("./resources/public/css", { recursive: true });
|
|
await fs.writeFile("./resources/public/css/main.css", result);
|
|
|
|
if (isDebug) {
|
|
let debugCSS = await compileSassDebug(worker);
|
|
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
|
}
|
|
|
|
const end = process.hrtime(start);
|
|
log.info("done: compile styles", `(${ppt(end)})`);
|
|
worker.terminate();
|
|
}
|
|
|
|
export async function compileSvgSprites() {
|
|
const start = process.hrtime();
|
|
log.info("init: compile svgsprite");
|
|
await generateSvgSprites();
|
|
const end = process.hrtime(start);
|
|
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
|
}
|
|
|
|
export async function compileTemplates() {
|
|
const start = process.hrtime();
|
|
log.info("init: compile templates");
|
|
await generateTemplates();
|
|
const end = process.hrtime(start);
|
|
log.info("done: compile templates", `(${ppt(end)})`);
|
|
}
|
|
|
|
export async function compilePolyfills() {
|
|
const start = process.hrtime();
|
|
log.info("init: compile polyfills");
|
|
|
|
const files = await findFiles("resources/polyfills/", isJsFile);
|
|
let result = [];
|
|
for (let path of files) {
|
|
const content = await fs.readFile(path, { encoding: "utf-8" });
|
|
result.push(content);
|
|
}
|
|
|
|
await fs.mkdir("./resources/public/js", { recursive: true });
|
|
fs.writeFile("resources/public/js/polyfills.js", result.join("\n"));
|
|
|
|
const end = process.hrtime(start);
|
|
log.info("done: compile polyfills", `(${ppt(end)})`);
|
|
}
|
|
|
|
export async function copyAssets() {
|
|
const start = process.hrtime();
|
|
log.info("init: copy assets");
|
|
|
|
await syncDirs("resources/images/", "resources/public/images/");
|
|
await syncDirs("resources/fonts/", "resources/public/fonts/");
|
|
await syncDirs(
|
|
"resources/plugins-runtime/",
|
|
"resources/public/plugins-runtime/",
|
|
);
|
|
|
|
const end = process.hrtime(start);
|
|
log.info("done: copy assets", `(${ppt(end)})`);
|
|
}
|