2020-10-19 08:45:17 +00:00
|
|
|
const pug = require("pug");
|
|
|
|
const sass = require("sass");
|
|
|
|
const fs = require("fs").promises;
|
|
|
|
const os = require("os");
|
|
|
|
const crypto = require("crypto");
|
|
|
|
const path = require("path");
|
|
|
|
const pj = path.join;
|
|
|
|
const babel = require("@babel/core");
|
|
|
|
const fetch = require("node-fetch");
|
|
|
|
const chalk = require("chalk");
|
|
|
|
const hint = require("jshint").JSHINT;
|
|
|
|
|
|
|
|
process.chdir(pj(__dirname, "src"));
|
|
|
|
|
|
|
|
const buildDir = "../build";
|
|
|
|
|
|
|
|
const validationQueue = [];
|
|
|
|
const validationHost =
|
|
|
|
os.hostname() === "future"
|
|
|
|
? "http://localhost:8888/"
|
|
|
|
: "http://validator.w3.org/nu/";
|
|
|
|
const static = new Map();
|
|
|
|
const links = new Map();
|
|
|
|
const pugLocals = { static, links };
|
|
|
|
|
|
|
|
const spec = require("./spec.js");
|
2020-10-11 13:26:24 +00:00
|
|
|
|
|
|
|
function hash(buffer) {
|
2020-10-19 08:45:17 +00:00
|
|
|
return crypto.createHash("sha256").update(buffer).digest("hex").slice(0, 10);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function validate(filename, body, type) {
|
2020-10-19 08:45:17 +00:00
|
|
|
const promise = fetch(validationHost + "?out=json", {
|
|
|
|
method: "POST",
|
|
|
|
body,
|
|
|
|
headers: {
|
|
|
|
"content-type": `text/${type}; charset=UTF-8`,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.then((res) => res.json())
|
|
|
|
.then((root) => {
|
|
|
|
return function cont() {
|
|
|
|
let concerningMessages = 0;
|
|
|
|
for (const message of root.messages) {
|
|
|
|
if (message.hiliteStart) {
|
|
|
|
let type = message.type;
|
|
|
|
if (message.type === "error") {
|
|
|
|
type = chalk.red("error");
|
|
|
|
} else if (message.type === "warning") {
|
|
|
|
type = chalk.yellow("warning");
|
|
|
|
} else {
|
|
|
|
continue; // don't care about info
|
|
|
|
}
|
|
|
|
let match;
|
|
|
|
if (
|
|
|
|
(match = message.message.match(
|
|
|
|
/Property “([\w-]+)” doesn't exist.$/
|
|
|
|
))
|
|
|
|
) {
|
|
|
|
// allow these properties specifically
|
|
|
|
if (
|
|
|
|
[
|
|
|
|
"scrollbar-width",
|
|
|
|
"scrollbar-color",
|
|
|
|
"overflow-anchor",
|
|
|
|
].includes(match[1])
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
concerningMessages++;
|
|
|
|
console.log(`validation: ${type} in ${filename}`);
|
|
|
|
console.log(` ${message.message}`);
|
|
|
|
const text = message.extract
|
|
|
|
.replace(/\n/g, "⏎")
|
|
|
|
.replace(/\t/g, " ");
|
|
|
|
console.log(
|
|
|
|
chalk.grey(
|
|
|
|
" " +
|
|
|
|
text.slice(0, message.hiliteStart) +
|
|
|
|
chalk.inverse(
|
|
|
|
text.substr(message.hiliteStart, message.hiliteLength)
|
|
|
|
) +
|
|
|
|
text.slice(message.hiliteStart + message.hiliteLength)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
console.log(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!concerningMessages) {
|
|
|
|
console.log(`validation: ${chalk.green("ok")} for ${filename}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
validationQueue.push(promise);
|
|
|
|
return promise;
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function runHint(filename, source) {
|
2020-10-19 08:45:17 +00:00
|
|
|
hint(source, {
|
|
|
|
esversion: 9,
|
|
|
|
undef: true,
|
|
|
|
// unused: true,
|
|
|
|
loopfunc: true,
|
|
|
|
globals: ["console", "URLSearchParams"],
|
|
|
|
browser: true,
|
|
|
|
asi: true,
|
|
|
|
});
|
|
|
|
const result = hint.data();
|
|
|
|
let problems = 0;
|
|
|
|
if (result.errors) {
|
|
|
|
for (const error of result.errors) {
|
|
|
|
if (error.evidence) {
|
|
|
|
const text = error.evidence.replace(/\t/g, " ");
|
|
|
|
if (["W014"].includes(error.code)) continue;
|
|
|
|
let type = error.code.startsWith("W")
|
|
|
|
? chalk.yellow("warning")
|
|
|
|
: chalk.red("error");
|
|
|
|
console.log(`hint: ${type} in ${filename}`);
|
|
|
|
console.log(
|
|
|
|
` ${error.line}:${error.character}: ${error.reason} (${error.code})`
|
|
|
|
);
|
|
|
|
console.log(
|
|
|
|
chalk.gray(
|
|
|
|
" " +
|
|
|
|
text.slice(0, error.character) +
|
|
|
|
chalk.inverse(text.substr(error.character, 1)) +
|
|
|
|
text.slice(error.character + 1)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
problems++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (problems) {
|
|
|
|
console.log(`hint: ${chalk.cyan(problems + " problems")} in ${filename}`);
|
|
|
|
} else {
|
|
|
|
console.log(`hint: ${chalk.green("ok")} for ${filename}`);
|
|
|
|
}
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addFile(sourcePath, targetPath) {
|
2020-10-19 08:45:17 +00:00
|
|
|
const contents = await fs.readFile(pj(".", sourcePath), { encoding: null });
|
|
|
|
static.set(sourcePath, `${targetPath}?static=${hash(contents)}`);
|
|
|
|
fs.writeFile(pj(buildDir, targetPath), contents);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addJS(sourcePath, targetPath) {
|
2020-10-19 08:45:17 +00:00
|
|
|
const source = await fs.readFile(pj(".", sourcePath), { encoding: "utf8" });
|
|
|
|
static.set(sourcePath, `${targetPath}?static=${hash(source)}`);
|
|
|
|
runHint(sourcePath, source);
|
|
|
|
fs.writeFile(pj(buildDir, targetPath), source);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addSass(sourcePath, targetPath) {
|
2020-10-19 08:45:17 +00:00
|
|
|
const renderedCSS = sass.renderSync({
|
|
|
|
file: pj(".", sourcePath),
|
|
|
|
outputStyle: "expanded",
|
|
|
|
indentType: "tab",
|
|
|
|
indentWidth: 1,
|
|
|
|
functions: {
|
|
|
|
"static($name)": function (name) {
|
|
|
|
if (!(name instanceof sass.types.String)) {
|
|
|
|
throw "$name: expected a string";
|
|
|
|
}
|
|
|
|
const result = static.get(name.getValue());
|
|
|
|
if (typeof result === "string") {
|
|
|
|
return new sass.types.String(result);
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
"static file '" + name.getValue() + "' does not exist"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}).css;
|
|
|
|
static.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`);
|
|
|
|
validate(sourcePath, renderedCSS, "css");
|
|
|
|
await fs.writeFile(pj(buildDir, targetPath), renderedCSS);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addPug(sourcePath, targetPath) {
|
2020-10-19 08:45:17 +00:00
|
|
|
function getRelative(staticTarget) {
|
|
|
|
const pathLayer = (
|
|
|
|
path.dirname(targetPath).replace(/\/$/, "").match(/\//g) || []
|
|
|
|
).length;
|
|
|
|
const prefix = Array(pathLayer).fill("../").join("");
|
|
|
|
const result = prefix + staticTarget.replace(/^\//, "");
|
|
|
|
if (result) return result;
|
|
|
|
else return "./";
|
|
|
|
}
|
|
|
|
function getStatic(target) {
|
|
|
|
return getRelative(static.get(target));
|
|
|
|
}
|
|
|
|
function getStaticName(target) {
|
|
|
|
return getRelative(static.get(target)).replace(/\?.*$/, "");
|
|
|
|
}
|
|
|
|
function getLink(target) {
|
|
|
|
return getRelative(links.get(target));
|
|
|
|
}
|
|
|
|
const renderedHTML = pug.compileFile(pj(".", sourcePath), { pretty: true })({
|
|
|
|
getStatic,
|
|
|
|
getStaticName,
|
|
|
|
getLink,
|
|
|
|
...pugLocals,
|
|
|
|
});
|
|
|
|
let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gms, "");
|
|
|
|
validate(sourcePath, renderedWithoutPHP, "html");
|
|
|
|
await fs.writeFile(pj(buildDir, targetPath), renderedHTML);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addBabel(sourcePath, targetPath) {
|
2020-10-19 08:45:17 +00:00
|
|
|
const originalCode = await fs.readFile(pj(".", sourcePath), "utf8");
|
|
|
|
|
|
|
|
const compiled = babel.transformSync(originalCode, {
|
|
|
|
sourceMaps: false,
|
|
|
|
sourceType: "script",
|
|
|
|
presets: [
|
|
|
|
[
|
|
|
|
"@babel/env",
|
|
|
|
{
|
|
|
|
targets: {
|
|
|
|
ie: 11,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
],
|
|
|
|
generatorOpts: {
|
|
|
|
comments: false,
|
|
|
|
minified: false,
|
|
|
|
sourceMaps: false,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
|
|
|
|
|
|
|
|
static.set(sourcePath, filenameWithQuery);
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
fs.writeFile(pj(buildDir, targetPath), originalCode),
|
|
|
|
fs.writeFile(pj(buildDir, minFilename), compiled.code),
|
|
|
|
fs.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map)),
|
|
|
|
]);
|
2020-10-11 13:26:24 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 08:45:17 +00:00
|
|
|
(async () => {
|
|
|
|
// Stage 1: Register
|
|
|
|
for (const item of spec) {
|
|
|
|
if (item.type === "pug") {
|
|
|
|
links.set(item.source, item.target.replace(/index.html$/, ""));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stage 2: Build
|
|
|
|
for (const item of spec) {
|
|
|
|
if (item.type === "file") {
|
|
|
|
await addFile(item.source, item.target);
|
|
|
|
} else if (item.type === "js") {
|
|
|
|
await addJS(item.source, item.target);
|
|
|
|
} else if (item.type === "sass") {
|
|
|
|
await addSass(item.source, item.target);
|
|
|
|
} else if (item.type === "babel") {
|
|
|
|
await addBabel(item.source, item.target);
|
|
|
|
} else if (item.type === "pug") {
|
|
|
|
await addPug(item.source, item.target);
|
|
|
|
} else {
|
|
|
|
throw new Error("Unknown item type: " + item.type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(chalk.green("All files emitted."));
|
|
|
|
|
|
|
|
await Promise.all(validationQueue).then((v) => {
|
|
|
|
console.log(`validation: using host ${chalk.cyan(validationHost)}`);
|
|
|
|
v.forEach((cont) => cont());
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(chalk.green("Build complete.") + "\n\n------------\n");
|
|
|
|
})();
|