@adobe/helix-deploy
Version:
Library and Commandline Tools to build and deploy OpenWhisk Actions
242 lines (224 loc) • 7.23 kB
JavaScript
/*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { fileURLToPath } from 'url';
import path from 'path';
import fse from 'fs-extra';
import webpack from 'webpack';
import chalk from 'chalk-template';
import BaseBundler from './BaseBundler.js';
// eslint-disable-next-line no-underscore-dangle
const __dirname = path.resolve(fileURLToPath(import.meta.url), '..');
/**
* Webpack based bundler
*/
export default class WebpackBundler extends BaseBundler {
constructor(cfg) {
super(cfg);
this.arch = 'node';
this.type = 'webpack';
}
async init() {
if (this.cfg.esm) {
throw new Error('Webpack bundler does not support ESM builds.');
}
}
async getWebpackConfig() {
const { cfg } = this;
const opts = {
target: 'node',
mode: 'production',
// the universal adapter is the entry point
entry: cfg.adapterFile || path.resolve(__dirname, '..', 'template', 'node-index.js'),
context: cfg.cwd,
output: {
path: cfg.cwd,
filename: path.relative(cfg.cwd, cfg.bundle),
library: 'main',
libraryTarget: 'umd',
globalObject: 'globalThis',
asyncChunks: false,
},
devtool: false,
externals: [
/^@aws-sdk\/.*$/,
[
...cfg.externals,
...cfg.serverlessExternals,
// the following are imported by the universal adapter and are assumed to be available
'./params.json',
'aws-sdk',
'@google-cloud/secret-manager',
'@google-cloud/storage',
].reduce((obj, ext) => {
// this makes webpack to ignore the module and just leave it as normal require.
// eslint-disable-next-line no-param-reassign
obj[ext] = `commonjs2 ${ext}`;
return obj;
}, {}),
],
module: {
rules: [{
test: /\.js$/,
type: 'javascript/auto',
}, {
test: /\.mjs$/,
type: 'javascript/esm',
}],
},
resolve: {
mainFields: ['main', 'module'],
extensions: ['.wasm', '.js', '.mjs', '.json'],
alias: {
// the main.js is imported in the universal adapter and is _the_ action entry point
'./main.js': cfg.file,
},
},
optimization: {
// we enable production mode in order to get the correct imports (eg micromark has special
// export condition for 'development'). but we disable minimize and keep named modules
// in order to easier match log errors to the bundle
minimize: false,
concatenateModules: false,
mangleExports: false,
moduleIds: 'named',
},
node: {
__dirname: true,
__filename: false,
},
plugins: [],
};
if (cfg.minify) {
opts.optimization = {
minimize: cfg.minify,
};
}
if (cfg.modulePaths && cfg.modulePaths.length > 0) {
opts.resolve.modules = cfg.modulePaths;
}
if (cfg.progressHandler) {
this.initProgressHandler(opts, cfg);
}
const customizePath = path.join(cfg.cwd, 'hlx.webpack.customize.js');
if (await fse.pathExists(customizePath)) {
cfg.log.info(`--: Using custom webpack config from ${customizePath}`);
const customize = await import(customizePath);
if (customize.extraPlugins && typeof customize.extraPlugins === 'function') {
opts.plugins.push(...customize.extraPlugins(cfg, opts));
}
}
return opts;
}
// eslint-disable-next-line class-methods-use-this
initProgressHandler(opts, cfg) {
opts.plugins.push(new webpack.ProgressPlugin(cfg.progressHandler));
}
async createWebpackBundle(arch) {
const { cfg } = this;
if (!cfg.depFile) {
throw Error('dependencies info path is undefined');
}
const m = cfg.minify ? 'minified ' : '';
if (!cfg.progressHandler) {
cfg.log.info(`--: creating ${arch} ${m}bundle using webpack ...`);
}
const config = await this.getWebpackConfig();
const compiler = webpack(config);
const stats = await new Promise((resolve, reject) => {
compiler.run((err, s) => {
if (err) {
reject(err);
} else {
resolve(s);
}
});
});
cfg.log.debug(stats.toString({
chunks: false,
colors: true,
}));
await this.resolveDependencyInfos(stats);
// write dependencies info file
await fse.writeJson(cfg.depFile, cfg.dependencies, { spaces: 2 });
if (!cfg.progressHandler) {
cfg.log.info(chalk`{green ok:} created ${arch} bundle {yellow ${config.output.filename}}`);
}
return stats;
}
async createBundle() {
if (!this.cfg.bundle) {
throw Error('bundle path is undefined');
}
return this.createWebpackBundle('node');
}
/**
* Resolves the dependencies by chunk. eg:
*
* {
* 'src/idx_json.bundle.js': [{
* id: '@adobe/helix-epsagon:1.2.0',
* name: '@adobe/helix-epsagon',
* version: '1.2.0' },
* ],
* ...
* }
*/
async resolveDependencyInfos(stats) {
const { cfg } = this;
// get list of dependencies
const depsByFile = {};
const resolved = {};
const jsonStats = stats.toJson({
chunks: true,
chunkModules: true,
});
await Promise.all(jsonStats.chunks
.map(async (chunk) => {
const chunkName = chunk.names[0];
const deps = {};
depsByFile[chunkName] = deps;
await Promise.all(chunk.modules.map(async (mod) => {
const segs = mod.identifier.split('/');
let idx = segs.lastIndexOf('node_modules');
if (idx >= 0) {
idx += 1;
if (segs[idx].charAt(0) === '@') {
idx += 1;
}
segs.splice(idx + 1);
const dir = path.resolve('/', ...segs);
try {
if (!resolved[dir]) {
const pkgJson = await fse.readJson(path.resolve(dir, 'package.json'));
const id = `${pkgJson.name}:${pkgJson.version}`;
resolved[dir] = {
id,
name: pkgJson.name,
version: pkgJson.version,
};
}
const dep = resolved[dir];
deps[dep.id] = dep;
} catch (e) {
// ignore
}
}
}));
}));
// sort the deps
Object.entries(depsByFile)
.forEach(([scriptFile, deps]) => {
cfg.dependencies[scriptFile] = Object.values(deps)
.sort((d0, d1) => d0.name.localeCompare(d1.name));
});
}
}