From c59da7d5e60a02dd57165a4bde2e4d6417d9c902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kuklis?= Date: Tue, 21 Mar 2023 10:53:54 -0400 Subject: [PATCH] STRWEB-69: Add support for transpilation (#73) * Add support for transpilation * Cleanup * Cleanup * Continue on transpilation * Cleanup * Cleanup * Cleanup * More work on transpilation * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup * More work on transpilation * Cleanup * Cleanup * Cleanup * Handle already transpiled CSS files * Cleanup * Cleanup * Fix tests * Add woff2 support * Remove empty scripts * skip stripes-ui from transpilation * Add disableDeprecationNotice to postcss * Improve path resolution * Update css entry * use path.sep * Cleanup * Adjust css dist path regex * More cleanup for Windows * Fix tests * Cleanup css based on feedback * Adjust tsconfig * Cleanup * Remove only to run all tests * Adjust dependencies * Add ts and tsx to babel-loader test in transpilation config * Adjust shared styles --- consts.js | 32 ++++ package.json | 5 + postcss.config.js | 39 +++++ test/webpack/babel-loader-rule.spec.js | 14 +- webpack.config.base.js | 83 ++++++++++- webpack.config.cli.dev.js | 97 ++---------- webpack.config.cli.js | 1 + webpack.config.cli.prod.js | 138 ++++++------------ ....js => webpack.config.cli.shared.styles.js | 0 webpack.config.cli.transpile.js | 109 ++++++++++++++ webpack/babel-loader-rule.js | 104 ++++++++----- webpack/babel-options.js | 2 + webpack/build.js | 3 + webpack/module-paths.js | 82 ++++++++++- webpack/serve.js | 8 +- webpack/stripes-node-api.js | 2 + webpack/transpile.js | 43 ++++++ webpack/tsconfig.json | 12 +- webpack/utils.js | 20 ++- 19 files changed, 557 insertions(+), 237 deletions(-) create mode 100644 consts.js create mode 100644 postcss.config.js rename webpack.config.cli.dev.shared.styles.js => webpack.config.cli.shared.styles.js (100%) create mode 100644 webpack.config.cli.transpile.js create mode 100644 webpack/transpile.js diff --git a/consts.js b/consts.js new file mode 100644 index 0000000..f44ca10 --- /dev/null +++ b/consts.js @@ -0,0 +1,32 @@ +// The list of the default externals +// https://webpack.js.org/configuration/externals/ +const defaultExternals = [ + '@folio/stripes', + '@folio/stripes-components', + '@folio/stripes-connect', + '@folio/stripes-core', + '@folio/stripes-util', + '@folio/stripes-form', + '@folio/stripes-final-form', + '@folio/stripes-logger', + '@folio/stripes-smart-components', + 'final-form', + 'final-form-arrays', + 'moment', + 'moment-timezone', + 'react', + 'react-dom', + 'react-final-form', + 'react-final-form-arrays', + 'react-final-form-listeners', + 'react-intl', + 'react-query', + 'react-redux', + 'react-router', + 'redux', + 'stripes-config', +]; + +module.exports = { + defaultExternals, +}; diff --git a/package.json b/package.json index b9d74b1..cfa9659 100644 --- a/package.json +++ b/package.json @@ -66,16 +66,21 @@ "regenerator-runtime": "^0.13.3", "semver": "^7.1.3", "serialize-javascript": "^5.0.0", + "source-map-loader": "^4.0.0", + "speed-measure-webpack-plugin": "^1.5.0", "stream-browserify": "^3.0.0", "style-loader": "^3.3.0", "svgo": "^1.2.2", "svgo-loader": "^2.2.1", "tapable": "^1.0.0", + "terser-webpack-plugin": "^5.3.5", "ts-loader": "^9.4.1", "typescript": "^4.2.4", + "url-loader": "^4.1.1", "util-ex": "^0.3.15", "webpack-dev-middleware": "^5.2.1", "webpack-hot-middleware": "^2.25.1", + "webpack-remove-empty-scripts": "^1.0.1", "webpack-virtual-modules": "^0.4.3" }, "devDependencies": { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1016842 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,39 @@ +const path = require('path'); +const postCssImport = require('postcss-import'); +const autoprefixer = require('autoprefixer'); +const postCssCustomProperties = require('postcss-custom-properties'); +const postCssCalc = require('postcss-calc'); +const postCssNesting = require('postcss-nesting'); +const postCssCustomMedia = require('postcss-custom-media'); +const postCssMediaMinMax = require('postcss-media-minmax'); +const postCssColorFunction = require('postcss-color-function'); + +const { generateStripesAlias, tryResolve } = require('./webpack/module-paths'); + +const locateCssVariables = () => { + const variables = 'lib/variables.css'; + const localPath = path.join(path.resolve(), variables); + + // check if variables are present locally (in cases when stripes-components is + // being built directly) if not look for them via stripes aliases + return tryResolve(localPath) ? + localPath : + path.join(generateStripesAlias('@folio/stripes-components'), variables); +}; + +module.exports = { + plugins: [ + postCssImport(), + autoprefixer(), + postCssCustomProperties({ + preserve: false, + importFrom: [locateCssVariables()], + disableDeprecationNotice: true + }), + postCssCalc(), + postCssNesting(), + postCssCustomMedia(), + postCssMediaMinMax(), + postCssColorFunction(), + ], +}; diff --git a/test/webpack/babel-loader-rule.spec.js b/test/webpack/babel-loader-rule.spec.js index 1cfca8a..90d0014 100644 --- a/test/webpack/babel-loader-rule.spec.js +++ b/test/webpack/babel-loader-rule.spec.js @@ -4,32 +4,32 @@ const babelLoaderRule = require('../../webpack/babel-loader-rule'); describe('The babel-loader-rule', function () { describe('test condition function', function () { beforeEach(function () { - this.sut = babelLoaderRule({ modules: {} }).test; + this.sut = babelLoaderRule(['@folio/inventory']); }); it('selects files for @folio scoped node_modules', function () { - const fileName = '/projects/folio/folio-testing-platform/node_modules/@folio/inventory/index.js'; - const result = this.sut(fileName); + const fileName = '/projects/folio/folio-testing-platform/node_modules/stripes-config'; + const result = this.sut.include(fileName); expect(result).to.equal(true); }); it('does not select node_modules files outside of @folio scope', function () { const fileName = '/projects/folio/folio-testing-platform/node_modules/lodash/lodash.js'; - const result = this.sut(fileName); + const result = this.sut.include(fileName); expect(result).to.equal(false); }); it('only selects .js file extensions', function () { const fileName = '/project/folio/folio-testing-platform/node_modules/@folio/search/package.json'; - const result = this.sut(fileName); - expect(result).to.equal(false); + const result = fileName.match(this.sut.test); + expect(result).to.equal(null); }); it('selects files outside of both @folio scope and node_modules', function () { // This test case would hold true for yarn-linked modules, @folio scoped or otherwise // Therefore this implies that we are not yarn-linking any non-@folio scoped modules const fileName = '/projects/folio/stripes-core/src/configureLogger.js'; - const result = this.sut(fileName); + const result = this.sut.include(fileName); expect(result).to.equal(true); }); }); diff --git a/webpack.config.base.js b/webpack.config.base.js index c5ece5e..ba700b7 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -3,9 +3,13 @@ const fs = require('fs'); const webpack = require('webpack'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); + const { generateStripesAlias } = require('./webpack/module-paths'); -const babelLoaderRule = require('./webpack/babel-loader-rule'); const typescriptLoaderRule = require('./webpack/typescript-loader-rule'); +const { isProduction } = require('./webpack/utils'); +const { getTranspiledCssPaths } = require('./webpack/module-paths'); // React doesn't like being included multiple times as can happen when using // yarn link. Here we find a more specific path to it by first looking in @@ -36,9 +40,9 @@ const specificReact = generateStripesAlias('react'); // Since we are now on the webpack 5 we can make use of dependOn (https://webpack.js.org/configuration/entry-context/#dependencies) // in order to create a dependency between stripes config and other chunks: -module.exports = { +const baseConfig = { entry: { - css: '@folio/stripes-components/lib/global.css', + css: ['@folio/stripes-components/lib/global.css'], stripesConfig: { import: 'stripes-config.js' }, @@ -60,6 +64,7 @@ module.exports = { template: fs.existsSync('index.html') ? 'index.html' : `${__dirname}/index.html`, }), new webpack.EnvironmentPlugin(['NODE_ENV']), + new RemoveEmptyScriptsPlugin(), ], module: { rules: [ @@ -97,6 +102,78 @@ module.exports = { loader: 'csv-loader', }], }, + { + test: /\.js.map$/, + enforce: 'pre', + use: ['source-map-loader'], + }, + { + test: /\.svg$/, + use: [{ + loader: 'url-loader', + options: { + esModule: false, + }, + }] + }, ], }, }; + + +const buildConfig = (modulePaths) => { + const transpiledCssPaths = getTranspiledCssPaths(modulePaths); + const cssDistPathRegex = /dist[\/\\]style\.css/; + + // already transpiled css files + if (transpiledCssPaths.length) { + transpiledCssPaths.forEach(cssPath => { + baseConfig.entry.css.push(cssPath); + }); + + baseConfig.module.rules.push({ + test: /\.css$/, + include: [cssDistPathRegex], + use: [ + { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, + { + loader: 'css-loader', + options: { + modules: false + }, + }, + ], + }); + } + + // css files not transpiled yet + baseConfig.module.rules.push({ + test: /\.css$/, + exclude: [cssDistPathRegex], + use: [ + { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[local]---[hash:base64:5]', + }, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + config: path.resolve(__dirname, 'postcss.config.js'), + }, + sourceMap: true, + }, + }, + ] + }); + + return baseConfig; +} + +module.exports = buildConfig; diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index cf9fac4..ff4c894 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -1,43 +1,27 @@ // Top level Webpack configuration for running a development environment // from the command line via devServer.js -const path = require('path'); const webpack = require('webpack'); -const postCssImport = require('postcss-import'); -const autoprefixer = require('autoprefixer'); -const postCssCustomProperties = require('postcss-custom-properties'); -const postCssCalc = require('postcss-calc'); -const postCssNesting = require('postcss-nesting'); -const postCssCustomMedia = require('postcss-custom-media'); -const postCssMediaMinMax = require('postcss-media-minmax'); -const postCssColorFunction = require('postcss-color-function'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const babelLoaderRule = require('./webpack/babel-loader-rule'); -const { generateStripesAlias, tryResolve } = require('./webpack/module-paths'); +const { getModulesPaths, getStripesModulesPaths } = require('./webpack/module-paths'); +const { tryResolve } = require('./webpack/module-paths'); +const babelLoaderRule = require('./webpack/babel-loader-rule'); const utils = require('./webpack/utils'); - -const base = require('./webpack.config.base'); +const buildBaseConfig = require('./webpack.config.base'); const cli = require('./webpack.config.cli'); -const buildConfig = (stripesConfig) => { - - const locateCssVariables = () => { - const variables = 'lib/variables.css'; - const localPath = path.join(path.resolve(), variables); - - // check if variables are present locally (in cases when stripes-components is - // being built directly) if not look for them via stripes aliases - return tryResolve(localPath) ? - localPath : - path.join(generateStripesAlias('@folio/stripes-components'), variables); - }; +const useBrowserMocha = () => { + return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; +}; - const useBrowserMocha = () => { - return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; - }; +const buildConfig = (stripesConfig) => { + const modulePaths = getModulesPaths(stripesConfig.modules); + const stripesModulePaths = getStripesModulesPaths(); + const allModulePaths = [...stripesModulePaths, ...modulePaths]; + const base = buildBaseConfig(allModulePaths); const devConfig = Object.assign({}, base, cli, { devtool: 'inline-source-map', mode: 'development', @@ -56,7 +40,7 @@ const buildConfig = (stripesConfig) => { devConfig.output.filename = 'bundle.js'; devConfig.entry = [ 'webpack-hot-middleware/client', - '@folio/stripes-components/lib/global.css', + ...devConfig.entry.css, '@folio/stripes-ui', ]; @@ -78,48 +62,7 @@ const buildConfig = (stripesConfig) => { devConfig.resolve.alias.process = 'process/browser.js'; devConfig.resolve.alias['mocha'] = useBrowserMocha(); - devConfig.module.rules.push(babelLoaderRule(stripesConfig)); - - devConfig.module.rules.push({ - test: /\.css$/, - use: [ - { - loader: 'style-loader' - }, - { - loader: 'css-loader', - options: { - modules: { - localIdentName: '[local]---[hash:base64:5]', - }, - sourceMap: true, - importLoaders: 1, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: [ - postCssImport(), - autoprefixer(), - postCssCustomProperties({ - preserve: false, - importFrom: [locateCssVariables()], - disableDeprecationNotice: true - }), - postCssCalc(), - postCssNesting(), - postCssCustomMedia(), - postCssMediaMinMax(), - postCssColorFunction(), - ], - }, - sourceMap: true, - }, - }, - ], - }); + devConfig.module.rules.push(babelLoaderRule(allModulePaths)); // add 'Buffer' global required for tests/reporting tools. devConfig.plugins.push( @@ -135,18 +78,6 @@ const buildConfig = (stripesConfig) => { "util": require.resolve('util-ex'), }; - devConfig.module.rules.push( - { - test: /\.svg$/, - use: [{ - loader: 'file-loader?name=img/[path][name].[contenthash].[ext]', - options: { - esModule: false, - }, - }] - }, - ); - return devConfig; } diff --git a/webpack.config.cli.js b/webpack.config.cli.js index 858aac9..b2f6f2e 100644 --- a/webpack.config.cli.js +++ b/webpack.config.cli.js @@ -9,5 +9,6 @@ module.exports = { filename: 'bundle.[name][contenthash].js', chunkFilename: 'chunk.[name][chunkhash].js', publicPath: '/', + clean: true }, }; diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index 8edc099..8589a12 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -1,131 +1,77 @@ // Top level Webpack configuration for building static files for // production deployment from the command line -const path = require('path'); const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); -const postCssImport = require('postcss-import'); -const autoprefixer = require('autoprefixer'); -const postCssCustomProperties = require('postcss-custom-properties'); -const postCssCalc = require('postcss-calc'); -const postCssNesting = require('postcss-nesting'); -const postCssCustomMedia = require('postcss-custom-media'); -const postCssMediaMinMax = require('postcss-media-minmax'); -const postCssColorFunction = require('postcss-color-function'); -const { generateStripesAlias, getSharedStyles } = require('./webpack/module-paths'); -const babelLoaderRule = require('./webpack/babel-loader-rule'); - -const base = require('./webpack.config.base'); +const { getSharedStyles } = require('./webpack/module-paths'); +const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); +const buildBaseConfig = require('./webpack.config.base'); const cli = require('./webpack.config.cli'); +const babelLoaderRule = require('./webpack/babel-loader-rule'); +const { getModulesPaths, getStripesModulesPaths, getTranspiledModules } = require('./webpack/module-paths'); const buildConfig = (stripesConfig) => { + const modulePaths = getModulesPaths(stripesConfig.modules); + const stripesModulePaths = getStripesModulesPaths(); + const allModulePaths = [...stripesModulePaths, ...modulePaths]; + const base = buildBaseConfig(allModulePaths); const prodConfig = Object.assign({}, base, cli, { mode: 'production', + devtool: 'source-map', infrastructureLogging: { appendOnly: true, level: 'warn', - } + }, }); + const transpiledModules = getTranspiledModules(allModulePaths); + const transpiledModulesRegex = new RegExp(transpiledModules.join('|')); + const smp = new SpeedMeasurePlugin(); + prodConfig.plugins = prodConfig.plugins.concat([ - new MiniCssExtractPlugin({ filename: 'style.[contenthash].css' }), new webpack.ProvidePlugin({ process: 'process/browser.js', }), ]); - prodConfig.resolve.alias = { - ...prodConfig.resolve.alias, - "stcom-interactionStyles": getSharedStyles("lib/sharedStyles/interactionStyles"), - "stcom-variables": getSharedStyles("lib/variables"), - }; - prodConfig.optimization = { - mangleWasmImports: true, + mangleWasmImports: false, minimizer: [ - '...', // in webpack@5 we can use the '...' syntax to extend existing minimizers + new TerserPlugin({ + // exclude stripes cache group from the minimizer + exclude: /stripes/, + }), new CssMinimizerPlugin(), ], - minimize: true, - } - - prodConfig.module.rules.push(babelLoaderRule(stripesConfig)); - - prodConfig.module.rules.push({ - test: /\.css$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, + splitChunks: { + // Do not process stripes chunk + chunks: (chunk) => { + return chunk.name !== 'stripes'; }, - { - loader: 'css-loader', - options: { - modules: { - localIdentName: '[local]---[hash:base64:5]', - }, - importLoaders: 1, + cacheGroups: { + // this cache group will be omitted by minimizer + stripes: { + // only include already transpiled modules + test: (module) => transpiledModulesRegex.test(module.resource), + name: 'stripes', + chunks: 'all' }, }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: [ - postCssImport(), - autoprefixer(), - postCssCustomProperties({ - preserve: false, - importFrom: [path.join(generateStripesAlias('@folio/stripes-components'), 'lib/variables.css')], - disableDeprecationNotice: true - }), - postCssCalc(), - postCssNesting(), - postCssCustomMedia(), - postCssMediaMinMax(), - postCssColorFunction(), - ], - }, - }, - }, - ], - }); - - prodConfig.module.rules.push( - { - test: /\.svg$/, - use: [ - { - loader: 'file-loader?name=img/[path][name].[contenthash].[ext]', - options: { - esModule: false, - }, - }, - { - loader: 'svgo-loader', - options: { - plugins: [ - { removeTitle: true }, - { convertColors: { shorthex: false } }, - { convertPathData: false } - ] - } - } - ] }, - ); + minimize: true, + } - // Remove all data-test or data-test-* attributes - const babelLoaderConfig = prodConfig.module.rules.find(rule => rule.loader === 'babel-loader'); + prodConfig.module.rules.push(babelLoaderRule(allModulePaths)); - babelLoaderConfig.options.plugins = (babelLoaderConfig.options.plugins || []).concat([ - [require.resolve('babel-plugin-remove-jsx-attributes'), { - patterns: ['^data-test.*$'] - }] - ]); + const webpackConfig = smp.wrap({ plugins: prodConfig.plugins }); + webpackConfig.plugins.push( + new MiniCssExtractPlugin({ filename: 'style.[contenthash].css' }) + ); - return prodConfig; -} + return { ...prodConfig, ...webpackConfig }; +}; module.exports = buildConfig; diff --git a/webpack.config.cli.dev.shared.styles.js b/webpack.config.cli.shared.styles.js similarity index 100% rename from webpack.config.cli.dev.shared.styles.js rename to webpack.config.cli.shared.styles.js diff --git a/webpack.config.cli.transpile.js b/webpack.config.cli.transpile.js new file mode 100644 index 0000000..c8a6896 --- /dev/null +++ b/webpack.config.cli.transpile.js @@ -0,0 +1,109 @@ +// Top level default Webpack configuration used for transpiling individual modules +// before publishing +const path = require('path'); +const webpack = require('webpack'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const babelOptions = require('./webpack/babel-options'); +const { processExternals } = require('./webpack/utils'); +const { defaultExternals } = require('./consts'); + +const config = { + mode: 'production', + devtool: 'source-map', + entry: path.resolve('./index.js'), + output: { + library: { + type: 'umd', + }, + path: path.resolve('./dist'), + filename: 'index.js', + umdNamedDefine: true, + }, + module: { + rules: [ + { + test: /\.(js|jsx|ts|tsx)$/, + exclude: /node_modules/, + loader: 'babel-loader', + options: babelOptions, + }, + { + test: /\.(woff2?)$/, + type: 'asset/resource', + generator: { + filename: './fonts/[name].[contenthash].[ext]', + }, + }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[local]---[hash:base64:5]', + }, + sourceMap: true, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + config: path.resolve(__dirname, 'postcss.config.js'), + }, + sourceMap: true, + }, + }, + ], + }, + { + test: /\.(jpg|jpeg|gif|png|ico)$/, + type: 'asset/resource', + generator: { + filename: './img/[name].[contenthash].[ext]', + }, + }, + { + test: /\.svg$/, + use: [{ + loader: 'url-loader', + options: { + esModule: false, + }, + }] + }, + { + test: /\.js.map$/, + enforce: "pre", + use: ['source-map-loader'], + } + ] + }, + // Set default externals. These can be extended by individual modules. + externals: processExternals(defaultExternals), +}; + +config.optimization = { + mangleWasmImports: true, + minimizer: [ + '...', // in webpack@5 we can use the '...' syntax to extend existing minimizers + new CssMinimizerPlugin(), + ], + minimize: true, +}; + +config.plugins = [ + new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new webpack.EnvironmentPlugin(['NODE_ENV']), +]; + +module.exports = config; diff --git a/webpack/babel-loader-rule.js b/webpack/babel-loader-rule.js index 5cd979f..78f62f8 100644 --- a/webpack/babel-loader-rule.js +++ b/webpack/babel-loader-rule.js @@ -1,49 +1,81 @@ const path = require('path'); + const babelOptions = require('./babel-options'); -const { getModulesPaths } = require('./module-paths'); +const { + getNonTranspiledModules, + getTranspiledModules, +} = require('./module-paths'); // a space delimited list of strings (typically namespaces) to use in addition // to "@folio" to determine if something needs Stripes-flavoured transpilation -const extraTranspile = process.env.STRIPES_TRANSPILE_TOKENS ? process.env.STRIPES_TRANSPILE_TOKENS.split(' ') : []; - -// These modules are already transpiled and should be excluded -const folioScopeBlacklist = [ -].map(segment => path.join('@folio', segment)); - -// Packages on NPM are typically distributed already transpiled. For historical -// reasons, Stripes modules are not and have their babel config centralised -// here. This ought to have changed by now, but for now the following logic is -// in effect and modules will be transpiled if: -// -// * they are in the @folio namespace -// * their name contains a string from STRIPES_TRANSPILE_TOKENS -// (typically other namespaces) -// * they aren't in node_modules (typically in a workspace) -// -// You'll see some chicanery here: we are only interested in these strings if -// they occur after the last instance of "node_modules" since, in some -// situations, our dependencies will get their own node_modules directories and -// while we want to transpile "@folio/ui-users/somefile.js" we don't want to -// transpile "@folio/ui-users/node_modules/nightmare/somefile.js" -function babelLoaderTest(fileName, modules) { - const nodeModIdx = fileName.lastIndexOf('node_modules'); - - if (fileName.endsWith('.js') - && (nodeModIdx === -1 - || ['@folio', ...extraTranspile].reduce((acc, cur) => (fileName.lastIndexOf(cur) > nodeModIdx) || acc, false) // is filename in folio namespace - || modules.findIndex(moduleName => fileName.includes(moduleName)) !== -1) // if file in stripes config modules - && (folioScopeBlacklist.findIndex(ignore => fileName.includes(ignore)) === -1)) { - return true; +const extraTranspile = process.env.STRIPES_TRANSPILE_TOKENS ? new RegExp(process.env.STRIPES_TRANSPILE_TOKENS.replaceAll(' ', '|')) : ''; + +// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex/6969486#6969486 +const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +module.exports = (modulePaths) => { + const modulesToTranspile = getNonTranspiledModules(modulePaths); + const transpiledModules = getTranspiledModules(modulePaths); + let includeRegex; + let excludeRegex; + + if (modulesToTranspile.length) { + includeRegex = new RegExp(modulesToTranspile.map(escapeRegExp).join('|')); + console.info('\nmodules to transpile:\n'); + modulesToTranspile.sort().forEach(m => console.log(m)); + } + + if (transpiledModules.length) { + excludeRegex = new RegExp(transpiledModules.map(escapeRegExp).join('|')) + console.info('\ntranspiled modules:\n'); + transpiledModules.forEach(m => console.log(m)); } - return false; -} -module.exports = (stripesConfig) => { - const stripesDepsPaths = getModulesPaths(stripesConfig.modules); + const folioModulePath = path.join('node_modules', '@folio'); + // A negative lookahead regex to find @folio modules present in node_modules + // which are still not transpiled ('dist' folder is not present). + // This currently happens when folio module is not listed in stripes config + // or under stripes.stripesDeps and another folio module includes it as a dependency. + // TODO: remove this after all modules are transpiled + const folioModulesRegex = new RegExp(`${escapeRegExp(folioModulePath)}(?!.*dist)`); return { - test: filename => babelLoaderTest(filename, stripesDepsPaths), loader: 'babel-loader', + test: /\.js$/, + include: function(modulePath) { + // exclude empty modules + if (!modulePath) { + return false; + } + + // regex which represents modules which should be included for transpilation + if (includeRegex && includeRegex.test(modulePath)) { + return true; + } + + // include STRIPES_TRANSPILE_TOKENS in transpilation + if (extraTranspile && extraTranspile.test(modulePath)) { + return true; + } + + // regex which represents modules which should be excluded from transpilation + if (excludeRegex && excludeRegex.test(modulePath)) { + return false; + } + + // if untranspiled @folio module is present in node_modules + // just transpile it + if (folioModulesRegex.test(modulePath)) { + return true; + } + + // skip everything from node_modules + if (/node_modules/.test(modulePath)) { + return false; + } + + return true; + }, options: { cacheDirectory: true, ...babelOptions, diff --git a/webpack/babel-options.js b/webpack/babel-options.js index 72522e8..be02a46 100644 --- a/webpack/babel-options.js +++ b/webpack/babel-options.js @@ -28,5 +28,7 @@ module.exports = { '@babel/plugin-proposal-throw-expressions', '@babel/plugin-syntax-import-meta', utils.isDevelopment && require.resolve('react-refresh/babel'), + utils.isProduction && ['remove-jsx-attributes', { patterns: [ '^data-test.*$' ] }] + ].filter(Boolean), }; diff --git a/webpack/build.js b/webpack/build.js index b4e93ba..ac5cb8c 100644 --- a/webpack/build.js +++ b/webpack/build.js @@ -5,6 +5,7 @@ const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); const applyWebpackOverrides = require('./apply-webpack-overrides'); const logger = require('./logger')(); const buildConfig = require('../webpack.config.cli.prod'); +const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const platformModulePath = path.join(path.resolve(), 'node_modules'); @@ -14,6 +15,8 @@ module.exports = function build(stripesConfig, options) { let config = buildConfig(stripesConfig); + config = sharedStylesConfig(config, {}); + if (!options.skipStripesBuild) { config.plugins.push(new StripesWebpackPlugin({ stripesConfig, createDll: options.createDll })); } diff --git a/webpack/module-paths.js b/webpack/module-paths.js index ab0f952..1c612c8 100644 --- a/webpack/module-paths.js +++ b/webpack/module-paths.js @@ -132,8 +132,8 @@ function getStripesDepsPaths(packageJsonPath) { const stripesDeps = stripes.stripesDeps; return stripesDeps.map(dep => { - const path = locatePackageJsonPath(dep); - return path ? path.replace('/package.json', '') : null; + const packageJsonPath = locatePackageJsonPath(dep); + return packageJsonPath ? path.dirname(packageJsonPath) : null; }); } @@ -152,6 +152,7 @@ function getStripesDepsPaths(packageJsonPath) { * './node_modules/@reshare/directory' * ] * + * */ function getModulesPaths(modules) { return Object @@ -160,7 +161,7 @@ function getModulesPaths(modules) { const packageJsonPath = locatePackageJsonPath(module); if (packageJsonPath) { - const modulePaths = [packageJsonPath.replace('/package.json', '')]; + const modulePaths = [path.dirname(packageJsonPath)]; const stripesDepPaths = getStripesDepsPaths(packageJsonPath); if (stripesDepPaths) { @@ -175,8 +176,77 @@ function getModulesPaths(modules) { .filter(module => !!module); } +/** + * Return full paths for all stripes dependencies defined in: + * + * https://github.com/folio-org/stripes/blob/ab01ed9c8d60d020d76f5682406b3bf901c24e76/package.json#L20-L27 + * +*/ +function getStripesModulesPaths() { + const packageJsonPath = locatePackageJsonPath('@folio/stripes'); + const packageJson = require(packageJsonPath); + const paths = []; + + if (!packageJson) { + return paths; + } + + Object.keys(packageJson.dependencies).forEach(moduleName => { + if (moduleName.match('@folio')) { + const stripesModulePath = locatePackageJsonPath(moduleName); + + if (stripesModulePath) { + paths.push(path.dirname(stripesModulePath)); + } + } + }); + + return paths; +} + +function getNonTranspiledModules(modules) { + const nonTranspiledModules = ['stripes-config']; + + modules.forEach(module => { + const distPath = tryResolve(path.join(module, 'dist')); + if (!distPath) { + nonTranspiledModules.push(module.split(path.sep).pop()); + } + }); + + return [...new Set(nonTranspiledModules)]; +} + +function getTranspiledModules(modules) { + const transpiledModules = []; + + modules.forEach(module => { + const distPath = tryResolve(path.join(module, 'dist')); + + if (distPath) { + transpiledModules.push(distPath); + } + }); + + return transpiledModules; +} + +function getTranspiledCssPaths(modules) { + const cssPaths = []; + + modules.forEach(module => { + const cssPath = tryResolve(path.join(module, 'dist', 'style.css')); + + if (cssPath) { + cssPaths.push(cssPath); + } + }); + + return cssPaths; +} + function getSharedStyles(filename) { - return path.resolve(generateStripesAlias('@folio/stripes-components'), filename + ".css"); + return path.resolve(generateStripesAlias('@folio/stripes-components'), `${filename}.css`); } module.exports = { @@ -185,4 +255,8 @@ module.exports = { getSharedStyles, locateStripesModule, getModulesPaths, + getStripesModulesPaths, + getNonTranspiledModules, + getTranspiledModules, + getTranspiledCssPaths, }; diff --git a/webpack/serve.js b/webpack/serve.js index 174af24..f3d60d6 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -8,6 +8,7 @@ const StripesWebpackPlugin = require('./stripes-webpack-plugin'); const applyWebpackOverrides = require('./apply-webpack-overrides'); const logger = require('./logger')(); const buildConfig = require('../webpack.config.cli.dev'); +const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const cwd = path.resolve(); const platformModulePath = path.join(cwd, 'node_modules'); @@ -18,15 +19,12 @@ module.exports = function serve(stripesConfig, options) { if (typeof stripesConfig.okapi !== 'object') throw new Error('Missing Okapi config'); if (typeof stripesConfig.okapi.url !== 'string') throw new Error('Missing Okapi URL'); if (stripesConfig.okapi.url.endsWith('/')) throw new Error('Trailing slash in Okapi URL will prevent Stripes from functioning'); + return new Promise((resolve) => { logger.log('starting serve...'); const app = express(); let config = buildConfig(stripesConfig); - let developmentConfig = require('../webpack.config.cli.dev.shared.styles'); - - if (process.env.NODE_ENV === 'development') { - config = developmentConfig(config, {}); - } + config = sharedStylesConfig(config, {}); config.plugins.push(new StripesWebpackPlugin({ stripesConfig })); diff --git a/webpack/stripes-node-api.js b/webpack/stripes-node-api.js index 2fd1d2a..afffb6d 100644 --- a/webpack/stripes-node-api.js +++ b/webpack/stripes-node-api.js @@ -1,7 +1,9 @@ const build = require('./build'); const serve = require('./serve'); +const transpile = require('./transpile'); module.exports = { build, serve, + transpile, }; diff --git a/webpack/transpile.js b/webpack/transpile.js new file mode 100644 index 0000000..a31bb0c --- /dev/null +++ b/webpack/transpile.js @@ -0,0 +1,43 @@ +const path = require('path'); +const webpack = require('webpack'); +const applyWebpackOverrides = require('./apply-webpack-overrides'); +const logger = require('./logger')(); +const { tryResolve } = require('./module-paths'); + +module.exports = function transpile(options = {}) { + return new Promise((resolve, reject) => { + logger.log('starting build...'); + let config = require('../webpack.config.cli.transpile'); + + // TODO: allow for name customization + const moduleTranspileConfigPath = path.join(process.cwd(), 'webpack.transpile.config.js'); + const packagePath = path.join(process.cwd(), 'package.json'); + + if (tryResolve(moduleTranspileConfigPath)) { + const moduleTranspileConfig = require(moduleTranspileConfigPath); + moduleTranspileConfig.externals = { ...config.externals, ...moduleTranspileConfig.externals }; + config = { ...config, ...moduleTranspileConfig } + } + + if (tryResolve(packagePath)) { + const packageJson = require(packagePath); + config.output.library = { + type: 'umd', + name: packageJson.name, + }; + } + + // Give the caller a chance to apply their own webpack overrides + config = applyWebpackOverrides(options.webpackOverrides, config); + + const compiler = webpack(config); + + compiler.run((err, stats) => { + if (err) { + reject(err); + } else { + resolve(stats); + } + }); + }); +}; diff --git a/webpack/tsconfig.json b/webpack/tsconfig.json index f752869..9d3be5c 100644 --- a/webpack/tsconfig.json +++ b/webpack/tsconfig.json @@ -12,7 +12,11 @@ "../../**/*.ts", "../../**/*.tsx", "node_modules/@folio/**/*.ts", - "node_modules/@folio/**/*.tsx" + "node_modules/@folio/**/*.tsx", + "../node_modules/@folio/**/*.ts", + "../node_modules/@folio/**/*.tsx", + "../../node_modules/@folio/**/*.ts", + "../../node_modules/@folio/**/*.tsx" ], "exclude": [ "../../**/*.test.ts", @@ -22,6 +26,10 @@ "node_modules/@folio/**/*.test.ts", "node_modules/@folio/**/*.test.tsx", "node_modules/@folio/**/test/**/*.ts", - "node_modules/@folio/**/test/**/*.tsx" + "node_modules/@folio/**/test/**/*.tsx", + "../node_modules/@folio/**/test/**/*.ts", + "../node_modules/@folio/**/test/**/*.tsx", + "../../node_modules/@folio/**/test/**/*.ts", + "../../node_modules/@folio/**/test/**/*.tsx" ] } diff --git a/webpack/utils.js b/webpack/utils.js index 3c82075..73aa472 100644 --- a/webpack/utils.js +++ b/webpack/utils.js @@ -1,3 +1,21 @@ +const isDevelopment = process.env.NODE_ENV === 'development'; +const isProduction = process.env.NODE_ENV === 'production'; +const processExternals = (externals) => { + return externals.reduce((acc, name) => { + acc[name] = { + root: name, + commonjs2: name, + commonjs: name, + amd: name, + umd: name + }; + + return acc; + }, {}); +}; + module.exports = { - isDevelopment: process.env.NODE_ENV === 'development' + processExternals, + isDevelopment, + isProduction, };