1 | /*
|
2 | * Copyright 2017-present Sonatype, Inc.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 |
|
17 | const fs = require('fs-extra');
|
18 | const path = require('path');
|
19 |
|
20 | /**
|
21 | * A Webpack plugin that copies the raw source of all imported modules to a separate directory, enabling
|
22 | * external analysis of _just_ the files that get included in the bundles. Within the destination folder,
|
23 | * modules are laid out in a subdirectory structure matching the original files' relative location to the
|
24 | * directory in which webpack runs. Note that any source files whose relative path is outside of the current directory
|
25 | * will have the `..` parts of its path replaced with `__..__` when copied into the destination tree.
|
26 | */
|
27 | /* global Promise */
|
28 | module.exports = class WebpackCopyModulesPlugin {
|
29 | constructor(options) {
|
30 | // this.destination is the absolute path to the destination folder
|
31 | this.destination = path.resolve(process.cwd(), options.destination);
|
32 | this.includePackageJsons = !!options.includePackageJsons;
|
33 | }
|
34 |
|
35 | apply(compiler) {
|
36 | // NOTE: this is only available in webpack 5+, but that's the only version we want to check for anyway
|
37 | const webpackVersion = compiler.webpack && compiler.webpack.version;
|
38 |
|
39 | compiler.hooks.emit.tapPromise('WebpackCopyModulesPlugin', this.handleEmit.bind(this, webpackVersion));
|
40 | }
|
41 |
|
42 | async handleEmit(webpackVersion, compilation) {
|
43 | const me = this,
|
44 | fileDependencies = new Set(),
|
45 | isWebpack5Plus = !!webpackVersion && webpackVersion.match(/^(\d+)\./)[1] >= 5;
|
46 |
|
47 | // add all fileDependencies that are actual files (parent directories are included in
|
48 | // compilation.fileDependencies)
|
49 | for (const fileDep of compilation.fileDependencies) {
|
50 | const exists = await fs.pathExists(fileDep),
|
51 | isFile = exists && (await fs.lstat(fileDep)).isFile();
|
52 |
|
53 | if (isFile) {
|
54 | fileDependencies.add(fileDep);
|
55 | }
|
56 | }
|
57 |
|
58 | // Webpack 5 already includes the package.json files, so no need for this step there
|
59 | const packageJsons = !isWebpack5Plus && this.includePackageJsons ?
|
60 | this.findPackageJsonPaths(fileDependencies) : [];
|
61 |
|
62 | return Promise.all([...fileDependencies, ...packageJsons].map(function(file) {
|
63 | const relativePath = replaceParentDirReferences(path.relative(process.cwd(), file)),
|
64 | destPath = path.join(me.destination, relativePath),
|
65 | destDir = path.dirname(destPath);
|
66 |
|
67 | return fs.pathExists(file)
|
68 | .then(exists => exists ?
|
69 | fs.mkdirs(destDir).then(() => fs.copy(file, destPath, { overwrite: false })) :
|
70 | Promise.resolve()
|
71 | );
|
72 | }));
|
73 | }
|
74 |
|
75 | findPackageJsonPaths(filePaths) {
|
76 | const packageJsons = new Set(),
|
77 |
|
78 | // dirs for which a package.json search has already been conducted.
|
79 | // If the package.json search algo ends up in one of these dirs it knows it can stop searching
|
80 | dirsAlreadySearchedForPackageJson = new Set();
|
81 |
|
82 | // find associated package.json files for each fileDependency
|
83 | filePaths.forEach(function(file) {
|
84 | let dirPath = path.dirname(file),
|
85 | oldDirPath;
|
86 |
|
87 | // until we reach the root
|
88 | while (dirPath !== oldDirPath) {
|
89 | if (dirsAlreadySearchedForPackageJson.has(dirPath)) {
|
90 | return;
|
91 | }
|
92 | else {
|
93 | dirsAlreadySearchedForPackageJson.add(dirPath);
|
94 |
|
95 | const packageJsonPath = path.join(dirPath, 'package.json');
|
96 |
|
97 | if (fs.pathExistsSync(packageJsonPath)) {
|
98 | packageJsons.add(packageJsonPath);
|
99 | return;
|
100 | }
|
101 | else {
|
102 | // loop again to check next parent dir
|
103 | oldDirPath = dirPath;
|
104 | dirPath = path.dirname(dirPath);
|
105 | }
|
106 | }
|
107 | }
|
108 | });
|
109 |
|
110 | return packageJsons;
|
111 | }
|
112 | };
|
113 |
|
114 | /**
|
115 | * Go through the path and replace all `..` parts with `__..__`
|
116 | */
|
117 | function replaceParentDirReferences(inputPath) {
|
118 | const pathParts = inputPath.split(path.sep);
|
119 |
|
120 | return pathParts.map(part => part === '..' ? '__..__' : part).join(path.sep);
|
121 | }
|