• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

herberttn / bytenode-webpack-plugin / 4275320650

pending completion
4275320650

push

github

GitHub
fix(examples): squirrel description is required (#20)

63 of 66 branches covered (95.45%)

Branch coverage included in aggregate %.

293 of 295 relevant lines covered (99.32%)

48.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.4
/src/plugin.ts
1
import { dirname, relative, resolve } from 'path';
15✔
2

3
import replaceString from 'replace-string';
15✔
4
import type { Hook } from 'tapable';
5
import { Compilation, ExternalsPlugin } from 'webpack';
15✔
6
import type { Compiler, WebpackPluginInstance } from 'webpack';
7
import VirtualModulesPlugin from 'webpack-virtual-modules';
15✔
8

9
import { createLoaderCode } from './loaders';
15✔
10
import { compileSource, replaceSource } from './sources';
15✔
11
import type { Options, Prepared, PreparedEntries, PreparedEntry, Source } from './types';
12
import { createFileMatcher, fromTargetToCompiledExtension, isTargetExtension, toLoaderFileName, toSiblingRelativeFileLocation } from './utils';
15✔
13

14
class BytenodeWebpackPlugin implements WebpackPluginInstance {
15

16
  private readonly name = 'BytenodeWebpackPlugin';
35✔
17
  private readonly options: Options;
18

19
  constructor(options: Partial<Options> = {}) {
20✔
20
    this.options = {
35✔
21
      compileAsModule: true,
22
      compileForElectron: false,
23
      debugLifecycle: false,
24
      keepSource: false,
25
      preventSourceMaps: true,
26
      ...options,
27
    };
28
  }
29

30
  apply(compiler: Compiler): void {
31
    const logger = compiler.getInfrastructureLogger(this.name);
34✔
32
    setupLifecycleLogging(compiler, this.name, this.options);
34✔
33

34
    logger.debug('original webpack.options.entry', compiler.options.entry);
34✔
35

36
    const { entries: { ignored, loaders, targets }, modules } = prepare(compiler);
34✔
37
    logger.debug('prepared ignores', Object.fromEntries(ignored.entries()));
31✔
38
    logger.debug('prepared loaders', Object.fromEntries(loaders.entries()));
31✔
39
    logger.debug('prepared targets', Object.fromEntries(targets.entries()));
31✔
40
    logger.debug('prepared modules', Object.fromEntries(modules.entries()));
31✔
41

42
    compiler.options.entry = Object.fromEntries([
31✔
43
      ...ignored.entries(),
44
      ...loaders.entries(),
45
      ...targets.entries(),
46
    ]);
47

48
    if (this.options.preventSourceMaps) {
31✔
49
      logger.log('Preventing source maps from being generated by changing webpack.options.devtool to false.');
31✔
50
      compiler.options.devtool = false;
31✔
51
    }
52

53
    if (this.options.compileForElectron) {
31✔
54
      const target = compiler.options.target;
5✔
55

56
      if (target) {
5✔
57
        const targets = Array.isArray(target) ? target : [target];
5✔
58

59
        if (!targets.some(target => target.startsWith('electron-'))) {
7✔
60
          logger.warn(`Consider using an electron target instead of or in addition to [${targets.join(', ')}] when compiling for electron.`);
3✔
61
        }
62
      }
63
    }
64

65
    logger.debug('modified webpack.options.devtool', compiler.options.devtool);
31✔
66
    logger.debug('modified webpack.options.entry', compiler.options.entry);
31✔
67

68
    logger.debug('adding electron as external');
31✔
69
    new ExternalsPlugin('commonjs', ['electron'])
31✔
70
      .apply(compiler);
71

72
    logger.debug('adding target imports from loader code as external');
31✔
73
    new ExternalsPlugin('commonjs', ({ context, contextInfo, request }, callback) => {
31✔
74
      if (context && contextInfo && request) {
407✔
75
        const requestLocation = resolve(context, request);
407✔
76

77
        if (contextInfo.issuer === toLoaderFileName(requestLocation)) {
407✔
78
          for (const target of Array.from(targets.values()).flatMap(target => target.import)) {
112✔
79
            const targetLocation = resolve(compiler.context, target);
91✔
80

81
            if (target === request || targetLocation === requestLocation) {
91✔
82
              logger.debug('external: context', { context, contextInfo, request, requestLocation, target, targetLocation });
55✔
83
              logger.debug('external: resolved to', target);
55✔
84

85
              return callback(undefined, target);
55✔
86
            }
87
          }
88
        }
89
      }
90

91
      return callback();
352✔
92
    }).apply(compiler);
93

94
    new VirtualModulesPlugin(Object.fromEntries(modules.entries()))
31✔
95
      .apply(compiler);
96

97
    // ensure hooks run last by tapping after the other plugins
98
    compiler.hooks.afterPlugins.tap(this.name, () => {
31✔
99
      logger.debug('hook: after plugins');
31✔
100
      const matches = createFileMatcher(this.options.include, this.options.exclude);
31✔
101

102
      compiler.hooks.compilation.tap(this.name, compilation => {
31✔
103
        logger.debug('hook: compilation');
31✔
104

105
        const stats = compilation.getLogger(this.name);
31✔
106
        const loaderOutputFiles: string[] = [];
31✔
107
        const targetOutputFiles: string[] = [];
31✔
108

109
        compilation.hooks.processAssets.tap({ name: this.name, stage: Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS }, (): void => {
31✔
110
          logger.debug('hook: process assets');
31✔
111
          stats.time('collect asset names');
31✔
112

113
          loaderOutputFiles.push(...collectOutputFiles(compilation, loaders));
31✔
114
          logger.debug('collected: loader output files', loaderOutputFiles);
31✔
115

116
          targetOutputFiles.push(...collectOutputFiles(compilation, targets));
31✔
117
          logger.debug('collected: target output files', targetOutputFiles);
31✔
118

119
          stats.timeEnd('collect asset names');
31✔
120
        });
121

122
        compilation.hooks.processAssets.tapPromise({ name: this.name, stage: Compilation.PROCESS_ASSETS_STAGE_DERIVED }, async (assets): Promise<void> => {
31✔
123
          logger.debug('hook: process assets promise');
31✔
124
          stats.time('process assets');
31✔
125

126
          for (const [name, asset] of Object.entries(assets)) {
31✔
127
            stats.group('asset', name);
100✔
128
            stats.time('took');
100✔
129

130
            if (loaderOutputFiles.includes(name)) {
100✔
131
              await updateLoaderToRequireCompiledAssets(compilation, name, asset);
50✔
132
            } else if (isTargetExtension(name) && matches(name)) {
50✔
133
              await updateTargetWithCompiledCode(compilation, name, asset, this.options);
42✔
134
            }
135

136
            stats.timeEnd('took');
95✔
137
            stats.groupEnd('asset', name);
95✔
138
          }
139

140
          stats.timeEnd('process assets');
26✔
141
        });
142

143
        async function updateLoaderToRequireCompiledAssets(compilation: Compilation, name: string, asset: Source): Promise<void> {
144
          logger.debug('updating loader to require compiled assets', { name });
50✔
145

146
          const source = await replaceSource(asset, raw => {
50✔
147
            logger.debug('initializing external target replacer');
50✔
148
            logger.debug({ outputPath: compiler.outputPath });
50✔
149

150
            for (let index = 0; index < targets.size; index++) {
50✔
151
              const target = Array.from(targets.values())[index];
106✔
152
              const fromLocation = loaderOutputFiles[index];
106✔
153
              const toLocation = targetOutputFiles[index];
106✔
154

155
              logger.debug('replacer', { name, target: { name: Array.from(targets.keys())[index], ...target }, fromLocation, toLocation });
106✔
156

157
              let to = relative(dirname(fromLocation), toLocation);
106✔
158

159
              if (!to.startsWith('.')) {
106✔
160
                to = toSiblingRelativeFileLocation(to);
106✔
161
              }
162

163
              // Use absolute path to load the compiled file in dev mode due to how electron-forge handles
164
              // the renderer process code loading (by using a server and not directly from the file system).
165
              // This should be safe exactly because it will only be used in dev mode, so the app code will
166
              // never be relocated after compiling with webpack and before starting electron.
167
              if (compiler.options.mode === 'development' && compiler.options.target === 'electron-renderer') {
106!
168
                to = resolve(compiler.outputPath, toLocation);
×
169
              }
170

171
              for (const from of target.import) {
106✔
172
                logger.debug('replacing within', name);
112✔
173
                logger.debug('  from:', from);
112✔
174
                logger.debug('    to:', to);
112✔
175

176
                raw = replaceString(raw, from, to);
112✔
177
              }
178
            }
179

180
            logger.debug('initializing compiled target replacer');
50✔
181

182
            for (const file of targetOutputFiles) {
50✔
183
              logger.debug('replacing within', name);
106✔
184
              logger.debug('  from:', file);
106✔
185
              logger.debug('    to:', fromTargetToCompiledExtension(file));
106✔
186

187
              raw = replaceString(raw, file, fromTargetToCompiledExtension(file));
106✔
188
            }
189

190
            return raw;
50✔
191
          });
192

193
          compilation.updateAsset(name, source);
50✔
194
        }
195

196
      });
197
    });
198

199
    async function updateTargetWithCompiledCode(compilation: Compilation, name: string, asset: Source, options: Options): Promise<void> {
200
      logger.debug('compiling asset source', { name });
42✔
201
      const source = await compileSource(asset, options);
42✔
202

203
      logger.debug('updating asset source with the compiled content');
37✔
204
      compilation.updateAsset(name, source);
37✔
205

206
      const to = fromTargetToCompiledExtension(name);
37✔
207

208
      logger.debug(`renaming asset to ${to}`);
37✔
209
      compilation.renameAsset(name, to);
37✔
210

211
      if (options.keepSource) {
37✔
212
        logger.debug('re-emitting decompiled asset due to plugin.options.keepSource being true');
1✔
213
        compilation.emitAsset(name, asset);
1✔
214
      } else {
215
        logger.debug('NOT re-emitting decompiled asset due to plugin.options.keepSource being false');
36✔
216
      }
217
    }
218
  }
219
}
220

221
function collectOutputFiles(compilation: Compilation, from: PreparedEntry): string[] {
222
  const files = [];
62✔
223

224
  for (const name of from.keys()) {
62✔
225
    const entrypoint = compilation.entrypoints.get(name);
100✔
226

227
    if (entrypoint) {
100✔
228
      files.push(...entrypoint.chunks.flatMap(chunk => Array.from(chunk.files.values())));
100✔
229
    }
230
  }
231

232
  return files;
62✔
233
}
234

235
function prepare(compiler: Compiler): Prepared {
236
  const { entry, output } = compiler.options;
34✔
237

238
  if (typeof entry === 'function') {
34✔
239
    throw new Error('webpack.options.entry cannot be a function, use strings or objects');
1✔
240
  }
241

242
  if (typeof output.filename === 'string' && !/.*[[\]]+.*/.test(output.filename)) {
33✔
243
    throw new Error('webpack.options.output.filename cannot be static, use a dynamic one like [name].js');
1✔
244
  }
245

246
  const entries: PreparedEntries = {
32✔
247
    ignored: new Map(),
248
    loaders: new Map(),
249
    targets: new Map(),
250
  };
251

252
  const modules = new Map();
32✔
253

254
  for (const [name, descriptor] of Object.entries(entry)) {
32✔
255
    if (descriptor.filename) {
51✔
256
      throw new Error('webpack.options.entry.filename is not supported, use webpack.options.output.filename');
1✔
257
    }
258

259
    const imports = descriptor.import as string[];
50✔
260

261
    // adds a new entry with a .compiled suffix, pointing to the original imports, which will be compiled
262
    entries.targets.set(name + '.compiled', { ...descriptor, import: imports });
50✔
263

264
    // changes the original entry to use loader files, which will load the decompiler and the new compiled entries
265
    entries.loaders.set(name, { import: imports.map(file => toLoaderFileName(file)) });
55✔
266

267
    // generates virtual modules with the code of the loader files
268
    for (const file of imports) {
50✔
269
      const code = createLoaderCode({ imports: [toSiblingRelativeFileLocation(file)] });
55✔
270
      const location = toLoaderFileName(file);
55✔
271

272
      modules.set(location, code);
55✔
273
    }
274
  }
275

276
  return {
31✔
277
    entries,
278
    modules,
279
  };
280
}
281

282
function setupLifecycleLogging(compiler: Compiler, name: string, options: Options): void {
283
  if (!options.debugLifecycle) {
34✔
284
    return;
33✔
285
  }
286

287
  const logger = compiler.getInfrastructureLogger(`${name}/lifecycle`);
1✔
288
  setupHooksLogging(name, 'compiler', compiler.hooks as unknown as Record<string, Hook<any, any>>);
1✔
289

290
  compiler.hooks.compilation.tap(name, compilation => {
1✔
291
    setupHooksLogging(name, 'compilation', compilation.hooks as unknown as Record<string, Hook<any, any>>);
1✔
292
  });
293

294
  compiler.hooks.normalModuleFactory.tap(name, normalModuleFactory => {
1✔
295
    setupHooksLogging(name, 'normalModuleFactory', normalModuleFactory.hooks as unknown as Record<string, Hook<any, any>>);
1✔
296
  });
297

298
  function setupHooksLogging(pluginName: string, type: string, hooks: Record<string, Hook<any, any>>): void {
299
    const deprecatedHooks = [
3✔
300
      'additionalChunkAssets',
301
      'afterOptimizeChunkAssets',
302
      'normalModuleLoader',
303
      'optimizeChunkAssets',
304
    ];
305

306
    // avoid maximum call stack size exceeded
307
    const recursiveHooks = ['infrastructureLog', 'log'];
3✔
308

309
    for (const [name, hook] of Object.entries(hooks)) {
3✔
310
      try {
103✔
311
        if (deprecatedHooks.includes(name) || recursiveHooks.includes(name)) {
103✔
312
          return;
2✔
313
        }
314

315
        hook.tap(pluginName, () => {
101✔
316
          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
317
          logger.debug(`${type} hook ${name} (${arguments.length} arguments)`);
150✔
318
        });
319
      } catch (_) {
320
        // ignore when unable to tap
321
      }
322
    }
323
  }
324
}
325

326
export {
327
  BytenodeWebpackPlugin,
15✔
328
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc