From a9b3e757088345fb53c3896c517fd6780360e2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Thu, 26 Jan 2023 11:55:52 +0100 Subject: [PATCH 1/2] feat: support internal paths in exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that we only support .css and .js exports for now. In the future we may want to support .json, .svg, or other static resources we see fit. This is part of [https://issues.liferay.com/browse/LPS-172747](LPS-172747). ---- ⚠ WARNING !!! This implementation is missing a corner use case: an AMD module that wants to use an internal path export ending in .js. We won't support that case unless it is absolutely necessary, since it can be fixed migrating the offending project from AMD to ESM. ---- --- .../src/utils/createEsm2AmdExportsBridges.js | 109 ++++++++++++------ .../src/utils/runWebpackAsBundler.js | 51 +++++++- .../src/utils/webpackExportCssLoader.js | 30 +++++ 3 files changed, 148 insertions(+), 42 deletions(-) create mode 100644 projects/npm-tools/packages/npm-scripts/src/utils/webpackExportCssLoader.js diff --git a/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js b/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js index cb9a622d24..6951e36a95 100644 --- a/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js +++ b/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js @@ -32,7 +32,9 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { const {exports, output} = buildConfig; exports.forEach((exportItem) => { - const {pkgName, scope} = splitModuleName(exportItem.name); + let {modulePath, pkgName, scope} = splitModuleName(exportItem.name); + + // Compute src and destination package.json objects const scopedPkgName = joinModuleName(scope, pkgName, ''); @@ -42,15 +44,21 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { basedir: projectDir, } )); - const srcPkgId = `${srcPkgJson.name}@${srcPkgJson.version}`; const pkgJson = { dependencies: {}, - main: './index.js', name: addNamespace(scopedPkgName, rootPkgJson), version: srcPkgJson.version, }; - const pkgId = `${pkgJson.name}@${pkgJson.version}`; + + // Tweak module path and package.json main's field + + if (!modulePath) { + modulePath = '/index'; + pkgJson.main = './index.js'; + } + + // Create output package dir const packageDir = path.join( output, @@ -61,6 +69,16 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { }) ); + fs.mkdirSync(packageDir, {recursive: true}); + + // Create/update output package.json file + + fs.writeFileSync( + path.join(packageDir, 'package.json'), + JSON.stringify(pkgJson, null, '\t'), + 'utf8' + ); + // // Compute the relative position of the bridge related to the real ES // module. @@ -71,10 +89,26 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { // Also, depending for npm-scoped scoped packages, and additional folder // level appears under `/o/js/resolved-module/...`. // + // Also, for internal module paths, we must add more `../`s. + // + + let rootDir = '../..'; + + if (exportItem.name.startsWith('@')) { + rootDir += '/..'; + } + + const hopsToAdd = modulePath.split('/').length - 1; + + for (let i = 0; i < hopsToAdd; i++) { + rootDir += '/..'; + } + + rootDir += webContextPath; - const rootDir = exportItem.name.startsWith('@') - ? `../../../..${webContextPath}` - : `../../..${webContextPath}`; + // Create output bridge file + + const pkgId = `${pkgJson.name}@${pkgJson.version}`; const bridgeSource = ` import * as esModule from "${rootDir}/__liferay__/exports/${flattenPkgName( @@ -82,7 +116,7 @@ import * as esModule from "${rootDir}/__liferay__/exports/${flattenPkgName( )}.js"; Liferay.Loader.define( - "${pkgId}/index", + "${pkgId}${modulePath}", ['module'], function (module) { module.exports = { @@ -94,39 +128,40 @@ Liferay.Loader.define( ); `; - fs.mkdirSync(packageDir, {recursive: true}); - - fs.writeFileSync( - path.join(packageDir, 'package.json'), - JSON.stringify(pkgJson, null, '\t'), - 'utf8' + const outputFilePath = path.join( + packageDir, + `${modulePath.substr(1).replace('/', path.sep)}.js` ); - fs.writeFileSync( - path.join(packageDir, 'index.js'), - bridgeSource, - 'utf8' - ); + fs.mkdirSync(path.dirname(outputFilePath), {recursive: true}); - manifest.packages[pkgId] = manifest.packages[pkgId] || { - dest: { - dir: '.', - id: pkgId, - name: pkgJson.name, - version: pkgJson.version, - }, - modules: { - 'index.js': { - flags: { - esModule: true, - useESM: true, - }, + fs.writeFileSync(outputFilePath, bridgeSource, 'utf8'); + + // Update output manifest.json + + if (!manifest.packages[pkgId]) { + const srcPkgId = `${srcPkgJson.name}@${srcPkgJson.version}`; + + manifest.packages[pkgId] = { + dest: { + dir: '.', + id: pkgId, + name: pkgJson.name, + version: pkgJson.version, }, - }, - src: { - id: srcPkgId, - name: srcPkgJson.name, - version: srcPkgJson.version, + modules: {}, + src: { + id: srcPkgId, + name: srcPkgJson.name, + version: srcPkgJson.version, + }, + }; + } + + manifest.packages[pkgId].modules[`${modulePath.substr(1)}.js`] = { + flags: { + esModule: true, + useESM: true, }, }; }); diff --git a/projects/npm-tools/packages/npm-scripts/src/utils/runWebpackAsBundler.js b/projects/npm-tools/packages/npm-scripts/src/utils/runWebpackAsBundler.js index d32bfb5e27..a039255390 100644 --- a/projects/npm-tools/packages/npm-scripts/src/utils/runWebpackAsBundler.js +++ b/projects/npm-tools/packages/npm-scripts/src/utils/runWebpackAsBundler.js @@ -5,6 +5,7 @@ /* eslint-disable @liferay/no-dynamic-require */ +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const path = require('path'); const resolve = require('resolve'); const TerserPlugin = require('terser-webpack-plugin'); @@ -40,13 +41,16 @@ module.exports = async function runWebpackAsBundler( await runWebpack(indexWebpackConfig, buildConfig.report); } - const webpackConfigs = getImportsWebpackConfigs(buildConfig); + const exportsWebpackConfig = getExportsWebpackConfig( + projectDir, + buildConfig + ); let i = 0; - for (const webpackConfig of webpackConfigs) { + for (const webpackConfig of exportsWebpackConfig) { createTempFile( - `webpackAsBundler.import[${i++}].config.json`, + `webpackAsBundler.export[${i++}].config.json`, JSON.stringify(webpackConfig, null, 2), {autoDelete: false} ); @@ -261,7 +265,7 @@ ${nonDefaultFields} }; } -function getImportsWebpackConfigs(buildConfig) { +function getExportsWebpackConfig(projectDir, buildConfig) { const {exports, imports} = buildConfig; const allExternals = convertImportsToExternals(imports, 3); @@ -277,9 +281,19 @@ function getImportsWebpackConfigs(buildConfig) { delete externals[pkgName]; + // Compute output file name: for the case of .css files, we want webpack + // to create a .js file with the same name as the CSS file and next to + // its output. That file is never used (as webpack leaves it empty), but + // it allows our exports CSS loader to put the valid .js stub in the + // proper place (__liferay__/exports). + + const entryKey = importPath.endsWith('.css') + ? `__liferay__/css/${flatPkgName.replace(/\.css$/, '')}` + : `__liferay__/exports/${flatPkgName}`; + const webpackConfig = { entry: { - [`__liferay__/exports/${flatPkgName}`]: { + [entryKey]: { import: importPath, }, }, @@ -327,6 +341,7 @@ function getImportsWebpackConfigs(buildConfig) { }, path: path.resolve(buildConfig.output), }, + plugins: [new MiniCssExtractPlugin()], resolve: { fallback: { path: false, @@ -334,6 +349,32 @@ function getImportsWebpackConfigs(buildConfig) { }, }; + // For CSS exports, add our loader so that the .js stub is created + + if (importPath.endsWith('.css')) { + webpackConfig.module.rules.push({ + include: /node_modules/, + test: new RegExp(`${importPath.replace('/', '\\/')}$`), + use: [ + { + loader: require.resolve('./webpackExportCssLoader'), + options: { + filename: + entryKey.replace('/css/', '/exports/') + '.css', + projectDir, + url: entryKey + '.css', + }, + }, + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: require.resolve('css-loader'), + }, + ], + }); + } + if (process.env.NODE_ENV === 'development') { webpackConfig.devtool = 'source-map'; webpackConfig.mode = 'development'; diff --git a/projects/npm-tools/packages/npm-scripts/src/utils/webpackExportCssLoader.js b/projects/npm-tools/packages/npm-scripts/src/utils/webpackExportCssLoader.js new file mode 100644 index 0000000000..be7dad7a28 --- /dev/null +++ b/projects/npm-tools/packages/npm-scripts/src/utils/webpackExportCssLoader.js @@ -0,0 +1,30 @@ +/** + * SPDX-FileCopyrightText: © 2019 Liferay, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* eslint-disable @liferay/no-dynamic-require */ + +const getBndWebContextPath = require('./getBndWebContextPath'); + +module.exports = function (content, _map, _meta) { + const {filename, projectDir, url} = this.getOptions(); + + const webContextPath = getBndWebContextPath(projectDir); + + this.emitFile( + filename + '.js', + ` +const link = document.createElement('link'); +link.setAttribute('rel', 'stylesheet'); +link.setAttribute('type', 'text/css'); +link.setAttribute( + 'href', + Liferay.ThemeDisplay.getPathContext() + '/o${webContextPath}/${url}' +); +document.querySelector('head').appendChild(link); +` + ); + + return content; +}; From eb2fce5d1bd41967049974938a119a1682222788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Thu, 26 Jan 2023 12:00:00 +0100 Subject: [PATCH 2/2] style: lint variable declarations --- .../npm-scripts/src/utils/createEsm2AmdExportsBridges.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js b/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js index 6951e36a95..9188727094 100644 --- a/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js +++ b/projects/npm-tools/packages/npm-scripts/src/utils/createEsm2AmdExportsBridges.js @@ -32,7 +32,9 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { const {exports, output} = buildConfig; exports.forEach((exportItem) => { - let {modulePath, pkgName, scope} = splitModuleName(exportItem.name); + const splittedModuleName = splitModuleName(exportItem.name); + const {pkgName, scope} = splittedModuleName; + let {modulePath} = splittedModuleName; // Compute src and destination package.json objects