react-static
Version:
A progressive static site generator for React
356 lines (314 loc) • 10.6 kB
JavaScript
/* eslint-disable import/no-dynamic-require */
import nodePath from 'path'
import chokidar from 'chokidar'
import resolveFrom from 'resolve-from'
import fs from 'fs-extra'
//
import { cleanSlashes, cutPathToRoot, isAbsoluteUrl } from '../utils'
import corePlugins, { validatePlugin } from './plugins'
// the default static.config.js location
const defaultConfig = {}
const DEFAULT_NAME_FOR_STATIC_CONFIG_FILE = 'static.config.js'
const DEFAULT_PATH_FOR_STATIC_CONFIG = nodePath.resolve(
nodePath.join(process.cwd(), DEFAULT_NAME_FOR_STATIC_CONFIG_FILE)
)
const DEFAULT_ROUTES = [{ path: '/' }]
const DEFAULT_ENTRY = 'index.js'
const DEFAULT_EXTENSIONS = ['.js', '.jsx']
// Retrieves the static.config.js from the current project directory
export default function getConfig(
state,
callback = async config => {
if (state.debug) {
console.log('getConfig():')
console.log(state)
}
return config
}
) {
const configPath =
state.configPath ||
state.packageConfig.config ||
DEFAULT_PATH_FOR_STATIC_CONFIG
state = {
...state,
originalConfig: configPath,
}
const resolvedPath = nodePath.resolve(configPath)
const noConfig =
configPath === DEFAULT_PATH_FOR_STATIC_CONFIG && !resolvedPath
if (noConfig) {
// last
state = buildConfig(state, defaultConfig)
return callback(state).catch(console.error)
}
state = buildConfigFromPath(state, resolvedPath || configPath)
if (state.stage === 'dev') {
chokidar
.watch(resolvedPath, {
ignoreInitial: true,
})
.on('all', async () => {
console.log('')
console.log(`Updating static.config.js`)
state = buildConfigFromPath(state, resolvedPath)
callback(state).catch(console.error)
})
}
return callback(state).catch(console.error)
}
function buildConfigFromPath(state, configPath) {
delete require.cache[configPath]
const config = require(configPath).default
return buildConfig(state, config)
}
export function buildConfig(state, config = {}) {
// Default Paths
let paths = {
root: nodePath.resolve(process.cwd()),
src: 'src',
dist: 'dist',
temp: 'tmp',
buildArtifacts: 'artifacts',
devDist: 'tmp/dev-server',
public: 'public',
plugins: 'plugins',
pages: 'src/pages',
nodeModules: 'node_modules',
assets: '',
...(config.paths || {}),
}
// Use the root to resolve all other relative paths
const resolvePath = relativePath => nodePath.resolve(paths.root, relativePath)
// Resolve and replace all pathss
const DIST =
process.env.REACT_STATIC_ENV === 'development'
? resolvePath(paths.devDist || paths.dist)
: resolvePath(paths.dist)
const ASSETS = nodePath.resolve(DIST, paths.assets)
paths = {
ROOT: paths.root,
SRC: resolvePath(paths.src),
DIST,
ASSETS,
PLUGINS: resolvePath(paths.plugins),
TEMP: resolvePath(paths.temp),
ARTIFACTS: resolvePath(paths.buildArtifacts),
PUBLIC: resolvePath(paths.public),
NODE_MODULES: resolvePath(paths.nodeModules),
EXCLUDE_MODULES:
paths.excludeResolvedModules || resolvePath(paths.nodeModules),
PACKAGE: resolvePath('package.json'),
HTML_TEMPLATE: nodePath.join(DIST, 'index.html'),
STATIC_DATA: nodePath.join(ASSETS, 'staticData'),
}
// siteRoot, basePath, publicPath, and assetPath resolution
let siteRoot = ''
let basePath = ''
let assetsPath = ''
if (process.env.REACT_STATIC_ENV === 'development') {
basePath = cleanSlashes(config.devBasePath)
assetsPath = config.devAssetsPath || paths.assets || assetsPath
} else if (state.staging) {
siteRoot = cutPathToRoot(config.stagingSiteRoot || '/', '$1')
basePath = cleanSlashes(config.stagingBasePath)
assetsPath = config.stagingAssetsPath || paths.assets || assetsPath
} else {
siteRoot = cutPathToRoot(config.siteRoot || '/', '$1')
basePath = cleanSlashes(config.basePath)
assetsPath = config.assetsPath || paths.assets || assetsPath
}
const publicPath = `${cleanSlashes(`${siteRoot}/${basePath}`, {
leading: false,
})}/`
if (assetsPath && !isAbsoluteUrl(assetsPath)) {
assetsPath = `/${cleanSlashes(`${basePath}/${assetsPath}`)}/`
}
// add trailing slash only if assetsPath was supplied, but no trailing slash
if (assetsPath && !assetsPath.endsWith('/')) {
assetsPath = `${assetsPath}/`
}
// Add the project root as a plugin. This allows the dev
// to use the plugin api directory in their project if they want
const plugins = [...(config.plugins || []), paths.ROOT]
// if (process.env.NODE_ENV !== 'test' && !entry) {
// throw new Error(
// `Could not resolve entry file from location: ${entry} using extensions: ${(
// config.extensions || DEFAULT_EXTENSIONS
// ).join(', ')}`
// )
// }
// Defaults
config = {
// Defaults
getSiteData: () => ({}),
prefetchRate: 5,
maxThreads: Infinity,
disableRoutePrefixing: false,
outputFileRate: 100,
extensions: DEFAULT_EXTENSIONS,
getRoutes: async () => DEFAULT_ROUTES,
minLoadTime: 200,
disablePreload: false,
disableRuntime: false,
preloadPollInterval: 300,
productionSourceMaps: false,
silent: false,
entry: DEFAULT_ENTRY,
// Config Overrides
...config,
// Materialized Overrides
paths,
plugins,
siteRoot,
basePath,
publicPath,
assetsPath,
extractCssChunks: config.extractCssChunks || false,
inlineCss: config.inlineCss || false,
babelExcludes: config.babelExcludes || [],
devServer: {
host: 'localhost',
port: 3000,
...(config.devServer || {}),
},
}
config.terser = config.terser || {}
config.terser.terserOptions = config.terser.terserOptions || {}
config.terser.terserOptions.mangle = config.terser.terserOptions.mangle || {}
config.terser.terserOptions.parse = config.terser.terserOptions.parse || {}
config.terser.terserOptions.compress =
config.terser.terserOptions.compress || {}
config.terser.terserOptions.output = config.terser.terserOptions.output || {}
// Set env variables to be used client side
process.env.REACT_STATIC_MIN_LOAD_TIME = config.minLoadTime
process.env.REACT_STATIC_PREFETCH_RATE = config.prefetchRate
process.env.REACT_STATIC_DISABLE_ROUTE_PREFIXING =
config.disableRoutePrefixing
process.env.REACT_STATIC_DISABLE_PRELOAD = config.disablePreload
process.env.REACT_STATIC_DISABLE_RUNTIME = config.disableRuntime
process.env.REACT_STATIC_PRELOAD_POLL_INTERVAL = config.preloadPollInterval
process.env.REACT_STATIC_SILENT = config.silent
process.env.REACT_STATIC_ROOT_PATH_READ_ONLY = paths.ROOT
process.env.REACT_STATIC_TEMPLATES_PATH = nodePath.join(
paths.ARTIFACTS,
'react-static-templates.js'
)
process.env.REACT_STATIC_PLUGINS_PATH = nodePath.join(
paths.ARTIFACTS,
'react-static-browser-plugins.js'
)
const resolvePlugin = originalLocation => {
let options = {}
if (Array.isArray(originalLocation)) {
options = originalLocation[1] || {}
originalLocation = originalLocation[0]
}
const location = [
() => {
// Absolute require
try {
const found = require.resolve(originalLocation)
return found.includes('.') ? nodePath.resolve(found, '../') : found
} catch (err) {
//
}
},
() => {
// Absolute
if (fs.pathExistsSync(originalLocation)) {
return originalLocation
}
},
() => {
// Plugins Dir
const found = nodePath.resolve(paths.PLUGINS, originalLocation)
if (fs.pathExistsSync(found)) {
return found
}
},
() => {
// Plugins Dir require
try {
const found = resolveFrom(paths.PLUGINS, originalLocation)
return found.includes('.') ? nodePath.resolve(found, '../') : found
} catch (err) {
//
}
},
() => {
// CWD
const found = nodePath.resolve(process.cwd(), originalLocation)
if (fs.pathExistsSync(found)) {
return found
}
},
() => {
// CWD require
try {
const found = resolveFrom(process.cwd(), originalLocation)
return found.includes('.') ? nodePath.resolve(found, '../') : found
} catch (err) {
//
}
},
() => {
if (process.env.NODE_ENV === 'test') {
// Allow plugins to be mocked
return require('path').resolve('./src/static/__mocks__/mock-plugin')
}
},
].reduce((prev, curr) => prev || curr(), null)
// TODO: We have to do this because we don't have a good mock for process.cwd() :(
if (!location) {
throw new Error(
`Could not find a plugin directory for the plugin: "${originalLocation}". We must bail!`
)
}
let nodeLocation = nodePath.join(location, 'node.api.js')
let browserLocation = nodePath.join(location, 'browser.api.js')
// Detect if the node plugin entry exists, and provide the nodeResolver to it
nodeLocation = fs.pathExistsSync(nodeLocation) ? nodeLocation : null
// Detect if the browser plugin entry exists, and provide the nodeResolver to it
browserLocation = fs.pathExistsSync(browserLocation)
? browserLocation
: null
let buildPluginHooks = () => ({})
try {
// Get the hooks for the node api
if (nodeLocation) {
buildPluginHooks = require(nodeLocation).default
} else if (originalLocation !== paths.ROOT && !browserLocation) {
throw new Error(
`Could not find a valid node.api.js or browser.api.js plugin file in "${location}". \n` +
`The original location: "${originalLocation}". \n` +
`The root location: "${paths.ROOT}".`
)
}
const resolvedPlugin = {
location,
nodeLocation,
browserLocation,
options,
hooks: buildPluginHooks(options) || {},
}
validatePlugin(resolvedPlugin)
// Recursively resolve plugins
if (resolvedPlugin.plugins) {
resolvedPlugin.plugins = resolvedPlugin.plugins.map(resolvePlugin)
}
return resolvedPlugin
} catch (err) {
console.error(
`The following error occurred in the plugin: "${originalLocation}"`
)
throw err
}
}
state = {
...state,
plugins: config.plugins.map(resolvePlugin),
config,
}
return corePlugins.afterGetConfig(state)
}