/** * Copyright 2013-2019 GenieACS Inc. * * This file is part of GenieACS. * * GenieACS is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * GenieACS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ import * as path from "path"; import * as fs from "fs"; import { rollup, WarningHandler } from "rollup"; import rollupJson from "@rollup/plugin-json"; import typescript from "@rollup/plugin-typescript"; import { terser } from "rollup-plugin-terser"; import nodeResolve from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import postcss from "postcss"; import postcssImport from "postcss-import"; import postcssPresetEnv from "postcss-preset-env"; import postcssColorMod from "postcss-color-mod-function"; import cssnano from "cssnano"; import { optimize } from "svgo"; import * as xmlParser from "../lib/xml-parser"; const MODE = process.env["NODE_ENV"] || "production"; const BUILD_METADATA = new Date() .toISOString() .split(".")[0] .replace(/[^0-9]/g, ""); const INPUT_DIR = path.resolve(__dirname, ".."); const OUTPUT_DIR = path.resolve(__dirname, "../dist"); const builtins = [ "path", "fs", "cluster", "os", "tls", "http", "https", "zlib", "crypto", "util", "vm", "querystring", "child_process", "dgram", "url", "readline", "stream", "bson", // not a built-in but not a direct dependency either ]; function rmDirSync(dirPath): void { if (!fs.existsSync(dirPath)) return; const files = fs.readdirSync(dirPath); for (const file of files) { const filePath = `${dirPath}/${file}`; if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); else rmDirSync(filePath); } fs.rmdirSync(dirPath); } // For lockfileVersion = 1 function stripDevDeps(deps): void { if (!deps["dependencies"]) return; for (const [k, v] of Object.entries(deps["dependencies"])) { if (v["dev"]) delete deps["dependencies"][k]; else stripDevDeps(v); } if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"]; } // For lockfileVersion = 2 function stripDevDeps2(deps): void { if (!deps["packages"]) return; for (const [k, v] of Object.entries(deps["packages"])) { delete v["devDependencies"]; if (v["dev"]) delete deps["packages"][k]; } } function xmlTostring(xml): string { const children = []; for (const c of xml.children || []) children.push(xmlTostring(c)); return xml.name === "root" && xml.bodyIndex === 0 ? children.join("") : `<${xml.name} ${xml.attrs}>${children.join("")}`; } function generateSymbol(id: string, svgStr: string): string { const xml = xmlParser.parseXml(svgStr); const svg = xml.children[0]; const svgAttrs = xmlParser.parseAttrs(svg.attrs); let viewBox = ""; for (const a of svgAttrs) { if (a.name === "viewBox") { viewBox = `viewBox="${a.value}"`; break; } } const symbolBody = xml.children[0].children .map((c) => xmlTostring(c)) .join(""); return `${symbolBody}`; } async function init(): Promise { // Delete any old output directory rmDirSync(OUTPUT_DIR); // Create output directory layout fs.mkdirSync(OUTPUT_DIR); fs.mkdirSync(OUTPUT_DIR + "/bin"); fs.mkdirSync(OUTPUT_DIR + "/public"); // Create package.json const packageJson = JSON.parse( fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString() ); delete packageJson["devDependencies"]; delete packageJson["private"]; packageJson["scripts"] = { install: packageJson["scripts"].install, configure: packageJson["scripts"].configure, }; packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`; fs.writeFileSync( path.resolve(OUTPUT_DIR, "package.json"), JSON.stringify(packageJson, null, 2) ); // Create npm-shrinkwrap.json const npmShrinkwrapJson = JSON.parse( fs.readFileSync(path.resolve(INPUT_DIR, "npm-shrinkwrap.json")).toString() ); npmShrinkwrapJson["version"] = packageJson["version"]; stripDevDeps(npmShrinkwrapJson); stripDevDeps2(npmShrinkwrapJson); fs.writeFileSync( path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"), JSON.stringify(npmShrinkwrapJson, null, 2) ); return Object.keys(packageJson["dependencies"]); } async function copyStatic(): Promise { const files = [ "LICENSE", "README.md", "CHANGELOG.md", "public/logo.svg", "public/favicon.png", ]; for (const file of files) { fs.copyFileSync( path.resolve(INPUT_DIR, file), path.resolve(OUTPUT_DIR, file) ); } } async function generateCss(): Promise { const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css"); const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css"); const cssIn = fs.readFileSync(cssInPath); const cssOut = await postcss([ postcssImport, postcssPresetEnv({ stage: 3, features: { "nesting-rules": true, }, }), postcssColorMod, ...(MODE === "production" ? [cssnano] : []), ]).process(cssIn, { from: cssInPath, to: cssOutPath }); fs.writeFileSync(cssOutPath, cssOut.css); } async function generateBackendJs(externals: string[]): Promise { for (const bin of [ "genieacs-cwmp", "genieacs-ext", "genieacs-nbi", "genieacs-fs", "genieacs-ui", ]) { const inputFile = path.resolve(INPUT_DIR, `bin/${bin}.ts`); const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`); const bundle = await rollup({ input: inputFile, external: [...builtins, ...externals], acorn: { allowHashBang: true, }, treeshake: { propertyReadSideEffects: false, moduleSideEffects: false, }, plugins: [ rollupJson({ preferConst: true }), { name: "", resolveId: (importee, importer) => { if (importee.endsWith("/package.json")) { const p = path.resolve(path.dirname(importer), importee); if (p === path.resolve(INPUT_DIR, "package.json")) return path.resolve(OUTPUT_DIR, "package.json"); } return null; }, }, typescript({ tsconfig: "./tsconfig.json", include: [`bin/${bin}.ts`, "lib/**/*.ts"], }), MODE === "production" ? terser() : null, ], }); await bundle.write({ format: "cjs", preferConst: true, sourcemap: "inline", sourcemapExcludeSources: true, banner: "#!/usr/bin/env node", file: outputFile, }); // Mark as executable const mode = fs.statSync(outputFile).mode; fs.chmodSync(outputFile, mode | 73); } } async function generateFrontendJs(externals: string[]): Promise { const inputFile = path.resolve(INPUT_DIR, "ui/app.ts"); const outputDir = path.resolve(OUTPUT_DIR, "public"); const inlineDeps = ["parsimmon", "espresso-iisojs"]; const bundle = await rollup({ input: inputFile, external: [ ...builtins, ...externals.filter((e) => !inlineDeps.includes(e)), ], plugins: [ rollupJson({ preferConst: true }), { name: "", resolveId: function (importee, importer) { if (importee.endsWith("/package.json")) { const p = path.resolve(path.dirname(importer), importee); if (p === path.resolve(INPUT_DIR, "package.json")) return path.resolve(OUTPUT_DIR, "package.json"); } return null; }, }, typescript({ tsconfig: "./tsconfig.json" }), nodeResolve(), commonjs(), MODE === "production" ? terser() : null, ], preserveEntrySignatures: false, treeshake: { propertyReadSideEffects: false, moduleSideEffects: false, }, onwarn: ((warning, warn) => { // Ignore circular dependency warnings if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning); }) as WarningHandler, }); await bundle.write({ manualChunks: (id) => { if (id.includes("node_modules/codemirror")) return "codemirror"; else if (id.includes("node_modules/yaml")) return "yaml"; return "app"; }, preferConst: true, format: "es", sourcemap: true, sourcemapExcludeSources: true, dir: outputDir, }); } async function generateIconsSprite(): Promise { const symbols = []; const iconsDir = path.resolve(INPUT_DIR, "ui/icons"); for (const file of fs.readdirSync(iconsDir)) { const id = path.parse(file).name; const filePath = path.join(iconsDir, file); const { data } = await optimize(fs.readFileSync(filePath).toString(), { plugins: [ { name: "preset-default", params: { overrides: { removeViewBox: false, }, }, }, ], }); symbols.push(generateSymbol(id, data)); } fs.writeFileSync( path.resolve(OUTPUT_DIR, "public/icons.svg"), `${symbols.join("")}` ); } init() .then((externals) => { Promise.all([ copyStatic(), generateCss(), generateIconsSprite(), generateBackendJs(externals), generateFrontendJs(externals), ]) .then(() => { // Ignore }) .catch((err) => { process.stderr.write(err.stack + "\n"); }); }) .catch((err) => { process.stderr.write(err.stack + "\n"); });