diff --git a/packages/cli-util/index.js b/packages/cli-util/index.js new file mode 100644 index 000000000..16693715c --- /dev/null +++ b/packages/cli-util/index.js @@ -0,0 +1,36 @@ +const { blue, yellow, red } = require("kleur"); +const { normalize } = require("path"); +const { statSync, existsSync } = require("fs"); +const symbols = require("./symbols"); +const which = require("which"); + +exports.isDir = function(str) { + return existsSync(str) && statSync(str).isDirectory(); +}; + +exports.hasCommand = function(str) { + return !!which.sync(str, { nothrow: true }); +}; + +exports.trim = function(str) { + return str.trim().replace(/^\t+/gm, ""); +}; + +exports.info = function(text, code) { + process.stderr.write(symbols.info + blue(" INFO ") + text + "\n"); + code && process.exit(code); +}; + +exports.warn = function(text, code) { + process.stdout.write(symbols.warning + yellow(" WARN ") + text + "\n"); + code && process.exit(code); +}; + +exports.error = function(text, code) { + process.stderr.write(symbols.error + red(" ERROR ") + text + "\n"); + code && process.exit(code); +}; + +exports.normalizePath = function(str) { + return normalize(str).replace(/\\/g, "/"); +}; diff --git a/packages/cli-util/package.json b/packages/cli-util/package.json new file mode 100644 index 000000000..e60667954 --- /dev/null +++ b/packages/cli-util/package.json @@ -0,0 +1,19 @@ +{ + "name": "@preact/cli-util", + "version": "3.0.0-rc.5", + "description": "Preact-cli's util library", + "main": "index.js", + "files": [ + "*.js" + ], + "engines": { + "node": ">=6" + }, + "dependencies": { + "kleur": "^3.0.3", + "which": "^1.2.14" + }, + "peerDependencies": { + "preact": "^8.1.0" + } +} diff --git a/packages/cli/lib/symbols.js b/packages/cli-util/symbols.js similarity index 100% rename from packages/cli/lib/symbols.js rename to packages/cli-util/symbols.js diff --git a/packages/cli/lib/lib/webpack/create-load-manifest.js b/packages/cli-util/webpack/create-load-manifest.js similarity index 100% rename from packages/cli/lib/lib/webpack/create-load-manifest.js rename to packages/cli-util/webpack/create-load-manifest.js diff --git a/packages/cli/lib/commands/build.js b/packages/cli/lib/commands/build.js index ac4e5c8f0..93633694c 100644 --- a/packages/cli/lib/commands/build.js +++ b/packages/cli/lib/commands/build.js @@ -1,7 +1,7 @@ const rimraf = require('rimraf'); const { resolve } = require('path'); const { promisify } = require('util'); -const { isDir, error, warn } = require('../util'); +const { isDir, error, warn } = require('@preact/cli-util'); const runWebpack = require('../lib/webpack/run-webpack'); const toBool = val => val === void 0 || (val === 'false' ? false : val); diff --git a/packages/cli/lib/commands/create.js b/packages/cli/lib/commands/create.js index 25ac8eb37..15400e511 100644 --- a/packages/cli/lib/commands/create.js +++ b/packages/cli/lib/commands/create.js @@ -7,7 +7,7 @@ const { green } = require('kleur'); const { resolve, join } = require('path'); const { prompt } = require('prompts'); const isValidName = require('validate-npm-package-name'); -const { info, isDir, hasCommand, error, trim, warn } = require('../util'); +const { info, isDir, hasCommand, error, trim, warn } = require('@preact/cli-util'); const { addScripts, install, initGit, isMissing } = require('../lib/setup'); const ORG = 'preactjs-templates'; diff --git a/packages/cli/lib/commands/list.js b/packages/cli/lib/commands/list.js index 2346c5f23..e311df72e 100644 --- a/packages/cli/lib/commands/list.js +++ b/packages/cli/lib/commands/list.js @@ -1,6 +1,6 @@ const fetch = require('isomorphic-unfetch'); const { bold, magenta } = require('kleur'); -const { error, info } = require('../util'); +const { error, info } = require('@preact/cli-util'); const REPOS_URL = 'https://api.github.com/users/preactjs-templates/repos'; diff --git a/packages/cli/lib/commands/watch.js b/packages/cli/lib/commands/watch.js index 226cde090..6ef4f200f 100644 --- a/packages/cli/lib/commands/watch.js +++ b/packages/cli/lib/commands/watch.js @@ -1,5 +1,5 @@ const runWebpack = require('../lib/webpack/run-webpack'); -const { warn } = require('../util'); +const { warn } = require('@preact/cli-util'); module.exports = async function(src, argv) { argv.src = src || argv.src; diff --git a/packages/cli/lib/index.js b/packages/cli/lib/index.js index d651be2bb..d33dea4c4 100755 --- a/packages/cli/lib/index.js +++ b/packages/cli/lib/index.js @@ -2,7 +2,7 @@ const envinfo = require('envinfo'); const sade = require('sade'); const notifier = require('update-notifier'); -const { error } = require('./util'); +const { error } = require('@preact/cli-util'); const pkg = require('../package'); const ver = process.version; diff --git a/packages/cli/lib/lib/setup.js b/packages/cli/lib/lib/setup.js index 475e9c555..64a3a6bd3 100644 --- a/packages/cli/lib/lib/setup.js +++ b/packages/cli/lib/lib/setup.js @@ -1,5 +1,5 @@ const spawn = require('cross-spawn-promise'); -const { hasCommand, warn } = require('../util'); +const { hasCommand, warn } = require('@preact/cli-util'); const stdio = 'ignore'; diff --git a/packages/cli/lib/lib/webpack/push-manifest.js b/packages/cli/lib/lib/webpack/push-manifest.js index 968864432..a14504752 100644 --- a/packages/cli/lib/lib/webpack/push-manifest.js +++ b/packages/cli/lib/lib/webpack/push-manifest.js @@ -1,4 +1,4 @@ -const createLoadManifest = require('./create-load-manifest'); +const createLoadManifest = require('@preact/cli-util/webpack/create-load-manifest'); module.exports = class PushManifestPlugin { constructor(env = {}) { diff --git a/packages/cli/lib/lib/webpack/render-html-plugin.js b/packages/cli/lib/lib/webpack/render-html-plugin.js deleted file mode 100644 index 7a54bb093..000000000 --- a/packages/cli/lib/lib/webpack/render-html-plugin.js +++ /dev/null @@ -1,106 +0,0 @@ -const { resolve } = require('path'); -const { existsSync } = require('fs'); -const HtmlWebpackExcludeAssetsPlugin = require('html-webpack-exclude-assets-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const prerender = require('./prerender'); -const createLoadManifest = require('./create-load-manifest'); -const { warn } = require('../../util'); -const { info } = require('../../util'); -let template = resolve(__dirname, '../../resources/template.html'); - -module.exports = async function(config) { - const { cwd, dest, isProd, src } = config; - const inProjectTemplatePath = resolve(src, 'template.html'); - if (existsSync(inProjectTemplatePath)) { - template = inProjectTemplatePath; - } - const htmlWebpackConfig = values => { - const { url, title, ...routeData } = values; - return Object.assign(values, { - filename: resolve(dest, url.substring(1), 'index.html'), - template: `!!ejs-loader!${config.template || template}`, - minify: isProd && { - collapseWhitespace: true, - removeScriptTypeAttributes: true, - removeRedundantAttributes: true, - removeStyleLinkTypeAttributes: true, - removeComments: true, - }, - favicon: existsSync(resolve(src, 'assets/favicon.ico')) - ? 'assets/favicon.ico' - : '', - inject: true, - compile: true, - inlineCss: config['inline-css'], - preload: config.preload, - manifest: config.manifest, - title: - title || - config.title || - config.manifest.name || - config.manifest.short_name || - (config.pkg.name || '').replace(/^@[a-z]\//, '') || - 'Preact App', - excludeAssets: [/(bundle|polyfills)(\..*)?\.js$/], - createLoadManifest: (assets, namedChunkGroups) => { - if (assets['push-manifest.json']) { - return JSON.parse(assets['push-manifest.json'].source()); - } - return createLoadManifest(assets, config.esm, namedChunkGroups); - }, - config, - url, - ssr() { - return config.prerender ? prerender({ cwd, dest, src }, values) : ''; - }, - scriptLoading: 'defer', - CLI_DATA: { preRenderData: { url, ...routeData } }, - }); - }; - - let pages = [{ url: '/' }]; - - if (config.prerenderUrls) { - if (existsSync(resolve(cwd, config.prerenderUrls))) { - try { - let result = require(resolve(cwd, config.prerenderUrls)); - if (typeof result.default !== 'undefined') { - result = result.default(); - } - if (typeof result === 'function') { - info(`Fetching URLs from ${config.prerenderUrls}`); - result = await result(); - info(`Fetched URLs from ${config.prerenderUrls}`); - } - if (typeof result === 'string') { - result = JSON.parse(result); - } - if (result instanceof Array) { - pages = result; - } - } catch (error) { - warn( - `Failed to load prerenderUrls file, using default!\n${ - config.verbose ? error.stack : error.message - }` - ); - } - // don't warn if the default file doesn't exist - } else if ( - config.prerenderUrls !== 'prerender-urls.json' || - config.verbose - ) { - warn( - `prerenderUrls file (${resolve( - cwd, - config.prerenderUrls - )}) doesn't exist, using default!` - ); - } - } - - return pages - .map(htmlWebpackConfig) - .map(conf => new HtmlWebpackPlugin(conf)) - .concat([new HtmlWebpackExcludeAssetsPlugin()]); -}; diff --git a/packages/cli/lib/lib/webpack/run-webpack.js b/packages/cli/lib/lib/webpack/run-webpack.js index 525a0e5a4..5310da282 100644 --- a/packages/cli/lib/lib/webpack/run-webpack.js +++ b/packages/cli/lib/lib/webpack/run-webpack.js @@ -9,7 +9,7 @@ const DevServer = require('webpack-dev-server'); const clientConfig = require('./webpack-client-config'); const serverConfig = require('./webpack-server-config'); const transformConfig = require('./transform-config'); -const { error, isDir, warn } = require('../../util'); +const { error, isDir, warn } = require('@preact/cli-util'); async function devBuild(env) { let config = await clientConfig(env); diff --git a/packages/cli/lib/lib/webpack/sw-plugin.js b/packages/cli/lib/lib/webpack/sw-plugin.js index 0e43e5752..b78158f6d 100644 --- a/packages/cli/lib/lib/webpack/sw-plugin.js +++ b/packages/cli/lib/lib/webpack/sw-plugin.js @@ -3,7 +3,7 @@ const BabelEsmPlugin = require('babel-esm-plugin'); const { DefinePlugin } = require('webpack'); const fs = require('fs'); const { resolve } = require('path'); -const { info } = require('../../util'); +const { info } = require('@preact/cli-util'); class SWBuilderPlugin { constructor(config) { const { src, brotli, esm } = config; diff --git a/packages/cli/lib/lib/webpack/transform-config.js b/packages/cli/lib/lib/webpack/transform-config.js index ac5a7ae54..1c26acc94 100644 --- a/packages/cli/lib/lib/webpack/transform-config.js +++ b/packages/cli/lib/lib/webpack/transform-config.js @@ -1,7 +1,7 @@ const { resolve } = require('path'); const webpack = require('webpack'); const fs = require('../../fs'); -const { error } = require('../../util'); +const { error } = require('@preact/cli-util'); const FILE = 'preact.config'; const EXTENSIONS = ['js', 'json']; diff --git a/packages/cli/lib/lib/webpack/webpack-client-config.js b/packages/cli/lib/lib/webpack/webpack-client-config.js index fdceaa8d9..ad018d370 100644 --- a/packages/cli/lib/lib/webpack/webpack-client-config.js +++ b/packages/cli/lib/lib/webpack/webpack-client-config.js @@ -8,13 +8,13 @@ const TerserPlugin = require('terser-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const CrittersPlugin = require('critters-webpack-plugin'); -const renderHTMLPlugin = require('./render-html-plugin'); +const renderHTMLPlugin = require('@preact/render-html-plugin'); const PushManifestPlugin = require('./push-manifest'); const baseConfig = require('./webpack-base-config'); const BabelEsmPlugin = require('babel-esm-plugin'); const { InjectManifest } = require('workbox-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); -const { normalizePath } = require('../../util'); +const { normalizePath } = require('@preact/cli-util'); const SWBuilderPlugin = require('./sw-plugin'); const cleanFilename = name => @@ -86,7 +86,7 @@ async function clientConfig(env) { plugins: [ new PushManifestPlugin(env), - ...(await renderHTMLPlugin(env)), + ...(await renderHTMLPlugin({})(env)), ...getBabelEsmPlugin(env), new CopyWebpackPlugin( [ diff --git a/packages/cli/lib/util.js b/packages/cli/lib/util.js deleted file mode 100644 index 555db47d6..000000000 --- a/packages/cli/lib/util.js +++ /dev/null @@ -1,36 +0,0 @@ -const { blue, yellow, red } = require('kleur'); -const { normalize } = require('path'); -const { statSync, existsSync } = require('fs'); -const symbols = require('./symbols'); -const which = require('which'); - -exports.isDir = function(str) { - return existsSync(str) && statSync(str).isDirectory(); -}; - -exports.hasCommand = function(str) { - return !!which.sync(str, { nothrow: true }); -}; - -exports.trim = function(str) { - return str.trim().replace(/^\t+/gm, ''); -}; - -exports.info = function(text, code) { - process.stderr.write(symbols.info + blue(' INFO ') + text + '\n'); - code && process.exit(code); -}; - -exports.warn = function(text, code) { - process.stdout.write(symbols.warning + yellow(' WARN ') + text + '\n'); - code && process.exit(code); -}; - -exports.error = function(text, code) { - process.stderr.write(symbols.error + red(' ERROR ') + text + '\n'); - code && process.exit(code); -}; - -exports.normalizePath = function(str) { - return normalize(str).replace(/\\/g, '/'); -}; diff --git a/packages/cli/package.json b/packages/cli/package.json index 932e9ec13..87aac9148 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -77,6 +77,8 @@ "@babel/preset-env": "^7.5.5", "@babel/preset-typescript": "^7.3.3", "@preact/async-loader": "^3.0.0-rc.0", + "@preact/cli-util": "^3.0.0-rc.5", + "@preact/render-html-plugin": "^3.0.0-rc.5", "autoprefixer": "^9.6.0", "babel-esm-plugin": "^0.5.0", "babel-loader": "^8.0.6", @@ -98,8 +100,6 @@ "get-port": "^5.0.0", "gittar": "^0.1.0", "glob": "^7.1.4", - "html-webpack-exclude-assets-plugin": "0.0.7", - "html-webpack-plugin": "^3.2.0", "ip": "^1.1.5", "isomorphic-unfetch": "^3.0.0", "kleur": "^3.0.3", @@ -133,8 +133,7 @@ "webpack-dev-server": "^3.4.1", "webpack-fix-style-only-entries": "^0.3.0", "webpack-merge": "^4.1.0", - "webpack-plugin-replace": "^1.2.0", - "which": "^1.2.14", + "webpack-plugin-replace": "^1.2.0", "workbox-webpack-plugin": "^4.3.1" } } diff --git a/packages/render-html/index.js b/packages/render-html/index.js new file mode 100644 index 000000000..59abcc03e --- /dev/null +++ b/packages/render-html/index.js @@ -0,0 +1,128 @@ +const { resolve } = require('path'); +const { existsSync } = require('fs'); +const HtmlWebpackExcludeAssetsPlugin = require('html-webpack-exclude-assets-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const createLoadManifest = require('@preact/cli-util/webpack/create-load-manifest'); +const { warn, info } = require('@preact/cli-util'); + +const defaultConfig = { + template: 'template.html', // Template path from src + favicon: 'assets/favicon.ico', // Favicon path from src + prerender: './prerender', // path to the prerender function +}; + +module.exports = (config = {}) => + async function(env) { + const { template, favicon, prerender } = { + ...defaultConfig, + ...config, + }; + const { cwd, dest, isProd, src } = env; + let templatePath = [ + ...(env.template ? [[env.template], [src, env.template]] : []), + ...(template ? [[template], [src, template]] : []), + [src, 'template.html'], + ].find(xs => existsSync(resolve(...xs))); + + if (templatePath) { + info(`Using custom template from ${resolve(...templatePath)}`); + } else if (env.template || config.template) { + warn( + `Unable to find supplied template path ${env.template || + config.template}. Reverting to default template.` + ); + } + + templatePath = resolve( + ...(templatePath ? templatePath : [__dirname, './resources/template.ejs']) + ); + + const htmlWebpackConfig = values => { + const { url, title, ...routeData } = values; + return Object.assign(values, { + filename: resolve(dest, url.substring(1), 'index.html'), + template: `!!ejs-loader!${templatePath}`, + minify: isProd && { + collapseWhitespace: true, + removeScriptTypeAttributes: true, + removeRedundantAttributes: true, + removeStyleLinkTypeAttributes: true, + removeComments: true, + }, + favicon: existsSync(resolve(src, favicon)) + ? resolve(src, favicon) + : resolve(__dirname, './resources/favicon.ico'), + inject: true, + compile: true, + inlineCss: env['inline-css'], + preload: env.preload, + manifest: env.manifest, + title: + title || + env.title || + env.manifest.name || + env.manifest.short_name || + (env.pkg.name || '').replace(/^@[a-z]\//, '') || + 'Preact App', + excludeAssets: [/(bundle|polyfills)(\..*)?\.js$/], + createLoadManifest: (assets, namedChunkGroups) => { + if (assets['push-manifest.json']) { + return JSON.parse(assets['push-manifest.json'].source()); + } + return createLoadManifest(assets, env.esm, namedChunkGroups); + }, + config: env, + url, + ssr() { + return env.prerender + ? require(prerender)({ cwd, dest, src }, values) + : ''; + }, + scriptLoading: 'defer', + CLI_DATA: { preRenderData: { url, ...routeData } }, + }); + }; + + let pages = [{ url: '/' }]; + + if (env.prerenderUrls) { + if (existsSync(resolve(cwd, env.prerenderUrls))) { + try { + let result = require(resolve(cwd, env.prerenderUrls)); + if (typeof result.default !== 'undefined') { + result = result.default(); + } + if (typeof result === 'function') { + info(`Fetching URLs from ${env.prerenderUrls}`); + result = await result(); + info(`Fetched URLs from ${env.prerenderUrls}`); + } + if (typeof result === 'string') { + result = JSON.parse(result); + } + if (result instanceof Array) { + pages = result; + } + } catch (error) { + warn( + `Failed to load prerenderUrls file, using default!\n${ + env.verbose ? error.stack : error.message + }` + ); + } + // don't warn if the default file doesn't exist + } else if (env.prerenderUrls !== 'prerender-urls.json' || env.verbose) { + warn( + `prerenderUrls file (${resolve( + cwd, + env.prerenderUrls + )}) doesn't exist, using default!` + ); + } + } + + return pages + .map(htmlWebpackConfig) + .map(conf => new HtmlWebpackPlugin(conf)) + .concat([new HtmlWebpackExcludeAssetsPlugin()]); + }; diff --git a/packages/render-html/package.json b/packages/render-html/package.json new file mode 100644 index 000000000..46ab76237 --- /dev/null +++ b/packages/render-html/package.json @@ -0,0 +1,20 @@ +{ + "name": "@preact/render-html-plugin", + "version": "3.0.0-rc.5", + "description": "Preact's html renderer plugin for Webpack", + "main": "index.js", + "files": [ + "*.js" + ], + "engines": { + "node": ">=6" + }, + "dependencies": { + "@preact/cli-util": "^3.0.0-rc.5", + "html-webpack-exclude-assets-plugin": "0.0.7", + "html-webpack-plugin": "^3.2.0" + }, + "peerDependencies": { + "preact": "^8.1.0" + } +} diff --git a/packages/cli/lib/lib/webpack/prerender.js b/packages/render-html/prerender.js similarity index 100% rename from packages/cli/lib/lib/webpack/prerender.js rename to packages/render-html/prerender.js diff --git a/packages/render-html/resources/favicon.ico b/packages/render-html/resources/favicon.ico new file mode 100644 index 000000000..f6da43368 Binary files /dev/null and b/packages/render-html/resources/favicon.ico differ diff --git a/packages/render-html/resources/template.ejs b/packages/render-html/resources/template.ejs new file mode 100644 index 000000000..28781915a --- /dev/null +++ b/packages/render-html/resources/template.ejs @@ -0,0 +1,42 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + <% if (htmlWebpackPlugin.options.manifest.theme_color) { %> + + <% } %> + <% const loadManifest = htmlWebpackPlugin.options.createLoadManifest(compilation.assets, webpack.namedChunkGroups);%> + <% const filesRegexp = htmlWebpackPlugin.options.inlineCss ? /\.(chunk\.\w{5}\.css|js)$/ : /\.(css|js)$/;%> + <% for (const file in loadManifest[htmlWebpackPlugin.options.url]) { %> + <% if (htmlWebpackPlugin.options.preload && file && file.match(filesRegexp)) { %> + <% /* crossorigin for main bundle as that is loaded from ` + + <% + /*Fetch and Promise polyfills are not needed for browsers that support type=module + Please re-evaluate below line if adding more polyfills.*/ + %> + + + <% } else { %> + + + <% } %> + + +