actless
Version:
Static website generator/scaffolding tool based on wig
789 lines (735 loc) • 22.1 kB
JavaScript
;
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const _ = require("lodash");
const Wig = require("wig");
const open = require("open");
const sass = require("gulp-sass")(require("sass"));
const postcss = require("gulp-postcss");
const plumber = require("gulp-plumber");
const browserify = require("browserify");
const babelify = require("babelify");
const factor = require("factor-bundle");
const file = require("gulp-file");
const concat = require("concat-stream");
const glob = require("glob");
const uglify = require("gulp-uglify");
const shell = require("gulp-shell");
const prettify = require("gulp-prettify");
const svgmin = require("gulp-svgmin");
const flatmap = require("gulp-flatmap");
const gulpconcat = require("gulp-concat");
const consolidate = require("gulp-consolidate");
const iconfont = require("gulp-iconfont");
const walkSync = require("walk-sync");
const webserver = require("gulp-webserver");
const typescript = require("gulp-typescript");
var options = {};
var hasBrowserList = fs.existsSync("./.browserlistrc");
if (!hasBrowserList) {
console.warn("Make .browserlistrc file for babel/postcss etc.");
}
// css compile options
options.css = {
srcDir: "",
destDir: "public/assets/css",
sass: {
enabled: true,
outputStyle: "expanded",
includePaths: [
"./node_modules/actless/sass",
"./node_modules/sanitize.css",
],
},
postcssImport: {
enabled: true,
options: {},
},
postcssPresetEnv: {
enabled: true,
options: {
stage: 1,
},
},
mqpacker: {
enabled: true,
options: {},
},
cssnano: {
enabled: true,
options: {},
},
stylelint: {
enabled: false,
options: {},
},
};
options.sass = options.css; // for backward compatibility
// js compile options
options.js = {
enabled: true,
srcDir: "assets/js",
entry: "assets/js/*.js",
watch: ["assets/js/**/*.js", "assets/js/**/*.jsx"],
destDir: "public/assets/js",
commonFileName: "common.js",
babelPresets: [["@babel/preset-env", {}], "@babel/preset-react"],
exclude: [],
skipMinify: false,
};
// ts compile options
options.ts = {
enabled: false,
src: "assets/ts/**/*{ts,tsx}",
destDir: options.js.srcDir,
exclude: [],
configFile: "",
options: {
jsx: "react",
target: "esnext",
moduleResolution: "node",
},
};
// webpack options
options.webpack = {
enabled: false,
};
// icon font compile options
options.icon = {
srcDir: "assets/icons/",
destDir: "public/assets/fonts/",
sassDir: "",
renameSrcFile: {
from: "iconsのコピー_",
to: "",
},
minifiedDir: "assets/icons_min/",
fontName: "icon",
iconCssName: "_icon",
cssTemplate: __dirname + "/lib/templates/_icon.scss",
cssFontPath: "../fonts/",
className: "icon",
exportGlyphsAsProp: true,
options: {},
};
// wig(HTML builder) compile options
options.wig = {
enabled: true,
publicDir: "public",
dataDir: "data",
tmplDir: "templates",
verbose: true,
};
// test server options
options.server = {
type: "node",
rootDir: "public",
gaeAppRoot: "app", // for app engine only
url: {}, // for backward compatibility...
options: {
path: "/",
livereload: true,
host: "localhost",
port: 3000,
fallback: undefined,
https: false,
},
};
// HTML prettify options
options.prettify = {
enabled: false,
tmpDir: "tmp_html",
options: {
indent_size: 2,
},
};
// generate assetHash for cache busterring
options.assetHash = {
enabled: true,
destDir: "",
extraAssetDir: [],
};
var actless = {};
actless.options = options;
actless.initTasks = function (gulp, rootPath) {
// set NODE_ENV to "production"
process.env.NODE_ENV = process.env.NODE_ENV || "production";
// compile css =======
if (!options.css.srcDir) {
if (options.css.sass.enabled) {
options.css.srcDir = "assets/sass";
} else {
options.css.srcDir = "assets/css";
}
}
if (!hasBrowserList) {
options.css.postcssPresetEnv.options.browsers = [
"last 2 versions",
"> 4%",
"not dead",
];
}
function runCss() {
var g = gulp
.src(path.join(rootPath, options.css.srcDir, "**", "!(_*)"))
.pipe(plumber());
if (options.css.sass.enabled) {
g = g.pipe(
sass({
includePaths: options.css.sass.includePaths,
outputStyle: options.css.sass.outputStyle,
}).on("error", sass.logError)
);
}
//postcss(preprocess)
var preprocessors = [];
if (!options.css.sass.enabled && options.css.postcssImport.enabled) {
if (options.css.stylelint.enabled) {
options.css.postcssImport.options.plugins =
options.css.postcssImport.options.plugins || [];
options.css.postcssImport.options.plugins.push(
require("stylelint")(options.css.stylelint.options)
);
}
preprocessors.push(
require("postcss-import")(options.css.postcssImport.options)
);
}
if (options.css.postcssPresetEnv.enabled) {
let opt = options.css.postcssPresetEnv.options;
preprocessors.push(require("postcss-preset-env")(opt));
}
if (preprocessors.length) {
g = g.pipe(postcss(preprocessors));
}
//postcss(postprocess)
var postprocessors = [];
if (options.css.mqpacker.enabled) {
postprocessors.push(
require("@hail2u/css-mqpacker")(options.css.mqpacker.options)
);
}
if (options.css.cssnano.enabled) {
postprocessors.push(require("cssnano")(options.css.cssnano.options));
}
if (postprocessors.length) {
g = g.pipe(postcss(postprocessors));
}
g = g
.pipe(plumber.stop())
.pipe(gulp.dest(path.join(rootPath, options.css.destDir)));
return g;
}
gulp.task("actless:css", runCss);
gulp.task("actless:sass", runCss); //for backward compatibility
function watchCss(cb) {
gulp.watch(
[
path.join(rootPath, options.css.srcDir) + "/**/*",
path.join(rootPath, options.css.srcDir) + "/**/*",
"!" + path.join(rootPath, options.css.srcDir) + "/**/*.swp",
],
gulp.series("actless:css")
);
cb();
}
gulp.task("actless:css:watch", watchCss);
gulp.task("actless:sass:watch", watchCss); //for backward compatibility
/*
build JS ========================================================
ref:http://qiita.com/inuscript/items/b933af4d44a4712cb8f8
*/
function runJs() {
var write = (filepath) => {
return concat((content) => {
var res = file(path.basename(filepath), content, {
src: true,
});
if (!options.js.skipMinify) {
res.pipe(uglify());
}
res.pipe(gulp.dest(path.join(rootPath, options.js.destDir)));
return res;
});
};
var files = glob.sync(path.join(rootPath, options.js.entry), {
nodir: true,
});
var outputFiles = files.map((fileName) => {
return write(fileName.replace(options.js.srcDir, options.js.destDir));
});
var b = browserify(files, {
extensions: ["js", "jsx"],
debug: true,
});
for (var i = 0, len = options.js.exclude.length; i < len; i++) {
b = b.exclude(options.js.exclude[i]);
}
b = b
.transform(
babelify.configure({
presets: options.js.babelPresets,
})
)
.plugin(factor, {
output: outputFiles,
})
.bundle()
.on("error", function (err) {
console.warn("Error : " + err.message + "\n" + err.stack);
this.emit("end"); // for prevent stop 'watch'
})
.pipe(write(options.js.commonFileName))
.on("error", function (err) {
console.warn("Error : " + err.message + "\n" + err.stack);
this.emit("end");
});
return b;
}
gulp.task("actless:js", runJs);
function watchJs(cb) {
gulp.watch(options.js.watch, gulp.series("actless:js"));
cb();
}
gulp.task("actless:js:watch", watchJs);
// typescript ================================================
let tsProject = typescript.createProject(
options.ts.configFile ? options.ts.configFile : options.ts.options
);
function runTS(cb) {
return gulp
.src(options.ts.src)
.pipe(tsProject())
.on("error", function (err) {
console.warn("Error : " + err.message + "\n" + err.stack);
this.emit("end");
})
.pipe(gulp.dest(options.ts.destDir));
}
gulp.task("actless:ts", runTS);
function watchTS(cb) {
gulp.watch(options.ts.src, gulp.series("actless:ts"));
cb();
}
gulp.task("actless:ts:watch", watchTS);
// webpack ===================================================
const runWebpack = shell.task(["npx webpack --mode production"]);
gulp.task("actless:webpack", runWebpack);
const watchWebpack = shell.task(["npx webpack --mode production -w"]);
gulp.task("actless:webpack:watch", watchWebpack);
// bulid icon font ===========================================
function runSvgmin() {
return gulp
.src(path.join(rootPath, options.icon.srcDir, "**", "*.svg"))
.pipe(
flatmap((stream, file) => {
var filename = file.path.replace(file.base, "");
filename = filename.replace(
options.icon.renameSrcFile.from,
options.icon.renameSrcFile.to
);
return stream.pipe(svgmin()).pipe(gulpconcat(filename));
})
)
.pipe(gulp.dest(path.join(rootPath, options.icon.minifiedDir)));
}
gulp.task("actless:icons:svgmin", runSvgmin);
let currentCodepoints = null;
function compileIcons() {
return gulp
.src(path.join(rootPath, options.icon.minifiedDir, "**", "*.svg"))
.pipe(
iconfont(
Object.assign(
{
fontName: options.icon.fontName,
className: options.icon.className,
formats: ["svg", "ttf", "eot", "woff"],
startUnicode: 0xf001,
fontHeight: 512,
descent: 64,
},
options.icon.options
)
)
)
.on("glyphs", (codepoints, opt) => {
currentCodepoints = codepoints;
})
.pipe(gulp.dest(path.join(rootPath, options.icon.destDir)));
}
gulp.task("actless:icons:compile", compileIcons);
function runIconHash() {
var data;
try {
data = fs.readFileSync(
path.join(
rootPath,
options.icon.destDir,
options.icon.fontName + ".woff"
)
);
} catch (e) {
console.log(e);
return;
}
var hash = crypto.createHash("md5");
var cssDir = options.icon.sassDir
? options.icon.sassDir
: options.css.srcDir;
hash.update(data);
currentCodepoints.forEach((val) => {
val.codepoint = val.unicode[0].charCodeAt(0).toString(16).toUpperCase();
});
return gulp
.src(options.icon.cssTemplate)
.pipe(
consolidate("nunjucks", {
hash: hash.digest("hex"),
glyphs: currentCodepoints,
fontName: options.icon.fontName,
fontPath: options.icon.cssFontPath,
className: options.icon.className,
varName: options.icon.iconCssName.substr(1),
isSass: !!options.css.sass.enabled,
exportProp: !!options.icon.exportGlyphsAsProp,
})
)
.pipe(
gulpconcat(
options.icon.iconCssName +
(options.css.sass.enabled ? ".scss" : ".css")
)
)
.pipe(gulp.dest(path.join(rootPath, cssDir)));
}
gulp.task("actless:icons:hash", runIconHash);
const watchIcons = gulp.series(
runSvgmin,
compileIcons,
runIconHash,
function (cb) {
gulp.watch(
path.join(rootPath, options.icon.srcDir, "**", "*.svg"),
gulp.series("actless:icons:svgmin")
);
gulp.watch(
path.join(rootPath, options.icon.minifiedDir, "**", "*.svg"),
gulp.series("actless:icons:compile")
);
gulp.watch(
path.join(
rootPath,
options.icon.destDir,
options.icon.fontName + ".woff"
),
gulp.series("actless:icons:hash")
);
cb();
}
);
gulp.task("actless:icons:watch", watchIcons);
gulp.task(
"actless:icons:build",
gulp.series(
"actless:icons:svgmin",
"actless:icons:compile",
"actless:icons:hash"
)
);
// wig ==========================
if (options.wig.enabled) {
var wigOpt = _.assign({}, options.wig);
wigOpt.rootDir = rootPath;
if (!_.isArray(wigOpt.tmplDir)) {
wigOpt.tmplDir = [wigOpt.tmplDir];
}
wigOpt.tmplDir.push(path.join(__dirname, "templates"));
// change output directory if prettify is enabled
if (options.prettify && options.prettify.enabled) {
wigOpt.outDir = options.prettify.tmpDir || "html_tmp";
}
var builder;
function runWig(cb) {
if (!builder) {
builder = new Wig(wigOpt);
//builder.addRendererFilter("date", require("nunjucks-date-filter"));
}
try {
builder.build();
} catch (e) {
console.log(e);
}
cb();
}
gulp.task("actless:wig", runWig);
var wigWatchSrc = [
path.join(rootPath, wigOpt.dataDir, "**", "*"),
path.join(rootPath, options.wig.tmplDir, "**", "*"),
"!" + path.join(rootPath, wigOpt.dataDir, "**", "*.swp"),
"!" + path.join(rootPath, options.wig.tmplDir, "**", "*.swp"),
];
const watchWig = function (cb) {
gulp.watch(wigWatchSrc, gulp.series("actless:wig"));
cb();
};
gulp.task("actless:wig:watch", watchWig);
}
// prettify =====================
// ※wigがdisabledの時=常にdisabled
if (options.wig.enabled && options.prettify.enabled) {
var prettifySrc = [];
var nonPrettifySrc = [];
prettifySrc.push(
path.join(rootPath, options.prettify.tmpDir, "**", "*.html")
);
nonPrettifySrc.push(
path.join(rootPath, options.prettify.tmpDir, "**", "*")
);
nonPrettifySrc.push(
"!" + path.join(rootPath, options.prettify.tmpDir, "**", "*.html")
);
function runPrettify() {
return gulp
.src(prettifySrc)
.pipe(prettify(options.prettify.options))
.pipe(gulp.dest(options.wig.publicDir));
}
gulp.task("actless:prettify", runPrettify);
function runNonPrettify() {
return gulp.src(nonPrettifySrc).pipe(gulp.dest(options.wig.publicDir));
}
gulp.task("actless:nonPrettify", runNonPrettify);
var prettifyTimeoutId;
gulp.task("actless:prettify:watch:run", function (cb) {
if (prettifyTimeoutId) {
clearTimeout(prettifyTimeoutId);
}
prettifyTimeoutId = setTimeout(runPrettify, 500);
cb();
});
var nonprettifyTimeoutId;
gulp.task("actless:nonPrettify:watch:run", function (cb) {
if (nonprettifyTimeoutId) {
clearTimeout(nonprettifyTimeoutId);
}
nonprettifyTimeoutId = setTimeout(runNonPrettify, 500);
cb();
});
function watchPrettify(cb) {
var prettifyTimeoutId = null;
var nonprettifyTimeoutId = null;
gulp.watch(prettifySrc, gulp.series("actless:prettify:watch:run"));
gulp.watch(nonPrettifySrc, gulp.series("actless:nonPrettify:watch:run"));
cb();
}
gulp.task("actless:prettify:watch", watchPrettify);
}
// test server ========================
options.server.options.port =
options.server.url.port || options.server.options.port;
options.server.options.host =
options.server.url.hostname || options.server.options.host;
var runServer = function (cb) {
console.log("actless:server not defined");
cb();
};
var runServerOpen = function (cb) {
console.log("actless:server:open not defined");
cb();
};
if (options.server.type !== "none") {
var testUrl = options.server.options.https ? "https://" : "http://";
testUrl = testUrl + options.server.options.host;
testUrl = testUrl + ":" + options.server.options.port;
console.log(testUrl);
runServerOpen = function (cb) {
open(testUrl);
cb();
};
}
if (options.server.type === "node") {
// test server(Nodejs)
runServer = () => {
return gulp
.src(options.server.rootDir)
.pipe(webserver(options.server.options));
};
} else if (options.server.type === "php") {
// test server(PHP)
var cmd =
"php -S " +
options.server.options.host +
":" +
options.server.options.port +
" -t" +
path.join(rootPath, options.server.rootDir);
runServer = shell.task([cmd]);
} else if (options.server.type === "python") {
// test server(Python)
var cmd =
"pushd " +
path.join(rootPath, options.server.rootDir) +
"; python -m SimpleHTTPServer " +
options.server.options.port +
"; popd";
runServer = shell.task([cmd]);
} else if (options.server.type === "gae") {
// test server(GAE)
var cmd =
"dev_appserver.py --port=" +
options.server.options.port +
" " +
path.join(rootPath, options.server.gaeAppRoot);
runServer = shell.task([cmd]);
}
gulp.task("actless:server", runServer);
gulp.task("actless:server:open", runServerOpen);
// generate asset file hash(JS/CSS) ============================
/* calc checksum ======================================= */
var assetHashDestDir = options.assetHash.destDir
? options.assetHash.destDir
: options.wig.dataDir;
var assetHashSrc = [
path.join(rootPath, options.css.destDir),
path.join(rootPath, options.js.destDir),
];
Array.prototype.push.apply(
assetHashSrc,
options.assetHash.extraAssetDir.map((v) => {
return path.join(rootPath, v);
})
);
function runAssetHash(cb) {
if (options.assetHash.enabled) {
var res = {};
var dir, files, fName, data, hash;
for (var i = 0, len = assetHashSrc.length; i < len; i++) {
dir = assetHashSrc[i];
files = walkSync(dir, {
directories: false,
});
for (var k = 0, kLen = files.length; k < kLen; k++) {
fName = files[k];
if (fName.charAt(0) !== ".") {
data = fs.readFileSync(path.join(dir, fName));
hash = crypto.createHash("md5");
hash.update(data.toString(), "utf8");
res[fName] = hash.digest("hex");
}
}
}
fs.writeFileSync(
path.join(assetHashDestDir, "_assetHash.json"),
JSON.stringify(res, null, 2)
);
}
cb();
}
gulp.task("actless:assetHash", runAssetHash);
var assetHashWatchSrc = [
path.join(rootPath, options.css.destDir, "**", "*.css"),
path.join(rootPath, options.js.destDir, "**", "*.js"),
];
Array.prototype.push.apply(
assetHashWatchSrc,
options.assetHash.extraAssetDir.map((v) => {
return path.join(rootPath, v, "**", "*");
})
);
const watchAssetHash = gulp.series("actless:assetHash", function (cb) {
gulp.watch(assetHashWatchSrc, gulp.series("actless:assetHash"));
cb();
});
gulp.task("actless:assetHash:watch", watchAssetHash);
// define gulp tasks ===========================================
// compile
const mainParaTasks = ["actless:css"];
if (options.js.enabled) {
mainParaTasks.push("actless:js");
}
const compileMainTasks = [];
if (mainParaTasks.length) {
compileMainTasks.push(gulp.parallel.apply(null, mainParaTasks));
}
compileMainTasks.push("actless:assetHash");
if (options.ts.enabled) {
compileMainTasks.unshift("actless:ts");
}
if (options.webpack.enabled) {
compileMainTasks.push("actless:webpack");
}
const compileParaTasks = [gulp.series.apply(null, compileMainTasks)];
if (options.wig.enabled) {
compileParaTasks.push(
options.prettify.enabled
? gulp.series(
"actless:wig",
gulp.parallel("actless:prettify", "actless:nonPrettify")
)
: "actless:wig"
);
}
const runCompile =
compileParaTasks.length === 1
? compileParaTasks[0]
: gulp.parallel.apply(null, compileParaTasks);
gulp.task("actless:compile", runCompile);
// compile all
const runCompileFull = gulp.parallel(
"actless:compile",
"actless:icons:build"
);
gulp.task("actless:compile:full", runCompileFull);
// watch
const watchTasks = ["actless:css:watch"];
if (options.js.enabled) {
watchTasks.push("actless:js:watch");
}
watchTasks.push("actless:assetHash:watch");
if (options.wig.enabled) {
watchTasks.push("actless:wig:watch");
}
if (options.ts.enabled) {
watchTasks.push("actless:ts:watch");
}
if (options.webpack.enabled) {
watchTasks.push("actless:webpack:watch");
}
if (options.wig.enabled && options.prettify.enabled) {
watchTasks.push("actless:prettify:watch");
}
const runWatch = gulp.parallel.apply(null, watchTasks);
gulp.task("actless:watch", runWatch);
// watch all
const runWatchFull = gulp.parallel("actless:watch", "actless:icons:watch");
gulp.task("actless:watch:full", runWatchFull);
// default
var defaultTasks = ["actless:compile", "actless:watch"];
// full
var fullTasks = ["actless:compile:full", "actless:watch:full"];
if (options.server.type !== "none") {
defaultTasks.push("actless:server", "actless:server:open");
fullTasks.push("actless:server", "actless:server:open");
}
const runDefault = gulp.parallel.apply(null, defaultTasks);
const runDefaultFull = gulp.parallel.apply(null, fullTasks);
const runDev = (cb) => {
process.env.NODE_ENV = "development";
options.js.skipMinify = true;
cb();
};
gulp.task("actless:developmentMode", runDev);
gulp.task("actless:default", runDefault);
gulp.task("actless:full", runDefaultFull);
gulp.task(
"actless:dev",
gulp.series("actless:developmentMode", "actless:default")
);
gulp.task("default", runDefault);
gulp.task("full", runDefaultFull);
gulp.task("dev", gulp.series("actless:developmentMode", "actless:default"));
return gulp;
};
module.exports = actless;