UNPKG

@mapbox/batfish

Version:

The React-powered static-site generator you didn't know you wanted

456 lines (429 loc) 12.8 kB
// 'use strict'; const _ = require('lodash'); const del = require('del'); const path = require('path'); const mkdirp = require('mkdirp'); const chalk = require('chalk'); const a = require('indefinite'); const isGlob = require('is-glob'); const isAbsoluteUrl = require('is-absolute-url'); const pathType = require('path-type'); const autoprefixer = require('autoprefixer'); const errorTypes = require('./error-types'); const joinUrlParts = require('./join-url-parts'); const getEnvBrowserslist = require('./get-env-browserslist'); // !!! // Whenever you add a new configuration property, // you'll need to add it to this schema. const configSchema = { siteBasePath: { validator: _.isString, description: 'string' }, siteOrigin: { validator: _.isString, description: 'string' }, publicAssetsPath: { validator: _.isString, description: 'string' }, applicationWrapperPath: { validator: isAbsolutePathToExistingFile, description: 'absolute path to an existing file' }, stylesheets: { validator: isArrayOf(isValidStylesheetItem), description: 'array of absolute file paths or globs, absolute URLs, or arrays of these things' }, browserslist: { validator: (x) => _.isString(x) || isArrayOf(_.isString)(x), description: 'string or array of strings' }, devBrowserslist: { validator: (x) => _.isString(x) || isArrayOf(_.isString)(x) || x === false, description: 'string, array of strings, or false' }, pagesDirectory: { validator: isAbsolutePathToExistingDirectory, description: 'absolute path to an existing directory' }, outputDirectory: { validator: isAbsolutePath, description: 'absolute path' }, temporaryDirectory: { validator: isAbsolutePath, description: 'absolute path' }, dataSelectors: { validator: (x) => _.isPlainObject(x) && _.every(x, _.isFunction), description: 'object whose values are functions' }, vendorModules: { validator: isArrayOf(_.isString), description: 'array of strings' }, hijackLinks: { validator: _.isBoolean, description: 'boolean' }, manageScrollRestoration: { validator: _.isBoolean, description: 'boolean' }, webpackLoaders: { validator: isArrayOf(_.isPlainObject), description: 'array of Webpack Rule objects' }, webpackPlugins: { validator: isArrayOf(_.isObject), description: 'array of Webpack plugins' }, // No good way to validate this, which is a Webpack Condition // so might be almost any type: // https://webpack.js.org/configuration/module/#condition webpackStaticIgnore: { validator: () => true }, webpackStaticStubReactComponent: { validator: isArrayOf(isAbsolutePath), description: 'array of absolute paths' }, babelPlugins: { validator: isArrayOf(isBabelSetting), description: 'array of absolute paths (require.resolve your plugins)' }, babelPresets: { validator: isArrayOf(isBabelSetting), description: 'array of absolute paths (require.resolve your presets)' }, babelPresetEnvOptions: { validator: _.isPlainObject, description: 'object' }, // No good way to validate babelExclude, which is a Webpack Condition // (see above). babelExclude: { validator: () => true }, babelInclude: { validator: _.isArray }, postcssPlugins: { validator: (x) => _.isFunction(x) || isArrayOf(_.isFunction)(x), description: 'function or array of functions' }, fileLoaderExtensions: { validator: (x) => isArrayOf(_.isString)(x) || _.isFunction(x), description: 'array of strings or a function' }, jsxtremeMarkdownOptions: { validator: _.isPlainObject, description: 'object' }, pageSpecificCss: { validator: _.isBoolean, description: 'boolean' }, staticHtmlInlineDeferCss: { validator: _.isBoolean, description: 'boolean' }, includePromisePolyfill: { validator: _.isBoolean, description: 'boolean' }, inlineJs: { validator: isArrayOf((x) => { return ( isAbsolutePathToExistingFile(x.filename) && (x.uglify === undefined || _.isBoolean(x)) ); }), description: 'array of objects with an absolute path for the "filename" prop, and an optional "boolean" uglify prop' }, production: { validator: _.isBoolean, description: 'boolean' }, developmentDevtool: { validator: (x) => _.isString(x) || x === false, description: 'string or the boolean value false' }, productionDevtool: { validator: (x) => _.isString(x) || x === false, description: 'string or the boolean value false' }, clearOutputDirectory: { validator: _.isBoolean, description: 'boolean' }, unprocessedPageFiles: { validator: isArrayOf((x) => !isAbsolutePath(x)), description: 'globs relative to the pagesDirectory, not absolute paths' }, ignoreWithinPagesDirectory: { validator: isArrayOf((x) => !isAbsolutePath(x)), description: 'globs relative to the pagesDirectory, not absolute paths' }, webpackConfigClientTransform: { validator: _.isFunction, description: 'function' }, webpackConfigStaticTransform: { validator: _.isFunction, description: 'function' }, port: { validator: _.isNumber, description: 'number' }, verbose: { validator: _.isBoolean, description: 'boolean' }, spa: { validator: _.isBoolean, description: 'boolean' }, sitemap: { validator: (x) => _.isBoolean(x) || _.isPlainObject(x), description: 'boolean or object' }, webpackStats: { validator: _.isBoolean, description: 'boolean' }, includePages: { validator: isArrayOf(_.isString), description: 'globs relative to the pagesDirectory, not absolute paths' } }; // Add defaults to a raw config object, and validate it. // All options and defaults are documented in the README. function validateConfig( rawConfig , projectDirectory ) { rawConfig = rawConfig || {}; projectDirectory = projectDirectory || process.cwd(); const configErrors = []; if (!_.isPlainObject(rawConfig)) { throw new errorTypes.ConfigFatalError( 'Your Batfish configuration module must export a function the returns a configuration object.' ); } // !!! // Any changes to these defaults require corresponding // changes to the documentation. const defaults = { production: false, verbose: false, port: 8080, publicAssetsPath: 'assets', pagesDirectory: path.join(projectDirectory, 'src/pages'), outputDirectory: path.join(projectDirectory, '_batfish_site'), temporaryDirectory: path.join(projectDirectory, '_batfish_tmp'), applicationWrapperPath: path.join( __dirname, '../webpack/empty-application-wrapper.js' ), stylesheets: [], babelPlugins: [], babelPresets: [], hijackLinks: true, manageScrollRestoration: true, // cf. https://github.com/facebook/create-react-app/pull/3741/files#r162787793 babelExclude: /[/\\\\]node_modules[/\\\\]/, babelInclude: [], siteBasePath: '', browserslist: ['> 5%', 'last 2 versions'], devBrowserslist: [ // Recent browsers supporting a lot of ES2015. 'Edge >= 88', 'Firefox >= 85', 'Chrome >= 88', 'Safari >= 14', 'iOS >= 14.4' ], fileLoaderExtensions: [ 'jpeg', 'jpg', 'png', 'gif', 'webp', 'mp4', 'webm', 'woff', 'woff2' ], jsxtremeMarkdownOptions: {}, includePromisePolyfill: true, pageSpecificCss: true, staticHtmlInlineDeferCss: true, developmentDevtool: 'source-map', productionDevtool: false, clearOutputDirectory: true, spa: false, webpackStaticStubReactComponent: [], sitemap: true, webpackStats: false }; const config = Object.assign({}, defaults, rawConfig); const envBrowserslist = getEnvBrowserslist( config.browserslist, config.devBrowserslist, config.production ); const defaultPostcssPlugins = [ autoprefixer({ overrideBrowserslist: envBrowserslist }) ]; // Invoke transform function properties if (typeof config.fileLoaderExtensions === 'function') { config.fileLoaderExtensions = config.fileLoaderExtensions( defaults.fileLoaderExtensions ); } // Apply postcssPlugins default. This depends on browserslist. if (typeof config.postcssPlugins === 'function') { config.postcssPlugins = config.postcssPlugins(defaultPostcssPlugins); } else if (!config.postcssPlugins) { config.postcssPlugins = defaultPostcssPlugins; } const validatePropertyType = ( propertyName , predicate , typeDescription ) => { const value = config[propertyName]; if (value === undefined) { return; } if (!predicate(value)) { configErrors.push( `${chalk.yellow(propertyName)} must be ${a(typeDescription)}.` ); } }; Object.keys(config).forEach((propertyName) => { const optionSchema = configSchema[propertyName]; if (!optionSchema) { configErrors.push( `${chalk.yellow(propertyName)} is not a valid configuration property.` ); return; } validatePropertyType( propertyName, optionSchema.validator, optionSchema.description ); }); // Validate every stylesheet entry. URLs will be checked when they actually // are called. And it's ok if globs don't point to existing files yet. // So just check if absolute paths point to existing files. if (Array.isArray(config.stylesheets)) { _.flatten(config.stylesheets).forEach((item) => { if (isAbsoluteUrl(item)) return; if (isGlob(item)) return; if (isExistingFile(item)) return; configErrors.push( `Stylesheet entry ${chalk.yellow( item )} does not point to an existing file.` ); }); } // Throw config errors. if (configErrors.length) { const error = new errorTypes.ConfigValidationError(); error.messages = configErrors; throw error; } // Normalize URL parts. if (config.siteOrigin) { config.siteOrigin = config.siteOrigin.replace(/\/$/, ''); } if (config.siteBasePath && config.siteBasePath !== '/') { config.siteBasePath = config.siteBasePath.replace(/\/$/, ''); if (config.siteBasePath[0] !== '/') { config.siteBasePath = '/' + config.siteBasePath; } } if (config.publicAssetsPath[0] !== '/') { config.publicAssetsPath = '/' + config.publicAssetsPath; } if (config.includePages) { config.includePages = config.includePages .map((p) => { // Ensure all includePages paths start with / or the siteBasePath. if (config.siteBasePath && !p.startsWith(config.siteBasePath)) { return joinUrlParts(config.siteBasePath, p); } if (p[0] === '/') return p; return `/${p}`; }) .map((p) => { // Ensure all includePages paths that do not end in wildcards // or extensions end with / if (path.extname(p)) return p; if (/\*$/.test(p)) return p; return `${p}/`; }); } if (config.spa) { // eslint-disable-next-line console.warn( 'The SPA mode is being deprecated in favour of Underreact. Please visit https://github.com/mapbox/underreact for more information.' ); config.hijackLinks = false; config.manageScrollRestoration = false; } mkdirp.sync(config.temporaryDirectory); del.sync(path.join(config.temporaryDirectory, '{*.*,.*}'), { force: true }); return config; } module.exports = validateConfig; function isAbsolutePath(value ) { if (!_.isString(value)) { return false; } return path.isAbsolute(value); } function isArrayOf(itemCheck ) { return (value ) => Array.isArray(value) && value.every(itemCheck); } function isExistingFile(value ) { if (!_.isString(value)) { return false; } return pathType.isFileSync(value); } function isExistingDirectory(value ) { if (!_.isString(value)) { return false; } return pathType.isDirectorySync(value); } function isValidStylesheetItem(value ) { if (Array.isArray(value)) { return value.every(isValidStylesheetItem); } if (!_.isString(value)) return false; return isAbsoluteUrl(value) || isAbsolutePath(value); } function isAbsolutePathToExistingFile(value ) { return isAbsolutePath(value) && isExistingFile(value); } function isAbsolutePathToExistingDirectory(value ) { return isAbsolutePath(value) && isExistingDirectory(value); } function isBabelSetting(value ) { return isAbsolutePath(value) || _.isFunction(value); }