UNPKG

htmlgaga

Version:

Manage non-SPA pages with webpack and React.js

938 lines (880 loc) 34.8 kB
"use strict"; function _interopDefault(ex) { return ex && "object" == typeof ex && "default" in ex ? ex.default : ex; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = {}; return e && Object.keys(e).forEach((function(k) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: !0, get: function() { return e[k]; } }); })), n.default = e, n; } var yargs = _interopDefault(require("yargs")), rimraf = _interopDefault(require("rimraf")), fs = require("fs"), path = require("path"), path__default = _interopDefault(path), pino = _interopDefault(require("pino")), MiniCssExtractPlugin = _interopDefault(require("mini-css-extract-plugin")), rehypePrism = _interopDefault(require("@mapbox/rehype-prism")), webpack = _interopDefault(require("webpack")), express = _interopDefault(require("express")), devMiddleware = _interopDefault(require("webpack-dev-middleware")), http = _interopDefault(require("http")), fs$1 = _interopDefault(require("fs-extra")), HtmlWebpackPlugin = _interopDefault(require("html-webpack-plugin")), CssoWebpackPlugin = _interopDefault(require("csso-webpack-plugin")), WebpackAssetsManifest = _interopDefault(require("webpack-manifest-plugin")), TerserJSPlugin = _interopDefault(require("terser-webpack-plugin")), prettier = _interopDefault(require("prettier")), react = require("react"), server = require("react-dom/server"), HtmlTags = _interopDefault(require("html-webpack-plugin/lib/html-tags")), tapable = require("tapable"), merge = _interopDefault(require("lodash.merge")), MessageType = require("./MessageType-08992331.cjs.prod.js"), WebSocket = _interopDefault(require("ws")); const cwd = process.cwd(), logger = pino({ level: process.env.LOG_LEVEL || "info", prettyPrint: { translateTime: "HH:MM:ss", ignore: "pid,hostname" }, serializers: { err: pino.stdSerializers.err, error: pino.stdSerializers.err } }), assetsRoot = "static", alias = { img: path.resolve(cwd, "static/img"), css: path.resolve(cwd, "static/css"), js: path.resolve(cwd, "static/js") }, publicFolder = "public", extensions = [ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".json" ], performance = require("perf_hooks").performance, PerformanceObserver = require("perf_hooks").PerformanceObserver, cacheRoot = path.join(cwd, ".htmlgaga", "cache"), babelPresets = [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], rules = [ { test: /\.(js|jsx|ts|tsx|mjs)$/i, exclude: /node_modules/, use: [ { loader: "babel-loader", options: { presets: [ ...babelPresets ], plugins: [ "react-require" ], cacheDirectory: !0, cacheCompression: !1 } } ] }, { test: /\.(md|mdx)$/i, use: [ { loader: "babel-loader", options: { presets: [ ...babelPresets ], plugins: [ "react-require" ], cacheDirectory: !0, cacheCompression: !1 } }, { loader: "@mdx-js/loader", options: { rehypePlugins: [ rehypePrism ] } } ] }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, loader: "file-loader", options: { name: "[name].[contenthash].[ext]" } }, { test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, use: [ { loader: "file-loader", options: { name: "[name].[ext]", outputPath: "fonts/" } } ] }, { test: /\.(sa|sc|c)ss$/i, use: [ { loader: MiniCssExtractPlugin.loader }, "css-loader", { loader: "postcss-loader", options: { ident: "postcss", plugins: [ require("tailwindcss"), require("autoprefixer") ] } }, "sass-loader" ] } ]; function isHtmlRequest(url) { return !!url.endsWith("/") || !!/\.html$/.test(url); } function deriveFilenameFromRelativePath(from, to) { const relativePath = path.relative(from, to), {ext: ext} = path.parse(relativePath); return relativePath.replace(ext, ".html"); } const doNotFilter = () => !0; async function collect(root = cwd, filter = doNotFilter, acc = []) { const files = await fs.promises.readdir(root); for (const file of files) { const filePath = path.resolve(root, file), stats = await fs.promises.stat(filePath); if (stats.isFile()) { const f = filePath; filter(f) && acc.push(f); } else stats.isDirectory() && await collect(filePath, filter, acc); } return acc; } function _classPrivateFieldGet(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to get private field on non-instance"); return descriptor.get ? descriptor.get.call(receiver) : descriptor.value; } function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to set private field on non-instance"); if (descriptor.set) descriptor.set.call(receiver, value); else { if (!descriptor.writable) throw new TypeError("attempted to set read only private field"); descriptor.value = value; } return value; } var _options = new WeakMap; class PrettyPlugin { constructor(options) { _options.set(this, { writable: !0, value: void 0 }), _classPrivateFieldSet(this, _options, options); } apply(compiler) { compiler.hooks.emit.tap("PrettyPlugin", compilation => { var _classPrivateFieldGet2; (null === (_classPrivateFieldGet2 = _classPrivateFieldGet(this, _options).html) || void 0 === _classPrivateFieldGet2 ? void 0 : _classPrivateFieldGet2.pretty) && Object.keys(compilation.assets).forEach(asset => { if (asset.endsWith(".html")) { const source = compilation.assets[asset].source(), prettyHtml = prettier.format(Buffer.isBuffer(source) ? source.toString() : source, { parser: "html" }); compilation.assets[asset] = { source: () => prettyHtml, size: () => prettyHtml.length }; } }); }); } } var _pagesDir = new WeakMap, _outputPath = new WeakMap, _clients = new WeakMap, _config = new WeakMap; class ClientsCompiler { constructor(pagesDir, outputPath, config) { _pagesDir.set(this, { writable: !0, value: void 0 }), _outputPath.set(this, { writable: !0, value: void 0 }), _clients.set(this, { writable: !0, value: void 0 }), _config.set(this, { writable: !0, value: void 0 }), _classPrivateFieldSet(this, _pagesDir, pagesDir), _classPrivateFieldSet(this, _outputPath, outputPath), _classPrivateFieldSet(this, _config, config); } createWebpackConfig(entry) { var _ref, _classPrivateFieldGet3; const relative = path__default.relative(_classPrivateFieldGet(this, _pagesDir), entry), outputHtml = relative.replace(/\.client\.(js|ts)$/, ".html"); return { experiments: { asset: !0 }, mode: "production", optimization: { minimize: !0, minimizer: [ new TerserJSPlugin({ terserOptions: {}, extractComments: !1 }) ], splitChunks: { cacheGroups: { vendors: path__default.resolve(cwd, "node_modules") } } }, entry: { [relative.replace(/\.client.*/, "").split(path__default.sep).join("-")]: entry }, output: { ecmaVersion: 5, path: path__default.resolve(_classPrivateFieldGet(this, _outputPath)), filename: "[name].[contenthash].js", chunkFilename: "[name]-[id].[contenthash].js", publicPath: null != ASSET_PATH ? ASSET_PATH : _classPrivateFieldGet(this, _config).assetPath }, module: { rules: rules }, resolve: { extensions: extensions, alias: alias }, externals: _classPrivateFieldGet(this, _config).globalScripts ? _classPrivateFieldGet(this, _config).globalScripts.map(script => ({ [script[0]]: script[1].global })) : [], plugins: [ new HtmlWebpackPlugin({ template: path__default.resolve(_classPrivateFieldGet(this, _outputPath), outputHtml), filename: outputHtml, minify: null === (_ref = null === (_classPrivateFieldGet3 = _classPrivateFieldGet(this, _config).html) || void 0 === _classPrivateFieldGet3 ? void 0 : _classPrivateFieldGet3.pretty) || void 0 === _ref || _ref }), new webpack.DefinePlugin({ '"production"': '"production"' }), new CssoWebpackPlugin({ restructure: !1 }), new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }), new WebpackAssetsManifest({ fileName: "client-assets.json", generate: generateManifest }), new PrettyPlugin(_classPrivateFieldGet(this, _config)) ] }; } async run(callback) { _classPrivateFieldSet(this, _clients, await collect(_classPrivateFieldGet(this, _pagesDir), filename => filename.endsWith(".client.js") || filename.endsWith(".client.ts"))); const configs = _classPrivateFieldGet(this, _clients).map(client => this.createWebpackConfig(client)); webpack(configs).run(callback); } } function Render(App) { return server.renderToStaticMarkup(react.createElement(App)); } function hasClientEntry(pageEntry, exts = "js,ts".split(",")) { const {name: name, dir: dir} = path__default.parse(pageEntry); for (let i = 0; i < exts.length; i++) { const ext = exts[i], clientEntry = path__default.join(dir, `${name}.client.${ext}`); if (fs$1.existsSync(clientEntry)) return { exists: !0, clientEntry: clientEntry }; } return { exists: !1 }; } function _defineProperty(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } const {htmlTagObjectToString: htmlTagObjectToString} = HtmlTags; async function loadHtmlTags(root, filename) { try { const {headTags: headTags, bodyTags: bodyTags} = await new Promise((function(resolve) { resolve(_interopNamespace(require(path__default.resolve(root, filename)))); })); return { headTags: headTags, bodyTags: bodyTags }; } catch (err) { return { headTags: [], bodyTags: [] }; } } async function loadAllHtmlTags(root, files) { return Promise.all(files.map(async file => await loadHtmlTags(root, file))).then(values => values.reduce((acc, cur) => (acc.headTags = acc.headTags.concat(cur.headTags), acc.bodyTags = acc.bodyTags.concat(cur.bodyTags), acc), { headTags: [], bodyTags: [] })); } class Ssr { constructor() { _defineProperty(this, "hooks", void 0), _defineProperty(this, "helmet", void 0), this.hooks = { helmet: new tapable.SyncHook }; } async run(pagesDir, templateName, cacheRoot, outputPath, htmlgagaConfig) { const htmlTags = await loadAllHtmlTags(cacheRoot, [ `${templateName}.json` ]); let {headTags: headTags} = htmlTags, {bodyTags: bodyTags} = htmlTags; htmlgagaConfig.globalScripts && (headTags = htmlgagaConfig.globalScripts.map(script => { const {global: global, ...others} = script[1]; return { tagName: "script", voidTag: !1, attributes: { ...others } }; }).concat(headTags)), bodyTags = []; let preloadStyles = ""; htmlgagaConfig.html.preload.style && (preloadStyles = headTags.filter(tag => "link" === tag.tagName).map(tag => `<link rel="preload" href="${tag.attributes.href}" as="${"stylesheet" === tag.attributes.rel ? "style" : ""}" />`).join("")); let preloadScripts = ""; htmlgagaConfig.html.preload.script && (preloadScripts = bodyTags.filter(tag => "script" === tag.tagName).concat(headTags.filter(tag => "script" === tag.tagName)).map(tag => `<link rel="preload" href="${tag.attributes.src}" as="script" />`).join("")); const hd = headTags.map(tag => htmlTagObjectToString(tag, !0)).join(""), bd = bodyTags.map(tag => htmlTagObjectToString(tag, !0)).join(""), appPath = `${path__default.resolve(outputPath, templateName + ".js")}`, {default: App} = await new Promise((function(resolve) { resolve(_interopNamespace(require(appPath))); })), html = Render(App); let body; var _htmlgagaConfig$html; (this.hooks.helmet.call(), body = this.helmet ? `<!DOCTYPE html><html ${this.helmet.htmlAttributes.toString()}><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /><meta name="generator" content="htmlgaga" />${this.helmet.title.toString()}${this.helmet.meta.toString()}${this.helmet.link.toString()}${preloadStyles}${preloadScripts}${hd}</head><body>${html}${bd}</body></html>` : `<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /><meta name="generator" content="htmlgaga" />${preloadStyles}${preloadScripts}${hd}</head><body>${html}${bd}</body></html>`, !1 === hasClientEntry(path__default.join(pagesDir, templateName)).exists) && (!0 === (null == htmlgagaConfig || null === (_htmlgagaConfig$html = htmlgagaConfig.html) || void 0 === _htmlgagaConfig$html ? void 0 : _htmlgagaConfig$html.pretty) && (body = prettier.format(body, { parser: "html" }))); fs$1.outputFileSync(path__default.join(outputPath, templateName + ".html"), body), fs$1.removeSync(appPath); } } function normalizeAssetPath() { const ASSET_PATH = process.env.ASSET_PATH; if (void 0 !== ASSET_PATH) return ASSET_PATH.endsWith("/") ? ASSET_PATH : ASSET_PATH + "/"; } function _defineProperty$1(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } const defaultConfiguration = { html: { pretty: !0, preload: { style: !0, script: !0 } }, plugins: [], assetPath: "" }, configuration = "htmlgaga.config.js"; class Builder { constructor(pagesDir) { _defineProperty$1(this, "pagesDir", void 0), _defineProperty$1(this, "config", void 0), this.pagesDir = pagesDir; } applyOptionsDefaults() { var _this$config$html, _this$config$plugins; this.config = { ...defaultConfiguration, ...this.config, html: merge({}, defaultConfiguration.html, null !== (_this$config$html = this.config.html) && void 0 !== _this$config$html ? _this$config$html : {}), plugins: merge([], defaultConfiguration.plugins, null !== (_this$config$plugins = this.config.plugins) && void 0 !== _this$config$plugins ? _this$config$plugins : []) }; } async resolveConfig() { const configName = path__default.resolve(this.pagesDir, "..", configuration); let config; try { config = await new Promise((function(resolve) { resolve(_interopNamespace(require(configName))); })); } catch (err) { config = {}; } this.config = config, this.applyOptionsDefaults(), logger.debug(configuration, this.config); } } function _defineProperty$2(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } class PersistDataPlugin { apply(compiler) { compiler.hooks.compilation.tap(PersistDataPlugin.PluginName, compilation => { HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync(PersistDataPlugin.PluginName, (htmlPluginData, callback) => { fs$1.outputJSON(path__default.join(cacheRoot, `${htmlPluginData.outputName.replace(/\.html$/, "")}.json`), { headTags: htmlPluginData.headTags, bodyTags: htmlPluginData.bodyTags }, () => { callback(null, htmlPluginData); }); }); }); } } function _defineProperty$3(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } _defineProperty$2(PersistDataPlugin, "PluginName", "PersistDataPlugin"); const NAME = "RemoveAssetsPlugin"; class RemoveAssetsPlugin { constructor(filter, callback) { _defineProperty$3(this, "filter", void 0), _defineProperty$3(this, "callback", void 0), this.filter = filter, this.callback = callback; } apply(compiler) { compiler.hooks.compilation.tap(NAME, compilation => { compilation.hooks.processAssets.tap(NAME, assets => { Object.keys(assets).forEach(filename => { this.filter(filename) && (delete assets[filename], this.callback && this.callback(filename)); }); }); }); } } function _defineProperty$4(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } function _classPrivateFieldGet$1(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to get private field on non-instance"); return descriptor.get ? descriptor.get.call(receiver) : descriptor.value; } function _classPrivateFieldSet$1(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to set private field on non-instance"); if (descriptor.set) descriptor.set.call(receiver, value); else { if (!descriptor.writable) throw new TypeError("attempted to set read only private field"); descriptor.value = value; } return value; } function generateManifest(seed, files, entrypoints) { return { files: files.reduce((manifest, {name: name, path: path}) => ({ ...manifest, [name]: path }), seed), entrypoints: entrypoints }; } const BEGIN = "begin", END = "end", ASSET_PATH = normalizeAssetPath(); var _pages = new WeakMap, _outputPath$1 = new WeakMap, _pageEntries = new WeakMap; class ProdBuilder extends Builder { constructor(pagesDir, outputPath) { super(pagesDir), _pages.set(this, { writable: !0, value: void 0 }), _outputPath$1.set(this, { writable: !0, value: void 0 }), _defineProperty$4(this, "config", void 0), _pageEntries.set(this, { writable: !0, value: void 0 }), _classPrivateFieldSet$1(this, _outputPath$1, outputPath), _classPrivateFieldSet$1(this, _pageEntries, []); } normalizedPageEntry(pagePath) { return path.relative(this.pagesDir, pagePath).replace(new RegExp(`\\${path.extname(pagePath)}$`), ""); } createWebpackConfig(pages) { const entries = pages.reduce((acc, page) => { const pageEntryKey = this.normalizedPageEntry(page); return _classPrivateFieldGet$1(this, _pageEntries).push(pageEntryKey), acc[pageEntryKey] = page, acc; }, {}), htmlPlugins = pages.map(page => { const filename = deriveFilenameFromRelativePath(this.pagesDir, page); return new HtmlWebpackPlugin({ chunks: [ this.normalizedPageEntry(page) ], filename: filename, minify: !1, inject: !1, cache: !1, showErrors: !1, meta: !1 }); }); return { experiments: { asset: !0 }, externals: [ "react-helmet", "react", "react-dom" ], mode: "production", entry: { ...entries }, optimization: { minimize: !1 }, output: { ecmaVersion: 5, path: path.resolve(_classPrivateFieldGet$1(this, _outputPath$1)), libraryTarget: "commonjs2", filename: pathData => { var _pathData$chunk, _pathData$chunk2; if ((null == pathData || null === (_pathData$chunk = pathData.chunk) || void 0 === _pathData$chunk ? void 0 : _pathData$chunk.name) && entries[null == pathData || null === (_pathData$chunk2 = pathData.chunk) || void 0 === _pathData$chunk2 ? void 0 : _pathData$chunk2.name]) return "[name].js"; return "[name].[contenthash].js"; }, chunkFilename: "[name]-[id].[contenthash].js", publicPath: null != ASSET_PATH ? ASSET_PATH : this.config.assetPath }, module: { rules: rules }, resolve: { extensions: extensions, alias: alias }, plugins: [ new PersistDataPlugin, new WebpackAssetsManifest({ fileName: "assets.json", generate: generateManifest }), new webpack.DefinePlugin({ '"production"': '"production"' }), new CssoWebpackPlugin({ restructure: !1 }), ...htmlPlugins, new RemoveAssetsPlugin(filename => -1 !== _classPrivateFieldGet$1(this, _pageEntries).indexOf(filename.replace(".html", "")), filename => logger.debug(`${filename} removed by RemoveAssetsPlugin`)), new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }) ] }; } runCallback(err, stats) { if (err) return err.stack ? logger.error(err.stack) : logger.error(err), void (err.details && logger.error(err.details)); if (!stats) return; const info = stats.toJson(); stats.hasErrors() && info.errors.forEach(err => logger.error(err.message)), stats.hasWarnings() && info.warnings.forEach(warning => logger.warn(warning.message)); } markEnd() { performance.mark(END), performance.measure("begin to end", BEGIN, END); new PerformanceObserver((list, observer) => { logger.info(`All ${this.pageOrPages(_classPrivateFieldGet$1(this, _pages).length)} built in ${(list.getEntries()[0].duration / 1e3).toFixed(2)}s!`), observer.disconnect(); }).observe({ entryTypes: [ "measure" ] }), performance.measure("Build time", BEGIN, END); } markBegin() { performance.mark(BEGIN); } pageOrPages(len) { return len < 2 ? len + " page" : len + " pages"; } async ssr() { for (const templateName of _classPrivateFieldGet$1(this, _pageEntries)) { const ssr = new Ssr; if (Array.isArray(this.config.plugins)) for (const plugin of this.config.plugins) plugin.apply(ssr); ssr.run(this.pagesDir, templateName, cacheRoot, _classPrivateFieldGet$1(this, _outputPath$1), this.config); } } async run() { this.markBegin(), logger.info("Collecting pages..."), _classPrivateFieldSet$1(this, _pages, await collect(this.pagesDir, searchPageEntry)), logger.info(`${this.pageOrPages(_classPrivateFieldGet$1(this, _pages).length)} collected`), await this.resolveConfig(), webpack(this.createWebpackConfig(_classPrivateFieldGet$1(this, _pages))).run(async (err, stats) => { this.runCallback(err, stats), await this.ssr(); const clientJsCompiler = new ClientsCompiler(this.pagesDir, _classPrivateFieldGet$1(this, _outputPath$1), this.config); await clientJsCompiler.run((err, stats) => { this.runCallback(err, stats), this.cleanCache(), fs$1.copySync(path.join(this.pagesDir, "..", "public"), _classPrivateFieldGet$1(this, _outputPath$1)), this.markEnd(); }); }); } cleanCache() { fs$1.removeSync(cacheRoot); } } const exts = "mjs,js,jsx,ts,tsx,md,mdx"; function searchPageEntry(pagePath, extList = exts) { return new RegExp(`.(${extList.split(",").join("|")})$`).test(pagePath) && extList.split(",").every(ext => !1 === pagePath.includes(`.client.${ext}`)); } function findRawFile(sourceDir, url, extList = exts.split(",")) { url.endsWith("/") && (url += "/index.html"); for (let i = 0; i < extList.length; i++) { const searchExt = extList[i], rawFilePath = path.join(sourceDir, url.replace(/\.html$/, `.${searchExt}`)); if (fs$1.existsSync(rawFilePath)) return { src: rawFilePath, exists: !0 }; } return { exists: !1 }; } function deriveEntryKeyFromRelativePath(from, to) { const relativePath = path.relative(from, to), {base: base, name: name} = path.parse(relativePath); return path.join(relativePath.replace(base, "") + name + "/index"); } const vendors = { "react-vendors": [ "react", "react-dom" ] }, socketClient = `${require.resolve("../Client")}`; function createEntries(pagesDir, pages, vendors$1 = vendors) { return { ...pages.reduce((acc, page) => { const entryKey = deriveEntryKeyFromRelativePath(pagesDir, page), hasClientJs = hasClientEntry(page), entry = { [entryKey]: { import: [ socketClient, page ], dependOn: Object.keys(vendors$1) } }; return !0 === hasClientJs.exists && (entry[deriveEntryKeyFromRelativePath(pagesDir, hasClientJs.clientEntry)] = { import: [ hasClientJs.clientEntry ], dependOn: [ entryKey ] }), acc = { ...acc, ...entry }; }, {}), ...vendors$1 }; } const createDOMRenderRule = pagesDir => ({ include: filename => filename.startsWith(pagesDir) && !1 === filename.includes(".client."), plugins: [ [ "react-dom-render", { hydrate: !1, root: "htmlgaga-app" } ] ] }); function createWebpackConfig(pages, pagesDir, socketUrl, options) { return { experiments: { asset: !0 }, mode: "development", entry: () => createEntries(pagesDir, pages), output: { ecmaVersion: 5, publicPath: "/" }, stats: "minimal", module: { rules: [ { test: /\.(mjs|js|jsx|ts|tsx)$/i, exclude: [ /node_modules/ ], use: [ { loader: "babel-loader", options: { presets: [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], plugins: [ "react-require" ], overrides: [ createDOMRenderRule(pagesDir) ] } } ] }, { test: /\.(mdx|md)$/, use: [ { loader: "babel-loader", options: { presets: [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], plugins: [ "react-require" ], overrides: [ createDOMRenderRule(pagesDir) ] } }, { loader: "@mdx-js/loader", options: { rehypePlugins: [ rehypePrism ] } } ] }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset" }, { test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, use: [ { loader: "file-loader", options: { name: "[name].[ext]", outputPath: "fonts/" } } ] }, { test: /\.(sa|sc|c)ss$/i, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { ident: "postcss", plugins: [ require("tailwindcss"), require("autoprefixer") ] } }, "sass-loader" ] } ] }, resolve: { extensions: extensions, alias: alias }, plugins: [ new webpack.DefinePlugin({ '"production"': '"development"', __WEBSOCKET__: JSON.stringify(socketUrl) }), new webpack.NoEmitOnErrorsPlugin ], ...options }; } function newHtmlWebpackPlugin(pagesDir, page, vendors$1 = vendors) { const htmlFilename = deriveFilenameFromRelativePath(pagesDir, page), entryKey = deriveEntryKeyFromRelativePath(pagesDir, page), hasClientJs = hasClientEntry(page); return new HtmlWebpackPlugin({ template: require.resolve("../devTemplate"), chunks: !0 === hasClientJs.exists ? [ ...Object.keys(vendors$1), entryKey, deriveEntryKeyFromRelativePath(pagesDir, hasClientJs.clientEntry) ] : [ ...Object.keys(vendors$1), entryKey ], chunksSortMode: "manual", filename: htmlFilename }); } function watchCompilation(compiler, wsServer) { if (!wsServer) return; compiler.hooks.done.tap("htmlgaga-reload", stats => { const statsJson = stats.toJson({ all: !1, hash: !0, assets: !0, warnings: !0, errors: !0, errorDetails: !1 }), hasErrors = stats.hasErrors(), hasWarnings = stats.hasWarnings(); wsServer.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) return client.send(JSON.stringify({ type: MessageType.MessageType.HASH, data: { hash: statsJson.hash, startTime: stats.startTime, endTime: stats.endTime } })), hasErrors ? (console.log(statsJson.errors), client.send(JSON.stringify({ type: MessageType.MessageType.ERRORS, data: statsJson.errors }))) : hasWarnings ? client.send(JSON.stringify({ type: MessageType.MessageType.WARNINGS, data: statsJson.warnings })) : void client.send(JSON.stringify({ type: MessageType.MessageType.RELOAD })); }); }), compiler.hooks.invalid.tap("htmlgaga-reload", () => { wsServer.clients.forEach(client => { client.readyState === WebSocket.OPEN && client.send(JSON.stringify({ type: MessageType.MessageType.INVALID })); }); }); } function createWebSocketServer(httpServer, socketPath) { const wsServer = new WebSocket.Server({ server: httpServer, path: socketPath }); function cleanup() { wsServer.close(() => { process.exit(1); }); } return wsServer.on("connection", socket => { socket.on("message", data => { console.log(`${data} from client`); }); }), wsServer.on("close", () => { console.log("closed"); }), process.on("SIGINT", cleanup), process.on("SIGTERM", cleanup), wsServer; } function _defineProperty$5(obj, key, value) { return key in obj ? Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }) : obj[key] = value, obj; } const PLUGIN_NAME = "InjectGlobalScripts"; class InjectGlobalScriptsPlugin { constructor(scripts) { _defineProperty$5(this, "scripts", void 0), this.scripts = scripts; } apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => { const globalScripts = this.scripts.map(script => ({ tagName: "script", voidTag: !1, attributes: { src: script } })); callback(null, { ...htmlPluginData, headTags: htmlPluginData.headTags.concat(globalScripts) }); }); }); } } function _classPrivateFieldGet$2(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to get private field on non-instance"); return descriptor.get ? descriptor.get.call(receiver) : descriptor.value; } function _classPrivateFieldSet$2(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) throw new TypeError("attempted to set private field on non-instance"); if (descriptor.set) descriptor.set.call(receiver, value); else { if (!descriptor.writable) throw new TypeError("attempted to set read only private field"); descriptor.value = value; } return value; } var _host = new WeakMap, _port = new WeakMap, _pages$1 = new WeakMap, _httpServer = new WeakMap; class DevServer extends Builder { constructor(pagesDir, {host: host, port: port}) { super(pagesDir), _host.set(this, { writable: !0, value: void 0 }), _port.set(this, { writable: !0, value: void 0 }), _pages$1.set(this, { writable: !0, value: void 0 }), _httpServer.set(this, { writable: !0, value: void 0 }), _classPrivateFieldSet$2(this, _host, host), _classPrivateFieldSet$2(this, _port, port), _classPrivateFieldSet$2(this, _pages$1, []); } listen() { _classPrivateFieldGet$2(this, _httpServer).listen(_classPrivateFieldGet$2(this, _port), _classPrivateFieldGet$2(this, _host), () => { const server = `http://${_classPrivateFieldGet$2(this, _host)}:${_classPrivateFieldGet$2(this, _port)}`; console.log(`Listening on ${server}`); }).on("error", err => { throw logger.info("You might run server on another port with option like --port 9999"), err; }); } async start() { await this.resolveConfig(); const webpackConfig = createWebpackConfig(_classPrivateFieldGet$2(this, _pages$1), this.pagesDir, `${_classPrivateFieldGet$2(this, _host)}:${_classPrivateFieldGet$2(this, _port)}/__websocket`, { externals: this.config.globalScripts ? this.config.globalScripts.reduce((acc, cur) => (acc[cur[0]] = cur[1].global, acc), {}) : [] }), compiler = webpack(webpackConfig), devMiddlewareInstance = devMiddleware(compiler), app = express(); app.use((pagesDir => (req, res, next) => { if (isHtmlRequest(req.url)) { const page = findRawFile(pagesDir, req.url); if (page.exists) { const src = page.src; _classPrivateFieldGet$2(this, _pages$1).includes(src) || (_classPrivateFieldGet$2(this, _pages$1).push(src), newHtmlWebpackPlugin(pagesDir, src).apply(compiler), new InjectGlobalScriptsPlugin(this.config.globalScripts ? this.config.globalScripts.map(script => script[1].src) : []).apply(compiler), devMiddlewareInstance.invalidate()); } } next(); })(this.pagesDir)), app.use(devMiddlewareInstance); const cwd = path.resolve(this.pagesDir, ".."); app.use(express.static(path.join(cwd, "public"))), app.use((function(req, res, next) { if (req.is("html")) return res.status(404).end("Page Not Found"); next(); })), _classPrivateFieldSet$2(this, _httpServer, http.createServer(app)); const wsServer = createWebSocketServer(_classPrivateFieldGet$2(this, _httpServer), "/__websocket"); return watchCompilation(compiler, wsServer), this.listen(), app; } } yargs.scriptName("htmlgaga").usage("$0 <cmd> [args]").command("dev", "Run development server", { host: { default: "localhost", description: "Host to run server" }, port: { default: 8080, description: "Port to run server" } }, (async function(argv) { const {host: host, port: port} = argv, pagesDir = path.resolve(cwd, "pages"); new DevServer(pagesDir, { host: host, port: port }).start(); })).command("build", "Build static html & assets", { dest: { default: "out", description: "The output directory" } }, (function(argv) { const pagesDir = path.resolve(cwd, "pages"); if (!fs.existsSync(pagesDir)) throw new Error("Couldn't find a `pages` directory. Make sure you have it under the project root"); const {dest: dest} = argv, outDir = path.resolve(cwd, dest); rimraf(outDir, async err => { if (err) return logger.error(err); rimraf(path.resolve(cwd, ".htmlgaga"), async err => { if (err) return logger.error(err); const builder = new ProdBuilder(pagesDir, outDir); process.env.NODE_ENV = "production", await builder.run(); }); }); })).help().argv;