UNPKG

@coderline/alphatab-webpack

Version:

A plugin for WebPack to bundle alphaTab into your webapps.

714 lines (703 loc) 33.6 kB
/*! * alphaTab WebPack Plugin v1.7.0-alpha.1536 (develop, build 1536) * * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This library uses code from WebPack (https://github.com/webpack/webpack/), licensed under: * * Copyright JS Foundation and other contributors * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * 'Software'), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * @preserve * @license */ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as url from 'node:url'; const JAVASCRIPT_MODULE_TYPE_AUTO = 'javascript/auto'; const JAVASCRIPT_MODULE_TYPE_ESM = 'javascript/esm'; function makeDependencySerializable(webPackWithAlphaTab, dependency, key) { webPackWithAlphaTab.webpack.util.serialization.register(dependency, key, null, { serialize(obj, context) { obj.serialize(context); }, deserialize(context) { if (typeof dependency.deserialize === 'function') { return dependency.deserialize(context); } const obj = new dependency(); obj.deserialize(context); return obj; } }); } function tapJavaScript(normalModuleFactory, pluginName, parserPlugin) { normalModuleFactory.hooks.parser.for(JAVASCRIPT_MODULE_TYPE_AUTO).tap(pluginName, parserPlugin); normalModuleFactory.hooks.parser.for(JAVASCRIPT_MODULE_TYPE_ESM).tap(pluginName, parserPlugin); } function parseModuleUrl(parser, expr) { if (expr.type !== 'NewExpression' || expr.arguments.length !== 2) { return; } const newExpr = expr; const [arg1, arg2] = newExpr.arguments; const callee = parser.evaluateExpression(newExpr.callee); if (!callee.isIdentifier() || !callee.identifier.includes('alphaTabUrl')) { return; } const arg1Value = parser.evaluateExpression(arg1); return [arg1Value, [arg1.range[0], arg2.range[1]]]; } const ALPHATAB_WORKER_RUNTIME_PREFIX = 'atworker_'; function getWorkerRuntime(parser, compilation, cachedContextify, workerIndexMap) { const i = workerIndexMap.get(parser.state) || 0; workerIndexMap.set(parser.state, i + 1); const name = `${cachedContextify(parser.state.module.identifier())}|${i}`; const hash = compilation.compiler.webpack.util.createHash(compilation.outputOptions.hashFunction); hash.update(name); const digest = hash.digest(compilation.outputOptions.hashDigest); const runtime = digest.slice(0, compilation.outputOptions.hashDigestLength); return ALPHATAB_WORKER_RUNTIME_PREFIX + runtime; } function isWorkerRuntime(runtime) { if (typeof runtime !== 'string') { return false; } return runtime.startsWith(ALPHATAB_WORKER_RUNTIME_PREFIX); } function injectWorkerRuntimeModule(webPackWithAlphaTab) { class AlphaTabWorkerRuntimeModule extends webPackWithAlphaTab.webpack.RuntimeModule { constructor() { super('alphaTab audio worker chunk loading', webPackWithAlphaTab.webpack.RuntimeModule.STAGE_BASIC); } generate() { const compilation = this.compilation; const runtimeTemplate = compilation.runtimeTemplate; const globalObject = runtimeTemplate.globalObject; const chunkLoadingGlobalExpr = `${globalObject}[${JSON.stringify(compilation.outputOptions.chunkLoadingGlobal)}]`; const initialChunkIds = new Set(this.chunk.ids); for (const c of this.chunk.getAllInitialChunks()) { if (webPackWithAlphaTab.webpack.javascript.JavascriptModulesPlugin.chunkHasJs(c, this.chunkGraph)) { continue; } for (const id of c.ids) { initialChunkIds.add(id); } } return webPackWithAlphaTab.webpack.Template.asString([ `if ( ! ('AudioWorkletGlobalScope' in ${globalObject}) ) { return; }`, 'const installedChunks = {', webPackWithAlphaTab.webpack.Template.indent(Array.from(initialChunkIds, id => `${JSON.stringify(id)}: 1`).join(',\n')), '};', '// importScripts chunk loading', `const installChunk = ${runtimeTemplate.basicFunction('data', [ runtimeTemplate.destructureArray(['chunkIds', 'moreModules', 'runtime'], 'data'), 'for(const moduleId in moreModules) {', webPackWithAlphaTab.webpack.Template.indent([ `if(${webPackWithAlphaTab.webpack.RuntimeGlobals.hasOwnProperty}(moreModules, moduleId)) {`, webPackWithAlphaTab.webpack.Template.indent(`${webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories}[moduleId] = moreModules[moduleId];`), '}' ]), '}', `if(runtime) runtime(${webPackWithAlphaTab.webpack.RuntimeGlobals.require});`, 'while(chunkIds.length)', webPackWithAlphaTab.webpack.Template.indent('installedChunks[chunkIds.pop()] = 1;'), 'parentChunkLoadingFunction(data);' ])};`, `const chunkLoadingGlobal = ${chunkLoadingGlobalExpr} = ${chunkLoadingGlobalExpr} || [];`, 'const parentChunkLoadingFunction = chunkLoadingGlobal.push.bind(chunkLoadingGlobal);', 'chunkLoadingGlobal.forEach(installChunk);', 'chunkLoadingGlobal.push = installChunk;' ]); } } AlphaTabWorkerRuntimeModule.Key = 'AlphaTabWorkerRuntime'; webPackWithAlphaTab.alphaTab.registerWebWorkerRuntimeModule = (pluginName, compilation) => { compilation.hooks.runtimeRequirementInTree .for(AlphaTabWorkerRuntimeModule.Key) .tap(pluginName, (chunk) => { compilation.addRuntimeModule(chunk, new AlphaTabWorkerRuntimeModule()); }); compilation.hooks.additionalChunkRuntimeRequirements.tap(pluginName, (chunk, runtimeRequirements) => { if (isWorkerRuntime(chunk.runtime)) { runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories); runtimeRequirements.add(webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey); } }); }; webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey = AlphaTabWorkerRuntimeModule.Key; } const AlphaTabWorkletSpecifierTag = Symbol('alphatab worklet specifier tag'); const workletIndexMap = new WeakMap(); /** * Configures the Audio Worklet aspects within webpack. * The counterpart which this plugin detects sits in alphaTab.main.ts * @param pluginName * @param options * @param compiler * @param compilation * @param normalModuleFactory * @param cachedContextify * @returns */ function configureAudioWorklet(webPackWithAlphaTab, pluginName, options, compiler, compilation, normalModuleFactory, cachedContextify) { if (options.audioWorklets === false) { return; } webPackWithAlphaTab.alphaTab.registerWorkletDependency(compilation, normalModuleFactory); const handleAlphaTabWorklet = (parser, expr) => { const [arg1] = expr.arguments; const parsedUrl = parseModuleUrl(parser, arg1); if (!parsedUrl) { return; } const [url] = parsedUrl; if (!url.isString()) { return; } const runtime = getWorkerRuntime(parser, compilation, cachedContextify, workletIndexMap); const block = new webPackWithAlphaTab.webpack.AsyncDependenciesBlock({ entryOptions: { chunkLoading: false, wasmLoading: false, runtime: runtime, library: { // prevent any built-in/default library settings // to be active for this chunk type: 'at-worklet' } } }); block.loc = expr.loc; const workletBootstrap = webPackWithAlphaTab.alphaTab.createWorkletDependency(url.string, [expr.range[0], expr.range[1]], compiler.options.output.workerPublicPath); workletBootstrap.loc = expr.loc; block.addDependency(workletBootstrap); parser.state.module.addBlock(block); return true; }; const parserPlugin = (parser) => { const pattern = 'alphaTabWorklet'; const itemMembers = 'addModule'; parser.hooks.preDeclarator.tap(pluginName, (decl) => { if (decl.id.type === 'Identifier' && decl.id.name === pattern) { parser.tagVariable(decl.id.name, AlphaTabWorkletSpecifierTag); return true; } return; }); parser.hooks.pattern.for(pattern).tap(pluginName, (pattern) => { parser.tagVariable(pattern.name, AlphaTabWorkletSpecifierTag); return true; }); parser.hooks.callMemberChain .for(AlphaTabWorkletSpecifierTag) .tap(pluginName, (expression, members) => { if (itemMembers !== members.join('.')) { return; } return handleAlphaTabWorklet(parser, expression); }); }; tapJavaScript(normalModuleFactory, pluginName, parserPlugin); } const workerIndexMap = new WeakMap(); /** * Configures the WebWorker aspects within webpack. * The counterpart which this plugin detects sits in alphaTab.main.ts * @param pluginName * @param options * @param compiler * @param compilation * @param normalModuleFactory * @param cachedContextify * @returns */ function configureWebWorker(webPackWithAlphaTab, pluginName, options, compiler, compilation, normalModuleFactory, cachedContextify) { if (options.audioWorklets === false) { return; } webPackWithAlphaTab.alphaTab.registerWebWorkerDependency(compilation, normalModuleFactory); new webPackWithAlphaTab.webpack.javascript.EnableChunkLoadingPlugin('import-scripts').apply(compiler); const handleAlphaTabWorker = (parser, expr) => { const [arg1, arg2] = expr.arguments; const parsedUrl = parseModuleUrl(parser, arg1); if (!parsedUrl) { return; } const [url, range] = parsedUrl; if (!url.isString()) { return; } const runtime = getWorkerRuntime(parser, compilation, cachedContextify, workerIndexMap); const block = new webPackWithAlphaTab.webpack.AsyncDependenciesBlock({ entryOptions: { chunkLoading: 'import-scripts', wasmLoading: false, runtime: runtime, library: { // prevent any built-in/default library settings // to be active for this chunk type: 'at-worker' } } }); block.loc = expr.loc; const workletBootstrap = webPackWithAlphaTab.alphaTab.createWebWorkerDependency(url.string, range, compiler.options.output.workerPublicPath); workletBootstrap.loc = expr.loc; block.addDependency(workletBootstrap); parser.state.module.addBlock(block); const dep1 = new webPackWithAlphaTab.webpack.dependencies.ConstDependency(`{ type: ${compilation.options.output.module ? '"module"' : 'undefined'} }`, arg2.range); dep1.loc = expr.loc; parser.state.module.addPresentationalDependency(dep1); parser.walkExpression(expr.callee); parser.walkExpression(arg1.callee); return true; }; const parserPlugin = (parser) => { parser.hooks.new .for('alphaTab.Environment.alphaTabWorker') .tap(pluginName, (expr) => handleAlphaTabWorker(parser, expr)); }; tapJavaScript(normalModuleFactory, pluginName, parserPlugin); } function injectWebWorkerDependency(webPackWithAlphaTab) { class AlphaTabWebWorkerDependency extends webPackWithAlphaTab.webpack.dependencies.ModuleDependency { constructor(request, range, publicPath) { super(request); this.range = range; this.publicPath = publicPath; } getReferencedExports() { return webPackWithAlphaTab.webpack.Dependency.NO_EXPORTS_REFERENCED; } get type() { return 'alphaTabWorker'; } get category() { return 'worker'; } updateHash(hash) { if (this._hashUpdate === undefined) { this._hashUpdate = JSON.stringify(this.publicPath); } hash.update(this._hashUpdate); } serialize(context) { const { write } = context; write(this.publicPath); super.serialize(context); } deserialize(context) { const { read } = context; this.publicPath = read(); super.deserialize(context); } } AlphaTabWebWorkerDependency.Template = class WorkerDependencyTemplate extends (webPackWithAlphaTab.webpack.dependencies.ModuleDependency.Template) { apply(dependency, source, templateContext) { const { chunkGraph, moduleGraph, runtimeRequirements } = templateContext; const dep = dependency; const block = moduleGraph.getParentBlock(dependency); const entrypoint = chunkGraph.getBlockChunkGroup(block); const chunk = entrypoint.getEntrypointChunk(); // We use the workerPublicPath option if provided, else we fallback to the RuntimeGlobal publicPath const workerImportBaseUrl = dep.publicPath ? `"${dep.publicPath}"` : webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath; runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath); runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI); runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.getChunkScriptFilename); source.replace(dep.range[0], dep.range[1] - 1, `/* worker import */ ${workerImportBaseUrl} + ${webPackWithAlphaTab.webpack.RuntimeGlobals.getChunkScriptFilename}(${JSON.stringify(chunk.id)}), ${webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI}`); } }; makeDependencySerializable(webPackWithAlphaTab, AlphaTabWebWorkerDependency, 'AlphaTabWebWorkerDependency'); webPackWithAlphaTab.alphaTab.createWebWorkerDependency = (request, range, publicPath) => new AlphaTabWebWorkerDependency(request, range, publicPath); webPackWithAlphaTab.alphaTab.registerWebWorkerDependency = (compilation, normalModuleFactory) => { compilation.dependencyFactories.set(AlphaTabWebWorkerDependency, normalModuleFactory); compilation.dependencyTemplates.set(AlphaTabWebWorkerDependency, new AlphaTabWebWorkerDependency.Template()); }; } function injectWorkletRuntimeModule(webPackWithAlphaTab) { class AlphaTabWorkletStartRuntimeModule extends webPackWithAlphaTab.webpack.RuntimeModule { constructor() { super('alphaTab audio worklet chunk lookup', webPackWithAlphaTab.webpack.RuntimeModule.STAGE_BASIC); } generate() { const compilation = this.compilation; const workletChunkLookup = new Map(); const allChunks = compilation.chunks; for (const chunk of allChunks) { const isWorkletEntry = isWorkerRuntime(chunk.runtime); if (isWorkletEntry) { const workletChunks = Array.from(chunk.getAllReferencedChunks()).map(c => { // force content chunk to be created compilation.hooks.contentHash.call(c); return compilation.getPath(webPackWithAlphaTab.webpack.javascript.JavascriptModulesPlugin.getChunkFilenameTemplate(c, compilation.outputOptions), { chunk: c, contentHashType: 'javascript' }); }); workletChunkLookup.set(String(chunk.id), workletChunks); } } return webPackWithAlphaTab.webpack.Template.asString([ `${AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks} = (() => {`, webPackWithAlphaTab.webpack.Template.indent([ 'const lookup = new Map(', webPackWithAlphaTab.webpack.Template.indent(JSON.stringify(Array.from(workletChunkLookup.entries()))), ');', 'return (chunkId) => lookup.get(String(chunkId)) ?? [];' ]), '})();' ]); } } AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks = '__webpack_require__.wsc'; webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks = AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks; webPackWithAlphaTab.alphaTab.registerWorkletRuntimeModule = (pluginName, compilation) => { compilation.hooks.runtimeRequirementInTree .for(AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks) .tap(pluginName, (chunk) => { compilation.addRuntimeModule(chunk, new AlphaTabWorkletStartRuntimeModule()); }); }; } function injectWorkletDependency(webPackWithAlphaTab) { /** * This module dependency injects the relevant code into a worklet bootstrap script * to install chunks which have been added to the worklet via addModule before the bootstrap script starts. */ class AlphaTabWorkletDependency extends webPackWithAlphaTab.webpack.dependencies.ModuleDependency { constructor(url, range, publicPath) { super(url); this.range = range; this.publicPath = publicPath; } get type() { return 'alphaTabWorklet'; } get category() { return 'worker'; } updateHash(hash) { if (this._hashUpdate === undefined) { this._hashUpdate = JSON.stringify(this.publicPath); } hash.update(this._hashUpdate); } serialize(context) { const { write } = context; write(this.publicPath); super.serialize(context); } deserialize(context) { const { read } = context; this.publicPath = read(); super.deserialize(context); } } AlphaTabWorkletDependency.Template = class AlphaTabWorkletDependencyTemplate extends (webPackWithAlphaTab.webpack.dependencies.ModuleDependency.Template) { apply(dependency, source, templateContext) { const { chunkGraph, moduleGraph, runtimeRequirements } = templateContext; const dep = dependency; const block = moduleGraph.getParentBlock(dependency); const entrypoint = chunkGraph.getBlockChunkGroup(block); const workletImportBaseUrl = dep.publicPath ? JSON.stringify(dep.publicPath) : webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath; const chunk = entrypoint.getEntrypointChunk(); // worklet global scope has no 'self', need to inject it for compatibility with chunks // some plugins like the auto public path need to right location. we pass this on from the main runtime // some plugins rely on importScripts to be defined. const workletInlineBootstrap = ` globalThis.self = globalThis.self || globalThis; globalThis.location = \${JSON.stringify(${webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI})}; globalThis.importScripts = (url) => { throw new Error("importScripts not available, dynamic loading of chunks not supported in this context", url) }; `; chunkGraph.addChunkRuntimeRequirements(chunk, new Set([ webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories, webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey ])); runtimeRequirements.add(webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks); source.replace(dep.range[0], dep.range[1] - 1, webPackWithAlphaTab.webpack.Template.asString([ '(/* worklet bootstrap */ async function(__webpack_worklet__) {', webPackWithAlphaTab.webpack.Template.indent([ `await __webpack_worklet__.addModule(URL.createObjectURL(new Blob([\`${workletInlineBootstrap}\`], { type: "application/javascript; charset=utf-8" })));`, `for (const fileName of ${webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks}(${JSON.stringify(chunk.id)})) {`, webPackWithAlphaTab.webpack.Template.indent([ `await __webpack_worklet__.addModule(new URL(${workletImportBaseUrl} + fileName, ${webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI}));` ]), '}' ]), '})(alphaTabWorklet)' ])); } }; makeDependencySerializable(webPackWithAlphaTab, AlphaTabWorkletDependency, 'AlphaTabWorkletDependency'); webPackWithAlphaTab.alphaTab.registerWorkletDependency = (compilation, normalModuleFactory) => { compilation.dependencyFactories.set(AlphaTabWorkletDependency, normalModuleFactory); compilation.dependencyTemplates.set(AlphaTabWorkletDependency, new AlphaTabWorkletDependency.Template()); }; webPackWithAlphaTab.alphaTab.createWorkletDependency = (request, range, publicPath) => new AlphaTabWorkletDependency(request, range, publicPath); } /**@target web */ const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/; const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g; const relativePathToRequest = (relativePath) => { if (relativePath === '') { return './.'; } if (relativePath === '..') { return '../.'; } if (relativePath.startsWith('../')) { return relativePath; } return `./${relativePath}`; }; const absoluteToRequest = (context, maybeAbsolutePath) => { if (maybeAbsolutePath[0] === '/') { if (maybeAbsolutePath.length > 1 && maybeAbsolutePath[maybeAbsolutePath.length - 1] === '/') { // this 'path' is actually a regexp generated by dynamic requires. // Don't treat it as an absolute path. return maybeAbsolutePath; } const querySplitPos = maybeAbsolutePath.indexOf('?'); let resource = querySplitPos === -1 ? maybeAbsolutePath : maybeAbsolutePath.slice(0, querySplitPos); resource = relativePathToRequest(path.posix.relative(context, resource)); return querySplitPos === -1 ? resource : resource + maybeAbsolutePath.slice(querySplitPos); } if (WINDOWS_ABS_PATH_REGEXP.test(maybeAbsolutePath)) { const querySplitPos = maybeAbsolutePath.indexOf('?'); let resource = querySplitPos === -1 ? maybeAbsolutePath : maybeAbsolutePath.slice(0, querySplitPos); resource = path.win32.relative(context, resource); if (!WINDOWS_ABS_PATH_REGEXP.test(resource)) { resource = relativePathToRequest(resource.replace(WINDOWS_PATH_SEPARATOR_REGEXP, '/')); } return querySplitPos === -1 ? resource : resource + maybeAbsolutePath.slice(querySplitPos); } // not an absolute path return maybeAbsolutePath; }; const _contextify = (context, request) => { return request .split('!') .map(r => absoluteToRequest(context, r)) .join('!'); }; const makeCacheableWithContext = (fn) => { const cache = new WeakMap(); const cachedFn = (context, identifier, associatedObjectForCache) => { if (!associatedObjectForCache) { return fn(context, identifier); } let innerCache = cache.get(associatedObjectForCache); if (innerCache === undefined) { innerCache = new Map(); cache.set(associatedObjectForCache, innerCache); } let cachedResult; let innerSubCache = innerCache.get(context); if (innerSubCache === undefined) { innerSubCache = new Map(); innerCache.set(context, innerSubCache); } else { cachedResult = innerSubCache.get(identifier); } if (cachedResult !== undefined) { return cachedResult; } const result = fn(context, identifier); innerSubCache.set(identifier, result); return result; }; cachedFn.bindContextCache = (context, associatedObjectForCache) => { let innerSubCache; if (associatedObjectForCache) { let innerCache = cache.get(associatedObjectForCache); if (innerCache === undefined) { innerCache = new Map(); cache.set(associatedObjectForCache, innerCache); } innerSubCache = innerCache.get(context); if (innerSubCache === undefined) { innerSubCache = new Map(); innerCache.set(context, innerSubCache); } } else { innerSubCache = new Map(); } const boundFn = (identifier) => { const cachedResult = innerSubCache.get(identifier); if (cachedResult !== undefined) { return cachedResult; } const result = fn(context, identifier); innerSubCache.set(identifier, result); return result; }; return boundFn; }; return cachedFn; }; const contextify = makeCacheableWithContext(_contextify); class AlphaTabWebPackPlugin { constructor(options) { this.options = options ?? {}; } apply(compiler) { // here we create all plugin related class implementations using // the webpack instance provided to this plugin (not as global import) // after that we use the helper and factory functions we add to webpack const webPackWithAlphaTab = { webpack: compiler.webpack, alphaTab: {} }; if ('alphaTab' in compiler.webpack.util.serialization.register) { // prevent multi registration webPackWithAlphaTab.alphaTab = compiler.webpack.util.serialization.register.alphaTab; } else { compiler.webpack.util.serialization.register.alphaTab = webPackWithAlphaTab.alphaTab; injectWebWorkerDependency(webPackWithAlphaTab); injectWorkerRuntimeModule(webPackWithAlphaTab); injectWorkletDependency(webPackWithAlphaTab); injectWorkletRuntimeModule(webPackWithAlphaTab); } this._webPackWithAlphaTab = webPackWithAlphaTab; this.configureSoundFont(compiler); this.configure(compiler); } configureSoundFont(compiler) { if (this.options.assetOutputDir === false) { return; } // register soundfont as resource compiler.options.module.rules.push({ test: /\.sf2/, type: 'asset/resource' }); compiler.options.module.rules.push({ test: /\.sf3/, type: 'asset/resource' }); } configure(compiler) { const pluginName = this.constructor.name; const cachedContextify = contextify.bindContextCache(compiler.context, compiler.root); compiler.hooks.thisCompilation.tap(pluginName, (compilation, { normalModuleFactory }) => { this._webPackWithAlphaTab.alphaTab.registerWebWorkerRuntimeModule(pluginName, compilation); this._webPackWithAlphaTab.alphaTab.registerWorkletRuntimeModule(pluginName, compilation); configureAudioWorklet(this._webPackWithAlphaTab, pluginName, this.options, compiler, compilation, normalModuleFactory, cachedContextify); configureWebWorker(this._webPackWithAlphaTab, pluginName, this.options, compiler, compilation, normalModuleFactory, cachedContextify); this.configureAssetCopy(this._webPackWithAlphaTab, pluginName, compiler, compilation); }); } configureAssetCopy(webPackWithAlphaTab, pluginName, compiler, compilation) { if (this.options.assetOutputDir === false) { return; } const options = this.options; compilation.hooks.processAssets.tapAsync({ name: pluginName, stage: this._webPackWithAlphaTab.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL }, async (_, callback) => { let alphaTabSourceDir = options.alphaTabSourceDir; if (!alphaTabSourceDir) { try { const isEsm = typeof import.meta.url === 'string'; if (isEsm) { alphaTabSourceDir = url.fileURLToPath(import.meta.resolve('@coderline/alphatab')); } else { alphaTabSourceDir = require.resolve('@coderline/alphatab'); } alphaTabSourceDir = path.resolve(alphaTabSourceDir, '..'); } catch { alphaTabSourceDir = compilation.getPath('node_modules/@coderline/alphatab/dist/'); } } let isValidAlphaTabSourceDir; if (alphaTabSourceDir) { try { await fs.promises.access(path.join(alphaTabSourceDir, 'alphaTab.mjs'), fs.constants.F_OK); isValidAlphaTabSourceDir = true; } catch { isValidAlphaTabSourceDir = false; } } else { isValidAlphaTabSourceDir = false; } if (!isValidAlphaTabSourceDir) { compilation.errors.push(new this._webPackWithAlphaTab.webpack.WebpackError('Could not find alphaTab, please ensure it is installed into node_modules or configure alphaTabSourceDir')); return; } const outputPath = (options.assetOutputDir ?? compiler.options.output.path); if (!outputPath) { compilation.errors.push(new this._webPackWithAlphaTab.webpack.WebpackError('Need output.path configured in application to store asset files.')); return; } async function copyFiles(subdir) { const fullDir = path.join(alphaTabSourceDir, subdir); compilation.contextDependencies.add(path.normalize(fullDir)); const files = await fs.promises.readdir(fullDir, { withFileTypes: true }); await fs.promises.mkdir(path.join(outputPath, subdir), { recursive: true }); await Promise.all(files .filter(f => f.isFile()) .map(async (file) => { // node v20.12.0 has parentPath pointing to the path (not the file) // see https://github.com/nodejs/node/pull/50976 const sourceFilename = path.join(file.parentPath ?? file.path, file.name); await fs.promises.copyFile(sourceFilename, path.join(outputPath, subdir, file.name)); const assetFileName = `${subdir}/${file.name}`; const existingAsset = compilation.getAsset(assetFileName); const data = await fs.promises.readFile(sourceFilename); const source = new webPackWithAlphaTab.webpack.sources.RawSource(data); if (existingAsset) { compilation.updateAsset(assetFileName, source, { copied: true, sourceFilename }); } else { compilation.emitAsset(assetFileName, source, { copied: true, sourceFilename }); } })); } await Promise.all([copyFiles('font'), copyFiles('soundfont')]); callback(); }); } } export { AlphaTabWebPackPlugin };