0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00
penpot/frontend/scripts/_helpers.js
Andrey Antukh e1c9691567 Improve scss compilation error handling
Don't stop watch scss process on compilation error
2024-11-25 12:44:10 +01:00

575 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 extension = {
useNewRenderer: true,
renderer: {
link(token) {
const href = token.href;
const text = token.text;
return `<a href="${href}" target="_blank">${text}</a>`;
},
},
};
marked.use(extension);
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)})`);
}