Repository navigation
Support for other service-workers #26857
Description
Activity
Hi, this can be achieved by a custom esbuilder where you can add additional plugins and run post build tasks.
Reacted by Øystein AmundsenAs far as I understand it, the only way to add a custom esbuild plugin is through nx where they override your @angular-devkit/build-angular with their custom @nx/angular builder. Then you have access to a
pluginproperty where you can point to an esbuild plugin.
But plugging ekstra code into the build process introduces at least two different problems:- you cannot specify an additional file to include as module in the build output. If you override the build options and include an ekstra entryPoint, this will be bundled in a way that makes it impossible to run as standalone code. You have to call an additional esbuild process to build out your service-worker template and overwrite the built code into the build process onEnd.
- In the
onEndhook, the builders result.outputFiles does not include all files nescessary to inject into the workbox manifest. Only javascript chunks are present, not html, css or assets.
I could wait until output is written to disk and call workbox-build.injectManifest from disk content, but that would exclude in-memory builds which is used when spinning up local dev-server, which makes debugging the service-worker cumbersome to say the least.
Code example of such a plugin is presented in the following stackoverflow issue: https://stackoverflow.com/questions/77887040/building-workbox-into-angular-using-custom-esbuilder
import * as esbuild from 'esbuild'; import { readFileSync } from 'fs'; import * as path from 'path'; const MATCH_FILES = /^.+\.(js|css|svg|img|html|txt|webmanifest)$/i; const manifest: { url: string; revision: string }[] = []; const projectRoot = 'apps/myApp/src'; const wbInject: esbuild.Plugin = { name: 'wbInject', setup(build: esbuild.PluginBuild) { // Append ngsw-worker to entry points // This is done so that it is discoverable in the onEnd hook const options = build.initialOptions; Object.assign(options.entryPoints || {}, { 'ngsw-worker': path.resolve(projectRoot, 'ngsw-worker.ts'), }); let workerCode = ''; // Pre-build the worker, so that we get a single module for this // as angular would chunk it otherwise build.onStart(async () => { console.log('Building service-worker'); const result = await esbuild.build({ entryPoints: [projectRoot + '/ngsw-worker.ts'], bundle: true, write: false, platform: 'browser', target: 'es2017', }); workerCode = result.outputFiles[0].text; }); // Inject manifest into worker build.onEnd(async result => { if (result.errors.length !== 0) return; // Add build output to manifest const files = result.outputFiles?.filter(f => f.path.match(MATCH_FILES) && !f.path.match(/ngsw-worker/)) || []; for (const file of files) await addToManifest(file.path, file.contents); const workerFile = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js$/)); const workerSourceMap = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js.map$/)); if (!workerFile) return; // Remove cache busting from worker file [workerFile, ...(workerSourceMap != null ? [workerSourceMap] : [])].forEach( file => (file.path = file.path.replace(/\-[A-Z0-9]{8}\./, '.')), ); // Inject manifest into worker const updatedWorkerCode = workerCode.replace('self.__WB_MANIFEST', JSON.stringify(manifest)); // Update the worker file in the output workerFile.contents = new TextEncoder().encode(updatedWorkerCode); }); }, }; async function addToManifest(filePath: string, contents?: Uint8Array) { const relPath = path.relative(process.cwd(), filePath); if (manifest.findIndex(i => i.url === relPath) !== -1) return; const content = contents || (await readFileSync(path.resolve(projectRoot, relPath))); const hash = await createHash(content); manifest.push({ url: relPath, revision: hash }); } async function createHash(content: BufferSource) { const hash: ArrayBuffer = await crypto.subtle.digest('SHA-256', content); return Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')) .join(''); } export default wbInject;
It would be nice if angular would consider a better support for plugins, and documentation for how a normal use-case like this could be implemented.
angular-automatic-lock-bot commented
on Mar 8, 2024 More actionsThis issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.
- locked and limited conversation to collaborators
on Mar 8, 2024
Command
serve
Description
I want to leverage todays file-watch when using
ng serve. I also want to use WorkBox and not angular-serviceworker. This conflicts, for in order to compile my WorkBox template and inject built files into it it requires a second build step to be performed after angular has done emitting files to the output directory. I managed to get something like this working using a custom webpack configuration, but with angular 17 we are moving away from webpack and over to esbuild, and this customization is no longer available.Describe the solution you'd like
In the angular.json config, a
serviceWorkeris either missing or declared as a pointer to an angular-serviceworker json config.I haven't thought this fully through yet, but something like the following config would be nice:
Describe alternatives you've considered
The alternative is to give up HMR and
ng servecompletely, build angular first, then build and injectManifest into service worker and then run the entire project through a local http-server. This greatly reduces productivity.