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 cb9a622d2..918872709 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,11 @@ function createEsm2AmdExportsBridges(projectDir, buildConfig, manifest) { const {exports, output} = buildConfig; exports.forEach((exportItem) => { - const {pkgName, scope} = splitModuleName(exportItem.name); + const splittedModuleName = splitModuleName(exportItem.name); + const {pkgName, scope} = splittedModuleName; + let {modulePath} = splittedModuleName; + + // Compute src and destination package.json objects const scopedPkgName = joinModuleName(scope, pkgName, ''); @@ -42,15 +46,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 +71,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 +91,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 +118,7 @@ import * as esModule from "${rootDir}/__liferay__/exports/${flattenPkgName( )}.js"; Liferay.Loader.define( - "${pkgId}/index", + "${pkgId}${modulePath}", ['module'], function (module) { module.exports = { @@ -94,39 +130,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 d32bfb5e2..a03925539 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 000000000..be7dad7a2 --- /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; +};