From 8bff16d9f724c085c12730fbdcb8c605b648db1f Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 17 Sep 2024 10:59:59 -0400 Subject: [PATCH] STRWEB-113 expose 'build' command publicly **DRAFT** **DRAFT** **DRAFT** Expose `build` as a `bin` script, allowing a platform to directly depend on this module and call the `build` API to generate a bundle without pulling in all the stripes-cli deps that are unrelated to producing a production bundle. CSS isn't being correctly bundled/handled here, but the bundle is otherwise functional. Hopefully, this is just a config glitch. Refs STRWEB-113 --- CHANGELOG.md | 1 + build/platform-storage.js | 64 ++++++++++++++ build/stripes-build-cli.js | 59 +++++++++++++ build/stripes-platform.js | 62 +++++++++++++ build/tenant-config.js | 38 ++++++++ build/webpack-common.js | 174 +++++++++++++++++++++++++++++++++++++ package.json | 7 +- 7 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 build/platform-storage.js create mode 100755 build/stripes-build-cli.js create mode 100644 build/stripes-platform.js create mode 100644 build/tenant-config.js create mode 100644 build/webpack-common.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c4ab6..98ce557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Don't worry about the order of CSS imports across modules. Refs STRWEB-110. * Remove postcss-plugins: postcss-nesting, postcss-custom-properties, postcss-color-function, postcss-calc. Add CSS variables entry point in webpack config. Refs STRWEB-111. +* Expose `build` API via `package.json::bin`. Refs STRWEB-113. ## [5.1.0](https://github.com/folio-org/stripes-webpack/tree/v5.1.0) (2024-03-12) [Full Changelog](https://github.com/folio-org/stripes-webpack/compare/v5.0.0...v5.1.0) diff --git a/build/platform-storage.js b/build/platform-storage.js new file mode 100644 index 0000000..f4338ea --- /dev/null +++ b/build/platform-storage.js @@ -0,0 +1,64 @@ +const Configstore = require('configstore'); + +// TODO: May want to modify storage key if running CLI locally +const storageKey = '@folio/stripes-cli'; + +const storageDefault = { + platforms: {}, +}; + +const platformDefault = { + aliases: {}, +}; + +// Creates and persists a virtual platform for use by the CLI +// Currently this maintains aliases for mapping a virtual platform during build +// TODO: This could also manage stripes.config.js properties like okapi, config, and modules +module.exports = class PlatformStorage { + constructor(stripesConfig, platformName) { + this.platformKey = platformName || 'default'; + this.config = new Configstore(storageKey, storageDefault); + + // Initialize platform storage + if (!this.config.has(`platforms.${this.platformKey}`)) { + this.config.set(`platforms.${this.platformKey}`, platformDefault); + } + } + + aliasKey(moduleName) { + if (moduleName) { + return `platforms.${this.platformKey}.aliases.${moduleName}`; + } + return `platforms.${this.platformKey}.aliases`; + } + + addAlias(moduleName, absolutePath) { + const key = this.aliasKey(moduleName); + this.config.set(key, absolutePath); // store absolute path + return true; + } + + hasAlias(moduleName) { + const key = this.aliasKey(moduleName); + return this.config.has(key); + } + + removeAlias(moduleName) { + const key = this.aliasKey(moduleName); + if (this.config.has(key)) { + this.config.delete(key); + } + } + + clearAliases() { + return this.config.set(this.aliasKey(), {}); + } + + getAllAliases() { + return this.config.get(this.aliasKey()); + } + + getStoragePath() { + return this.config.path; + } +}; diff --git a/build/stripes-build-cli.js b/build/stripes-build-cli.js new file mode 100755 index 0000000..a4bcaa8 --- /dev/null +++ b/build/stripes-build-cli.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const build = require('../webpack/build'); +const nodeApi = require('../webpack/stripes-node-api'); +const StripesPlatform = require('./stripes-platform'); + +nodeApi.StripesModuleParser = require('../webpack/stripes-module-parser').StripesModuleParser; +nodeApi.StripesBuildError = require('../webpack/stripes-build-error'); + +function processStats(stats) { + console.log(stats.toString({ + chunks: false, + colors: true, + })); + // Check for webpack compile errors and exit + if (stats.hasErrors()) { + processError(); + } +} + +process.title = 'stripes-cli'; +process.env.NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV : 'production'; + +const stripesConfig = require(path.join(process.env.PWD, process.argv[3])); +const output = process.argv.length === 5 ? process.argv[4] : null; + +const argv = { + stripesConfig, + output, +}; +const context = {}; + +const platform = new StripesPlatform(argv.stripesConfig, context, argv); +const webpackOverrides = platform.getWebpackOverrides(context); + +if (argv.output) { + argv.outputPath = argv.output; +} else if (!argv.outputPath) { + argv.outputPath = './output'; +} +if (argv.maxChunks) { + webpackOverrides.push(limitChunks(argv.maxChunks)); +} +if (argv.cache === false) { + webpackOverrides.push(ignoreCache); +} +if (context.plugin && context.plugin.beforeBuild) { + webpackOverrides.push(context.plugin.beforeBuild(argv)); +} + +console.log('Building...'); +nodeApi.build(platform.getStripesConfig(), Object.assign({}, argv, { webpackOverrides })) + .then(processStats) + .catch(e => { + console.error('ERROR', e); + }); diff --git a/build/stripes-platform.js b/build/stripes-platform.js new file mode 100644 index 0000000..6e338fa --- /dev/null +++ b/build/stripes-platform.js @@ -0,0 +1,62 @@ +const path = require('path'); +const { defaultConfig, emptyConfig, mergeConfig } = require('./tenant-config'); +const webpackCommon = require('./webpack-common'); +const logger = console; + +module.exports = class StripesPlatform { + constructor(stripesConfig, context, options) { + this.isAppContext = false; + this.aliases = {}; + this.addAliasesAsModules = true; + + // Start with stripes.config.js or internal defaults + this.applyDefaultConfig(stripesConfig); + + // Apply any command options last + this.applyCommandOptions(options); + } + + applyDefaultConfig(stripesConfig) { + // TODO: Validate incoming config + if (stripesConfig) { + // When modules are specified in a config file, do not automatically apply aliases as modules + if (stripesConfig.modules) { + this.addAliasesAsModules = false; + } + this.config = mergeConfig(emptyConfig, stripesConfig); + } else { + this.config = mergeConfig(emptyConfig, defaultConfig); + } + } + + applyCommandOptions(options) { + if (options) { + if (options.okapi) { + this.config.okapi.url = options.okapi; + } + if (options.tenant) { + this.config.okapi.tenant = options.tenant; + } + if (options.hasAllPerms) { + this.config.config.hasAllPerms = true; + } + if (options.languages) { + this.config.config.languages = options.languages; + } + } + } + + getWebpackOverrides(context) { + const overrides = []; + overrides.push(webpackCommon.cliResolve(context)); + overrides.push(webpackCommon.cliAliases(this.aliases)); + return overrides; + } + + getStripesConfig() { + const config = Object.assign({}, this.config); + delete config.aliases; + logger.log('using stripes tenant config:', config); + return config; + } +}; diff --git a/build/tenant-config.js b/build/tenant-config.js new file mode 100644 index 0000000..ee6f4f6 --- /dev/null +++ b/build/tenant-config.js @@ -0,0 +1,38 @@ +const { merge } = require('lodash'); + +const defaultConfig = { + okapi: { + url: 'http://localhost:9130', + tenant: 'diku', + }, + config: { + logCategories: 'core,path,action,xhr', + logPrefix: '--', + showPerms: false, + hasAllPerms: false, + languages: ['en'], + useSecureTokens: true, + }, + modules: { + }, + branding: { + }, +}; + +const emptyConfig = { + okapi: {}, + config: {}, + modules: {}, + branding: {}, +}; + +// Merge two stripes configurations +function mergeConfig(base, extend) { + return merge({}, base, extend); +} + +module.exports = { + defaultConfig, + emptyConfig, + mergeConfig, +}; diff --git a/build/webpack-common.js b/build/webpack-common.js new file mode 100644 index 0000000..f9d501d --- /dev/null +++ b/build/webpack-common.js @@ -0,0 +1,174 @@ +const fs = require('fs'); +const path = require('path'); +const { set } = require('lodash'); +const webpack = require('webpack'); +const logger = console; + +// Display error to the console and exit +function processError(err) { + if (err) { + console.error(err); + } + process.exit(1); +} + +// Display webpack output to the console +function processStats(stats) { + console.log(stats.toString({ + chunks: false, + colors: true, + })); + // Check for webpack compile errors and exit + if (stats.hasErrors()) { + processError(); + } +} + +// Webpack config override: +// This adjusts Webpack's resolve configuration to account for the location of stripes-core. +function cliResolve(context) { + return (config) => { + if (context.isGlobalYarn) { + config.resolve.modules.push(context.globalDirs.yarn.packages); + config.resolveLoader.modules.push(context.globalDirs.yarn.packages); + } else { + config.resolve.modules.push(path.resolve(__dirname, path.join('..', 'node_modules'))); + config.resolveLoader.modules.push(path.resolve(__dirname, path.join('..', 'node_modules'))); + } + logger.log('entry:', config.entry); + logger.log('resolve.modules:', config.resolve.modules); + logger.log('resolveLoader.modules:', config.resolveLoader.modules); + return config; + }; +} + +// Webpack config override: +// Alias support for serving from within app's own directory +// or serving an entire platform without yarn linking +function cliAliases(aliases) { + return (config) => { + if (aliases) { + const moduleNames = Object.getOwnPropertyNames(aliases); + for (const moduleName of moduleNames) { + config.resolve.alias[moduleName] = aliases[moduleName]; + } + logger.log('resolve.alias:', config.resolve.alias); + } + return config; + }; +} + +// Show eslint failures at runtime +function emitLintWarnings(config) { + config.module.rules.push({ + enforce: 'pre', + test: /\.js$/, + exclude: /node_modules/, + loader: 'eslint-loader', + options: { + emitWarning: true, + }, + }); + return config; +} + +// Controls the webpack chunk output +function limitChunks(maxChunks) { + return (config) => { + config.plugins.push(new webpack.optimize.LimitChunkCountPlugin({ + maxChunks, + })); + return config; + }; +} + +// shouldModuleBeIncluded - +// this is a slimmed-down version of stripes-webpack's shouldModuleBeIncluded. We still need to +// transpile things that need to be transpiled, which is our working project's files and any from an `@folio` scoped module. + +function shouldModuleBeIncluded(modulePath) { + const nodeModulesRegex = /node_modules/; + + // https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex/6969486#6969486 + const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const folioModulePath = path.join('node_modules', '@folio'); + const folioModulesRegex = new RegExp(`${escapeRegExp(folioModulePath)}(?!.*dist)`); + + if (folioModulesRegex.test(modulePath)) return true; + + // exclude empty modules + if (!modulePath) return false; + + // skip everything from node_modules + return !nodeModulesRegex.test(modulePath); +} + +// test coverage is activated by including the instrumentation plugin to the babel plugin stack. +// the test stack currently includes es-build for dev and production, but unfortunately, the tool includes +// no test coverage instrumentation :o( https://github.com/evanw/esbuild/issues/184 + +function enableCoverage(config) { + const babelLoaderConfigIndex = config.module.rules.findIndex((rule) => { + return rule?.oneOf?.[1]?.use?.[0].loader === 'babel-loader'; + }); + + if (!config.module.rules[babelLoaderConfigIndex]?.oneOf?.[1].use[0].options?.plugins) { + set(config.module.rules[babelLoaderConfigIndex], 'oneOf[1].use[0].options.plugins', []); + } + + // only use babel configuration for test coverage.. + const babelConfigurationItem = config.module.rules[babelLoaderConfigIndex].oneOf[1]; + + // exclude files from coverage reports here. + config.module.rules[babelLoaderConfigIndex].oneOf[1].use[0].options.plugins.push( + [require.resolve('babel-plugin-istanbul'), { + exclude: [ + '**/*.test.js', + '**/tests/*.js', + '**/stories/*.js', + '/node_modules/*' + ] + }] + ); + + babelConfigurationItem.test = /\.js$/; + babelConfigurationItem.include = shouldModuleBeIncluded; + + // replace the esbuild tooling with babel-loader + set(config.module, `rules[${babelLoaderConfigIndex}]`, babelConfigurationItem); + + return config; +} + +function enableMirage(scenario) { + return (config) => { + const mirageEntry = path.resolve('./test/bigtest/network/boot.js'); + + if (fs.existsSync(mirageEntry)) { + config.plugins.push(new webpack.EnvironmentPlugin({ + MIRAGE_SCENARIO: scenario === true ? 'default' : scenario + })); + + config.entry.unshift(mirageEntry); + } + + return config; + }; +} + +function ignoreCache(config) { + return { ...config, cache: false }; +} + +module.exports = { + processError, + processStats, + cliResolve, + cliAliases, + emitLintWarnings, + limitChunks, + enableCoverage, + enableMirage, + ignoreCache, +}; diff --git a/package.json b/package.json index ed0d6aa..a40edca 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "test": "mocha --opts ./test/mocha.opts './test/webpack/**/*.js'" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" + }, + "main": {}, + "bin": { + "stripes": "./build/stripes-build-cli.js" }, "dependencies": { "@babel/core": "^7.9.0", @@ -39,6 +43,7 @@ "babel-plugin-remove-jsx-attributes": "^0.0.2", "buffer": "^6.0.3", "commander": "^2.9.0", + "configstore": "^3.1.1", "connect-history-api-fallback": "^1.3.0", "core-js": "^3.6.1", "crypto-browserify": "^3.12.0",