add build script
This commit is contained in:
parent
36710eccc4
commit
fd9bd48925
6 changed files with 2113 additions and 6 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
target
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">
|
<script id="main" type="module">
|
||||||
import {main} from "./src/main.js";
|
import {main} from "./src/main.js";
|
||||||
main(document.body);
|
main(document.body);
|
||||||
</script>
|
</script>
|
||||||
|
|
28
package.json
28
package.json
|
@ -1,9 +1,31 @@
|
||||||
{
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node scripts/serve-local.js"
|
"start": "node scripts/serve-local.js",
|
||||||
|
"build": "node scripts/build.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.11.1",
|
||||||
|
"@babel/preset-env": "^7.11.0",
|
||||||
|
"@rollup/plugin-babel": "^5.1.0",
|
||||||
|
"@rollup/plugin-commonjs": "^15.0.0",
|
||||||
|
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
|
"autoprefixer": "^10.0.1",
|
||||||
|
"cheerio": "^1.0.0-rc.3",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
"finalhandler": "^1.1.2",
|
"finalhandler": "^1.1.2",
|
||||||
"serve-static": "^1.14.1"
|
"mdn-polyfills": "^5.20.0",
|
||||||
|
"postcss": "^8.1.1",
|
||||||
|
"postcss-css-variables": "^0.17.0",
|
||||||
|
"postcss-flexbugs-fixes": "^4.2.1",
|
||||||
|
"postcss-import": "^12.0.1",
|
||||||
|
"postcss-url": "^8.0.0",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rollup": "^2.26.4",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"serve-static": "^1.14.1",
|
||||||
|
"xxhashjs": "^0.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
297
scripts/build.js
Normal file
297
scripts/build.js
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cheerio = require("cheerio");
|
||||||
|
const fsRoot = require("fs");
|
||||||
|
const fs = fsRoot.promises;
|
||||||
|
const path = require("path");
|
||||||
|
const xxhash = require('xxhashjs');
|
||||||
|
const { rollup } = require('rollup');
|
||||||
|
const postcss = require("postcss");
|
||||||
|
const postcssImport = require("postcss-import");
|
||||||
|
// needed for legacy bundle
|
||||||
|
const babel = require('@rollup/plugin-babel');
|
||||||
|
// needed to find the polyfill modules in the main-legacy.js bundle
|
||||||
|
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||||
|
// needed because some of the polyfills are written as commonjs modules
|
||||||
|
const commonjs = require('@rollup/plugin-commonjs');
|
||||||
|
// multi-entry plugin so we can add polyfill file to main
|
||||||
|
const multi = require('@rollup/plugin-multi-entry');
|
||||||
|
const { terser } = require("rollup-plugin-terser");
|
||||||
|
const replace = require("@rollup/plugin-replace");
|
||||||
|
// replace urls of asset names with content hashed version
|
||||||
|
const postcssUrl = require("postcss-url");
|
||||||
|
|
||||||
|
const cssvariables = require("postcss-css-variables");
|
||||||
|
const autoprefixer = require("autoprefixer");
|
||||||
|
const flexbugsFixes = require("postcss-flexbugs-fixes");
|
||||||
|
|
||||||
|
const projectDir = path.join(__dirname, "../");
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
// get version number
|
||||||
|
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
|
||||||
|
// clear target dir
|
||||||
|
const targetDir = path.join(projectDir, "target/");
|
||||||
|
await removeDirIfExists(targetDir);
|
||||||
|
await fs.mkdir(targetDir);
|
||||||
|
await fs.mkdir(path.join(targetDir, "images"));
|
||||||
|
const assets = new AssetMap(targetDir);
|
||||||
|
const imageAssets = await copyFolder(path.join(projectDir, "images"), path.join(targetDir, "images"));
|
||||||
|
assets.addSubMap(imageAssets);
|
||||||
|
await assets.write(`bundle-esm.js`, await buildJs("src/main.js", assets));
|
||||||
|
await assets.write(`bundle-legacy.js`, await buildJsLegacy("src/main.js", assets, ["src/polyfill.js"]));
|
||||||
|
await assets.write(`bundle.css`, await buildCss("css/main.css", assets));
|
||||||
|
const globalHash = assets.hashForAll();
|
||||||
|
|
||||||
|
await buildHtml(assets);
|
||||||
|
console.log(`built matrix.to ${version} (${globalHash}) successfully with ${assets.size} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildHtml(assets) {
|
||||||
|
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
|
||||||
|
const doc = cheerio.load(devHtml);
|
||||||
|
doc("link[rel=stylesheet]").attr("href", assets.resolve(`bundle.css`));
|
||||||
|
const mainScripts = [
|
||||||
|
`<script type="module">import {main} from "./${assets.resolve(`bundle-esm.js`)}"; main(document.body);</script>`,
|
||||||
|
`<script type="text/javascript" nomodule src="${assets.resolve(`bundle-legacy.js`)}"></script>`,
|
||||||
|
`<script type="text/javascript" nomodule>bundle.main(document.body);</script>`
|
||||||
|
];
|
||||||
|
doc("script#main").replaceWith(mainScripts.join(""));
|
||||||
|
const html = doc.html();
|
||||||
|
// include in the global hash, even not hashed itself
|
||||||
|
assets.addToHashForAll("index.html", html);
|
||||||
|
await assets.writeUnhashed("index.html", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReplaceUrlPlugin(assets) {
|
||||||
|
const replacements = {};
|
||||||
|
for (const [key, value] of assets) {
|
||||||
|
replacements[key] = value;
|
||||||
|
}
|
||||||
|
return replace(replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildJs(mainFile, assets, extraFiles = []) {
|
||||||
|
// create js bundle
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: extraFiles.concat(mainFile),
|
||||||
|
plugins: [multi(), terser(), createReplaceUrlPlugin(assets)],
|
||||||
|
});
|
||||||
|
const {output} = await bundle.generate({
|
||||||
|
format: 'es',
|
||||||
|
});
|
||||||
|
const code = output[0].code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildJsLegacy(mainFile, assets, extraFiles = []) {
|
||||||
|
// compile down to whatever IE 11 needs
|
||||||
|
const babelPlugin = babel.babel({
|
||||||
|
babelHelpers: 'bundled',
|
||||||
|
exclude: 'node_modules/**',
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
corejs: "3",
|
||||||
|
targets: "IE 11",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// create js bundle
|
||||||
|
const rollupConfig = {
|
||||||
|
// important the extraFiles come first,
|
||||||
|
// so polyfills are available in the global scope
|
||||||
|
// if needed for the mainfile
|
||||||
|
input: extraFiles.concat(mainFile),
|
||||||
|
plugins: [multi(), commonjs(), nodeResolve(), createReplaceUrlPlugin(assets), babelPlugin, terser()]
|
||||||
|
};
|
||||||
|
const bundle = await rollup(rollupConfig);
|
||||||
|
const {output} = await bundle.generate({
|
||||||
|
format: 'iife',
|
||||||
|
name: `bundle`
|
||||||
|
});
|
||||||
|
const code = output[0].code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildCss(entryPath, assets) {
|
||||||
|
entryPath = path.join(projectDir, entryPath);
|
||||||
|
const assetUrlMapper = ({absolutePath}) => {
|
||||||
|
const relPath = absolutePath.substr(projectDir.length);
|
||||||
|
return assets.resolve(path.join(projectDir, "target", relPath));
|
||||||
|
};
|
||||||
|
|
||||||
|
const preCss = await fs.readFile(entryPath, "utf8");
|
||||||
|
const options = [
|
||||||
|
postcssImport,
|
||||||
|
cssvariables(),
|
||||||
|
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
||||||
|
flexbugsFixes(),
|
||||||
|
postcssUrl({url: assetUrlMapper}),
|
||||||
|
];
|
||||||
|
const cssBundler = postcss(options);
|
||||||
|
const result = await cssBundler.process(preCss, {from: entryPath});
|
||||||
|
return result.css;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeDirIfExists(targetDir) {
|
||||||
|
try {
|
||||||
|
await fs.rmdir(targetDir, {recursive: true});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyFolder(srcRoot, dstRoot, filter = null, assets = null) {
|
||||||
|
assets = assets || new AssetMap(dstRoot);
|
||||||
|
const dirEnts = await fs.readdir(srcRoot, {withFileTypes: true});
|
||||||
|
for (const dirEnt of dirEnts) {
|
||||||
|
const dstPath = path.join(dstRoot, dirEnt.name);
|
||||||
|
const srcPath = path.join(srcRoot, dirEnt.name);
|
||||||
|
if (dirEnt.isDirectory()) {
|
||||||
|
await fs.mkdir(dstPath);
|
||||||
|
await copyFolder(srcPath, dstPath, filter, assets);
|
||||||
|
} else if ((dirEnt.isFile() || dirEnt.isSymbolicLink()) && (!filter || filter(srcPath))) {
|
||||||
|
const content = await fs.readFile(srcPath);
|
||||||
|
await assets.write(dstPath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentHash(str) {
|
||||||
|
var hasher = new xxhash.h32(0);
|
||||||
|
hasher.update(str);
|
||||||
|
return hasher.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetMap {
|
||||||
|
constructor(targetDir) {
|
||||||
|
// remove last / if any, so substr in create works well
|
||||||
|
this._targetDir = path.resolve(targetDir);
|
||||||
|
this._assets = new Map();
|
||||||
|
// hashes for unhashed resources so changes in these resources also contribute to the hashForAll
|
||||||
|
this._unhashedHashes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_toRelPath(resourcePath) {
|
||||||
|
let relPath = resourcePath;
|
||||||
|
if (path.isAbsolute(resourcePath)) {
|
||||||
|
if (!resourcePath.startsWith(this._targetDir)) {
|
||||||
|
throw new Error(`absolute path ${resourcePath} that is not within target dir ${this._targetDir}`);
|
||||||
|
}
|
||||||
|
relPath = resourcePath.substr(this._targetDir.length + 1); // + 1 for the /
|
||||||
|
}
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
_create(resourcePath, content) {
|
||||||
|
const relPath = this._toRelPath(resourcePath);
|
||||||
|
const hash = contentHash(Buffer.from(content));
|
||||||
|
const dir = path.dirname(relPath);
|
||||||
|
const extname = path.extname(relPath);
|
||||||
|
const basename = path.basename(relPath, extname);
|
||||||
|
const dstRelPath = path.join(dir, `${basename}-${hash}${extname}`);
|
||||||
|
this._assets.set(relPath, dstRelPath);
|
||||||
|
return dstRelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(resourcePath, content) {
|
||||||
|
const relPath = this._create(resourcePath, content);
|
||||||
|
const fullPath = path.join(this.directory, relPath);
|
||||||
|
if (typeof content === "string") {
|
||||||
|
await fs.writeFile(fullPath, content, "utf8");
|
||||||
|
} else {
|
||||||
|
await fs.writeFile(fullPath, content);
|
||||||
|
}
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeUnhashed(resourcePath, content) {
|
||||||
|
const relPath = this._toRelPath(resourcePath);
|
||||||
|
this._assets.set(relPath, relPath);
|
||||||
|
const fullPath = path.join(this.directory, relPath);
|
||||||
|
if (typeof content === "string") {
|
||||||
|
await fs.writeFile(fullPath, content, "utf8");
|
||||||
|
} else {
|
||||||
|
await fs.writeFile(fullPath, content);
|
||||||
|
}
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
get directory() {
|
||||||
|
return this._targetDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(resourcePath) {
|
||||||
|
const relPath = this._toRelPath(resourcePath);
|
||||||
|
const result = this._assets.get(relPath);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`unknown path: ${relPath}, only know ${Array.from(this._assets.keys()).join(", ")}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubMap(assetMap) {
|
||||||
|
if (!assetMap.directory.startsWith(this.directory)) {
|
||||||
|
throw new Error(`map directory doesn't start with this directory: ${assetMap.directory} ${this.directory}`);
|
||||||
|
}
|
||||||
|
const relSubRoot = assetMap.directory.substr(this.directory.length + 1);
|
||||||
|
for (const [key, value] of assetMap._assets.entries()) {
|
||||||
|
this._assets.set(path.join(relSubRoot, key), path.join(relSubRoot, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._assets.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnhashed(relPath) {
|
||||||
|
const resolvedPath = this._assets.get(relPath);
|
||||||
|
if (!resolvedPath) {
|
||||||
|
throw new Error("Unknown asset: " + relPath);
|
||||||
|
}
|
||||||
|
return relPath === resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._assets.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(relPath) {
|
||||||
|
return this._assets.has(relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
hashForAll() {
|
||||||
|
const globalHashAssets = Array.from(this).map(([, resolved]) => resolved);
|
||||||
|
globalHashAssets.push(...this._unhashedHashes);
|
||||||
|
globalHashAssets.sort();
|
||||||
|
return contentHash(globalHashAssets.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHashForAll(resourcePath, content) {
|
||||||
|
this._unhashedHashes.push(`${resourcePath}-${contentHash(Buffer.from(content))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build().catch(err => console.error(err));
|
18
src/polyfill.js
Normal file
18
src/polyfill.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "core-js/stable";
|
||||||
|
import "regenerator-runtime/runtime";
|
Loading…
Reference in a new issue