diff --git a/consts.js b/consts.js new file mode 100644 index 0000000..1db7962 --- /dev/null +++ b/consts.js @@ -0,0 +1,29 @@ +// TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 +// Anythign that we want *the platform to provide to modules should be here. +// If an item is not in this list, modules will each load their own version of it. +// This can be problematic for React Context if mutliple copies of the same context are loaded. + +const singletons = { + '@folio/stripes': '^9.3.0', + '@folio/stripes-components': '^13.1.0', + '@folio/stripes-connect': '^10.0.1', + '@folio/stripes-core': '^11.1.0', + '@folio/stripes-shared-context': '^1.0.0', + "moment": "^2.29.0", + 'react': '~18.3', + 'react-dom': '~18.3', + 'react-intl': '^7.1.14', + 'react-query': '^3.39.3', + 'react-redux': '^8.1', + 'react-router': '^5.2.0', + 'react-router-dom': '^5.2.0', + 'redux-observable': '^1.2.0', + 'rxjs': '^6.6.3' +}; + +const defaultRegistryUrl = 'http://localhost:3001/registry'; + +module.exports = { + defaultRegistryUrl, + singletons, +}; diff --git a/package.json b/package.json index c79f806..945ca4b 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@svgr/webpack": "^8.1.0", "add-asset-html-webpack-plugin": "^6.0.0", + "axios": "^1.3.6", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.3", "buffer": "^6.0.3", "connect-history-api-fallback": "^1.3.0", "core-js": "^3.6.1", + "cors": "^2.8.5", "css-loader": "^6.4.0", "csv-loader": "^3.0.3", "debug": "^4.0.1", @@ -52,6 +54,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.7.6", "node-object-hash": "^1.2.0", + "portfinder": "^1.0.32", "postcss": "^8.4.2", "postcss-custom-media": "^9.0.1", "postcss-import": "^15.0.1", @@ -70,6 +73,7 @@ "util-ex": "^0.3.15", "validate-npm-package-name": "^6.0.2", "webpack-dev-middleware": "^5.2.1", + "webpack-dev-server": "^4.13.1", "webpack-hot-middleware": "^2.25.1", "webpack-remove-empty-scripts": "^1.0.1", "webpack-virtual-modules": "^0.4.3" @@ -88,4 +92,4 @@ "react-dom": "^18.2.0", "webpack": "^5.58.1" } -} +} \ No newline at end of file diff --git a/webpack.config.base.js b/webpack.config.base.js index 0be1037..14d76f3 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -5,11 +5,16 @@ 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 { ModuleFederationPlugin } = require('webpack').container; -const { generateStripesAlias } = require('./webpack/module-paths'); +const { generateStripesAlias, } = require('./webpack/module-paths'); +const { processShared } = require('./webpack/utils'); const typescriptLoaderRule = require('./webpack/typescript-loader-rule'); const { isProduction } = require('./webpack/utils'); const { getTranspiledCssPaths } = require('./webpack/module-paths'); +const { singletons } = require('./consts'); + +const shared = processShared(singletons, { singleton: true, eager: true }); // 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 @@ -65,6 +70,7 @@ const baseConfig = { }), new webpack.EnvironmentPlugin(['NODE_ENV']), new RemoveEmptyScriptsPlugin(), + new ModuleFederationPlugin({ name: 'host', shared }), ], module: { rules: [ @@ -131,7 +137,6 @@ const baseConfig = { }, }; - const buildConfig = (modulePaths) => { const transpiledCssPaths = getTranspiledCssPaths(modulePaths); const cssDistPathRegex = /dist[\/\\]style\.css/; diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 19dad06..4c42e83 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -9,7 +9,7 @@ const esbuildLoaderRule = require('./webpack/esbuild-loader-rule'); const utils = require('./webpack/utils'); const buildBaseConfig = require('./webpack.config.base'); const cli = require('./webpack.config.cli'); - +const StripesFederationPlugin = require('./webpack/stripes-federation-plugin'); const useBrowserMocha = () => { return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; @@ -56,7 +56,8 @@ const buildConfig = (stripesConfig) => { if (utils.isDevelopment) { devConfig.plugins = devConfig.plugins.concat([ new webpack.HotModuleReplacementPlugin(), - new ReactRefreshWebpackPlugin() + new ReactRefreshWebpackPlugin(), + new StripesFederationPlugin(stripesConfig) ]); } diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js new file mode 100644 index 0000000..db97085 --- /dev/null +++ b/webpack.config.federate.remote.js @@ -0,0 +1,145 @@ +const path = require('path'); +const webpack = require('webpack'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const StripesTranslationsPlugin = require('./webpack/stripes-translations-plugin'); +const { container } = webpack; +const { processExternals, processShared } = require('./webpack/utils'); +const { getStripesModulesPaths } = require('./webpack/module-paths'); +const esbuildLoaderRule = require('./webpack/esbuild-loader-rule'); +const { singletons } = require('./consts'); + +const buildConfig = (metadata) => { + const { host, port, name, displayName, main } = metadata; + + // using main from metadata since the location of main could vary between modules. + let mainEntry = path.join(process.cwd(), main || 'index.js'); + const stripesModulePaths = getStripesModulesPaths(); + const translationsPath = path.join(process.cwd(), 'translations', displayName.split('.').shift()); + const iconsPath = path.join(process.cwd(), 'icons'); + + // yeah, yeah, soundsPath vs sound. sorry. `sound` is a legacy name. + // other paths are plural and I'm sticking with that convention. + const soundsPath = path.join(process.cwd(), 'sound'); + + const shared = processShared(singletons, { singleton: true }); + + const config = { + name, + devtool: 'inline-source-map', + mode: 'development', + entry: mainEntry, + output: { + publicPath: `${host}:${port}/`, + }, + devServer: { + port: port, + open: false, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + static: [ + { + directory: translationsPath, + publicPath: '/translations' + }, + { + directory: iconsPath, + publicPath: '/icons' + }, + { + directory: soundsPath, + publicPath: '/sounds' + }, + ] + }, + module: { + rules: [ + esbuildLoaderRule(stripesModulePaths), + { + 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: /\.svg$/, + type: 'asset/inline', + resourceQuery: { not: /icon/ } // exclude built-in icons from stripes-components which are loaded as react components. + }, + { + test: /\.svg$/, + resourceQuery: /icon/, // stcom icons use this query on the resource. + use: ['@svgr/webpack'] + }, + { + test: /\.js.map$/, + enforce: "pre", + use: ['source-map-loader'], + } + ] + }, + // TODO: remove this after stripes-config is gone. + externals: processExternals({ 'stripes-config': true }), + plugins: [ + new StripesTranslationsPlugin({ federate: true }), + new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }), + new container.ModuleFederationPlugin({ + library: { type: 'var', name }, + name, + filename: 'remoteEntry.js', + exposes: { + './MainEntry': mainEntry, + }, + shared + }), + ] + }; + + return config; +} + +module.exports = buildConfig; diff --git a/webpack/federate.js b/webpack/federate.js new file mode 100644 index 0000000..7380f8f --- /dev/null +++ b/webpack/federate.js @@ -0,0 +1,67 @@ +const path = require('path'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); +const axios = require('axios'); +const { snakeCase } = require('lodash'); +const portfinder = require('portfinder'); + +const buildConfig = require('../webpack.config.federate.remote'); +const { tryResolve } = require('./module-paths'); +const logger = require('./logger')(); + +// Remotes will be serve starting from port 3002 +portfinder.setBasePort(3002); + +module.exports = async function federate(options = {}) { + logger.log('starting federation...'); + + const packageJsonPath = tryResolve(path.join(process.cwd(), 'package.json')); + + if (!packageJsonPath) { + console.error('package.json not found'); + process.exit(); + } + + const port = options.port ?? await portfinder.getPortPromise(); + const host = `http://localhost`; + const url = `${host}:${port}/remoteEntry.js`; + + const { name: packageName, version, description, stripes, main } = require(packageJsonPath); + const { permissionSets: _, ...stripesRest } = stripes; + const name = snakeCase(packageName); + const metadata = { + module: packageName, + version, + description, + host, + port, + url, + name, + main, + ...stripesRest, + }; + + const config = buildConfig(metadata); + + // TODO: allow for configuring registryUrl via env var or stripes config + const registryUrl = 'http://localhost:3001/registry'; + + // update registry + axios.post(registryUrl, metadata).catch(error => { + console.error(`Registry not found. Please check ${registryUrl}`); + process.exit(); + }); + + const compiler = webpack(config); + const server = new WebpackDevServer(config.devServer, compiler); + console.log(`Starting remote server on port ${port}`); + server.start(); + + compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { + try { + await axios.delete(registryUrl, { data: metadata }); + } catch (error) { + console.error(`registry not found. Please check ${registryUrl}`); + } + }); +}; diff --git a/webpack/module-paths.js b/webpack/module-paths.js index 00ea92b..de1ce03 100644 --- a/webpack/module-paths.js +++ b/webpack/module-paths.js @@ -63,7 +63,7 @@ function locateStripesModule(context, moduleName, alias, ...segments) { } // When available, try for the alias first - if (alias[moduleName]) { + if (alias && alias[moduleName]) { tryPaths.unshift({ request: path.join(alias[moduleName], ...segments), }); @@ -264,4 +264,5 @@ module.exports = { getNonTranspiledModules, getTranspiledModules, getTranspiledCssPaths, + locatePackageJsonPath, }; diff --git a/webpack/registryServer.js b/webpack/registryServer.js new file mode 100644 index 0000000..b4e3fb4 --- /dev/null +++ b/webpack/registryServer.js @@ -0,0 +1,44 @@ +const express = require('express'); +const cors = require('cors'); + +// Registry data +const registry = { remotes: {} }; + +const registryServer = { + start: () => { + const app = express(); + + app.use(express.json()); + app.use(cors()); + + // add/update remote to registry + app.post('/registry', (req, res) => { + const metadata = req.body; + const { name } = metadata; + + registry.remotes[name] = metadata; + res.status(200).send(`Remote ${name} metadata updated`); + }); + + // return entire registry for machines + app.get('/registry', (_, res) => res.json(registry)); + + // return entire registry for humans + app.get('/code', (_, res) => res.send(`
${JSON.stringify(registry, null, 2)}`));
+
+ app.delete('/registry', (req, res) => {
+ const metadata = req.body;
+ const { name } = metadata;
+
+ delete registry.remotes[name];
+
+ res.status(200).send(`Remote ${name} removed`);
+ });
+
+ app.listen(3001, () => {
+ console.log('Starting registry server at http://localhost:3001');
+ });
+ }
+};
+
+module.exports = registryServer;
diff --git a/webpack/serve.js b/webpack/serve.js
index ef64a1c..2d64af8 100644
--- a/webpack/serve.js
+++ b/webpack/serve.js
@@ -9,6 +9,7 @@ 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 registryServer = require('./registryServer');
const cwd = path.resolve();
const platformModulePath = path.join(cwd, 'node_modules');
@@ -24,6 +25,15 @@ module.exports = function serve(stripesConfig, options) {
logger.log('starting serve...');
const app = express();
+
+ // stripes module registry
+ try {
+ registryServer.start();
+ }
+ catch (e) {
+ console.error(e)
+ }
+
let config = buildConfig(stripesConfig);
config = sharedStylesConfig(config, {});
diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js
index bb2c48e..d5579ed 100644
--- a/webpack/stripes-config-plugin.js
+++ b/webpack/stripes-config-plugin.js
@@ -13,6 +13,7 @@ const { SyncHook } = require('tapable');
const stripesModuleParser = require('./stripes-module-parser');
const StripesBuildError = require('./stripes-build-error');
const stripesSerialize = require('./stripes-serialize');
+const { defaultRegistryUrl } = require('../consts');
const logger = require('./logger')('stripesConfigPlugin');
const stripesConfigPluginHooksMap = new WeakMap();
@@ -47,7 +48,14 @@ module.exports = class StripesConfigPlugin {
const enabledModules = this.options.modules;
logger.log('enabled modules:', enabledModules);
const { config, metadata, icons, stripesDeps, warnings, lazyImports } = stripesModuleParser.parseAllModules(enabledModules, compiler.context, compiler.options.resolve.alias, this.lazy);
- this.mergedConfig = Object.assign({}, this.options, { modules: config });
+ const modulesInitialState = {
+ app: [],
+ handler: [],
+ plugin: [],
+ settings: [],
+ };
+ this.mergedOkapi = Object.assign({ registryUrl: defaultRegistryUrl }, this.options.okapi);
+ this.mergedConfig = Object.assign({ modules: modulesInitialState }, this.options, { modules: config, okapi: this.mergedOkapi });
this.metadata = metadata;
this.icons = icons;
this.warnings = warnings;
@@ -58,18 +66,16 @@ module.exports = class StripesConfigPlugin {
StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap(
{ name: 'StripesConfigPlugin', context: true },
- context => Object.assign(context, { config, metadata, icons, stripesDeps, warnings }));
+ context => Object.assign(context, { config }));
// Wait until after other plugins to generate virtual stripes-config
compiler.hooks.afterPlugins.tap('StripesConfigPlugin', (theCompiler) => this.afterPlugins(theCompiler));
- compiler.hooks.emit.tapAsync('StripesConfigPlugin', (compilation, callback) => this.processWarnings(compilation, callback));
}
afterPlugins(compiler) {
// Data provided by other stripes plugins via hooks
const pluginData = {
branding: {},
- errorLogging: {},
translations: {},
};
@@ -79,22 +85,15 @@ module.exports = class StripesConfigPlugin {
const stripesVirtualModule = `
${Array.from(this.lazyImports).join('\n')}
const { okapi, config, modules } = ${serialize(this.mergedConfig, { space: 2 })};
- const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)};
const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)};
+ const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)};
const translations = ${serialize(pluginData.translations, { space: 2 })};
const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)};
const icons = ${stripesSerialize.serializeWithRequire(this.icons)};
- export { okapi, config, modules, branding, errorLogging, translations, metadata, icons };
+ export { branding, config, errorLogging, icons, metadata, modules, okapi, translations };
`;
logger.log('writing virtual module...', stripesVirtualModule);
this.virtualModule.writeModule('node_modules/stripes-config.js', stripesVirtualModule);
}
-
- processWarnings(compilation, callback) {
- if (this.warnings.length) {
- compilation.warnings.push(new StripesBuildError(`stripes-config-plugin:\n ${this.warnings.join('\n ')}`));
- }
- callback();
- }
};
diff --git a/webpack/stripes-federation-plugin.js b/webpack/stripes-federation-plugin.js
new file mode 100644
index 0000000..50fcf21
--- /dev/null
+++ b/webpack/stripes-federation-plugin.js
@@ -0,0 +1,38 @@
+// This webpack plugin wraps all other stripes webpack plugins to simplify inclusion within the webpack config
+const spawn = require('child_process').spawn;
+const path = require('path');
+const portfinder = require('portfinder');
+
+const { locateStripesModule } = require('./module-paths');
+
+portfinder.setBasePort(3002);
+
+module.exports = class StripesFederationPlugin {
+ constructor(stripesConfig) {
+ this.stripesConfig = stripesConfig;
+ }
+
+ async startRemotes(modules) {
+ const ctx = process.cwd();
+
+ for (const moduleName in modules) {
+ const packageJsonPath = locateStripesModule(ctx, moduleName, {}, 'package.json');
+ const basePath = path.dirname(packageJsonPath);
+
+ portfinder.getPort((err, port) => {
+ const child = spawn(`yarn stripes federate --port ${port}`, {
+ cwd: basePath,
+ shell: true,
+ });
+
+ child.stdout.pipe(process.stdout);
+ });
+ }
+ }
+
+ apply() {
+ const { modules } = this.stripesConfig;
+
+ this.startRemotes(modules);
+ }
+};
diff --git a/webpack/stripes-node-api.js b/webpack/stripes-node-api.js
index afffb6d..2b0d9d7 100644
--- a/webpack/stripes-node-api.js
+++ b/webpack/stripes-node-api.js
@@ -1,9 +1,11 @@
const build = require('./build');
const serve = require('./serve');
const transpile = require('./transpile');
+const federate = require('./federate');
module.exports = {
build,
serve,
transpile,
+ federate,
};
diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js
index c2dcbb2..2d04d21 100644
--- a/webpack/stripes-translations-plugin.js
+++ b/webpack/stripes-translations-plugin.js
@@ -16,23 +16,30 @@ function prefixKeys(obj, prefix) {
module.exports = class StripesTranslationPlugin {
constructor(options) {
+ // in module federation mode, we emit translations for the module being built and
+ // for any stripesDeps it has.
+ this.federate = options?.federate || false;
+
// Include stripes-core et al because they have translations
- this.modules = {
+ // translations should come from the host application for stripes
+ // rather than being overwritten by consuming apps.
+ this.modules = this.federate ? {} : {
'@folio/stripes-core': {},
'@folio/stripes-components': {},
'@folio/stripes-smart-components': {},
'@folio/stripes-form': {},
'@folio/stripes-ui': {},
};
- Object.assign(this.modules, options.modules);
- this.languageFilter = options.config.languages || [];
+
+ Object.assign(this.modules, options?.modules);
+ this.languageFilter = options?.config?.languages || [];
logger.log('language filter', this.languageFilter);
}
apply(compiler) {
// Used to help locate modules
this.context = compiler.context;
- // 'publicPath' is not present when running tests via karma-webpack
+ // 'publicPath' is not present when running tests via karma-webpack
// so when running in test mode use absolute 'path'.
this.publicPath = process.env.NODE_ENV !== 'test' ? compiler.options.output.publicPath : `./absolute${compiler.options.output.path}`;
this.aliases = compiler.options.resolve.alias;
@@ -44,27 +51,30 @@ module.exports = class StripesTranslationPlugin {
new webpack.ContextReplacementPlugin(/moment[/\\]locale/, filterRegex).apply(compiler);
}
- // Hook into stripesConfigPlugin to supply paths to translation files
- // and gather additional modules from stripes.stripesDeps
- StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => {
- // Add stripesDeps
- for (const [key, value] of Object.entries(context.stripesDeps)) {
- // TODO: merge translations from all versions of stripesDeps
- this.modules[key] = value[value.length - 1];
+ if (this.federate) {
+ const packageJsonPath = path.join(this.context, 'package.json');
+ const packageJson = StripesTranslationPlugin.loadFile(packageJsonPath);
+
+ this.modules[packageJson.name] = {};
+ if (packageJson) {
+ const stripesDeps = packageJson?.stripes?.stripesDeps;
+ if (stripesDeps) {
+ stripesDeps.forEach((dep) => {
+ // TODO: merge translations from all versions of stripesDeps
+ this.modules[dep] = {};
+ });
+ }
}
- // Gather all translations available in each module
- const allTranslations = this.gatherAllTranslations();
-
- const fileData = this.generateFileNames(allTranslations);
- const allFiles = _.mapValues(fileData, data => data.browserPath);
+ compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => {
+ compilation.hooks.processAssets.tap({
+ name: 'StripesTranslationsPlugin',
+ stage: compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS
+ }, () => {
- config.translations = allFiles;
- logger.log('stripesConfigPluginBeforeWrite', config.translations);
+ const allTranslations = this.gatherAllTranslations();
+ const fileData = this.generateFileNames(allTranslations, false);
- compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => {
- // Emit merged translations to the output directory
- compilation.hooks.processAssets.tap('StripesTranslationsPlugin', () => {
Object.keys(allTranslations).forEach((language) => {
logger.log(`emitting translations for ${language} --> ${fileData[language].emitPath}`);
const content = JSON.stringify(allTranslations[language]);
@@ -75,7 +85,34 @@ module.exports = class StripesTranslationPlugin {
});
});
});
- });
+ } else {
+ // Hook into stripesConfigPlugin to supply paths to translation files
+ // and gather additional modules from stripes.stripesDeps
+ StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => {
+ // Gather all translations available in each module
+ const allTranslations = this.gatherAllTranslations();
+
+ const fileData = this.generateFileNames(allTranslations);
+ const allFiles = _.mapValues(fileData, data => data.browserPath);
+
+ config.translations = allFiles;
+ logger.log('stripesConfigPluginBeforeWrite', config.translations);
+
+ compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => {
+ // Emit merged translations to the output directory
+ compilation.hooks.processAssets.tap('StripesTranslationsPlugin', () => {
+ Object.keys(allTranslations).forEach((language) => {
+ logger.log(`emitting translations for ${language} --> ${fileData[language].emitPath}`);
+ const content = JSON.stringify(allTranslations[language]);
+ compilation.assets[fileData[language].emitPath] = {
+ source: () => content,
+ size: () => content.length,
+ };
+ });
+ });
+ });
+ });
+ }
}
// Locate each module's translations directory (current) or package.json data (fallback)
@@ -176,14 +213,14 @@ module.exports = class StripesTranslationPlugin {
}
// Assign output path names for each to be accessed later by stripes-config-plugin
- generateFileNames(allTranslations) {
+ generateFileNames(allTranslations, useSuffix = true) {
const files = {};
- const timestamp = Date.now(); // To facilitate cache busting, could also generate a hash
+ const timestamp = useSuffix ? Date.now() : ''; // To facilitate cache busting, could also generate a hash
Object.keys(allTranslations).forEach((language) => {
files[language] = {
// Fetching from the browser must take into account public path. The replace regex removes double slashes
browserPath: `${this.publicPath}/translations/${language}-${timestamp}.json`.replace(/\/\//, '/'),
- emitPath: `translations/${language}-${timestamp}.json`,
+ emitPath: `translations/${language}${timestamp ? `-${timestamp}` : ''}.json`,
};
});
return files;
diff --git a/webpack/utils.js b/webpack/utils.js
index b29bfda..d106d65 100644
--- a/webpack/utils.js
+++ b/webpack/utils.js
@@ -14,8 +14,20 @@ const processExternals = (peerDeps) => {
}, {});
};
+const processShared = (shared, options = {}) => {
+ return Object.keys(shared).reduce((acc, name) => {
+ acc[name] = {
+ requiredVersion: shared[name],
+ ...options
+ };
+
+ return acc;
+ }, {});
+};
+
module.exports = {
processExternals,
isDevelopment,
isProduction,
+ processShared,
};