UNPKG

@serwist/next

Version:

A module that integrates Serwist into your Next.js application.

286 lines (271 loc) 13.5 kB
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { InjectManifest } from '@serwist/webpack-plugin'; import { ChildCompilationPlugin, relativeToOutputPath } from '@serwist/webpack-plugin/internal'; import { globSync } from 'glob'; import crypto from 'node:crypto'; import { createRequire } from 'node:module'; import { green, bold, white, yellow, red } from 'kolorist'; import semver from 'semver'; import { validationErrorMap, SerwistConfigError } from '@serwist/build/schema'; import { z } from 'zod'; import { i as injectManifestOptions } from './chunks/schema.js'; import '@serwist/webpack-plugin/schema'; const findFirstTruthy = (arr, fn)=>{ for (const i of arr){ const resolved = fn(i); if (resolved) { return resolved; } } return undefined; }; const getFileHash = (file)=>crypto.createHash("md5").update(fs.readFileSync(file)).digest("hex"); const getContentHash = (file, isDev)=>{ if (isDev) { return "development"; } return getFileHash(file).slice(0, 16); }; createRequire(import.meta.url); const loadTSConfig = (baseDir, relativeTSConfigPath)=>{ try { const tsConfigPath = findFirstTruthy([ relativeTSConfigPath ?? "tsconfig.json", "jsconfig.json" ], (filePath)=>{ const resolvedPath = path.join(baseDir, filePath); return fs.existsSync(resolvedPath) ? resolvedPath : undefined; }); if (!tsConfigPath) { return undefined; } return JSON.parse(fs.readFileSync(tsConfigPath, "utf-8")); } catch { return undefined; } }; const require$1 = createRequire(import.meta.url); const LOGGING_SPACE_PREFIX = semver.gte(require$1("next/package.json").version, "16.0.0") ? "" : " "; const prefixedLog = (prefixType, ...message)=>{ let prefix; let consoleMethod; switch(prefixType){ case "wait": prefix = `${white(bold("○"))} (serwist)`; consoleMethod = "log"; break; case "error": prefix = `${red(bold("X"))} (serwist)`; consoleMethod = "error"; break; case "warn": prefix = `${yellow(bold("⚠"))} (serwist)`; consoleMethod = "warn"; break; case "info": prefix = `${white(bold("○"))} (serwist)`; consoleMethod = "log"; break; case "event": prefix = `${green(bold("✓"))} (serwist)`; consoleMethod = "log"; break; } if ((message[0] === "" || message[0] === undefined) && message.length === 1) { message.shift(); } if (message.length === 0) { console[consoleMethod](""); } else { console[consoleMethod](`${LOGGING_SPACE_PREFIX}${prefix}`, ...message); } }; const info = (...message)=>prefixedLog("info", ...message); const event = (...message)=>prefixedLog("event", ...message); const validateInjectManifestOptions = (input)=>{ const result = injectManifestOptions.safeParse(input, { error: validationErrorMap }); if (!result.success) { throw new SerwistConfigError({ moduleName: "@serwist/next", message: z.prettifyError(result.error) }); } return result.data; }; const dirname = "__dirname" in globalThis ? __dirname : fileURLToPath(new URL(".", import.meta.url)); const withSerwistInit = (userOptions)=>{ if (!process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING && process.env.TURBOPACK && !userOptions.disable) { process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING = "1"; console.warn(`[@serwist/next] WARNING: You are using '@serwist/next' with \`next dev --turbopack\`, but it doesn't support Turbopack. Do one of the following: - Set \`disable\` to \`process.env.NODE_ENV !== "production"\`. - Use webpack by running \`next dev --webpack\` instead of \`next dev --turbopack\`. - Migrate to '@serwist/turbopack' which has experimental support for Turbopack. See https://serwist.pages.dev/docs/next/turbo for more information. - Migrate to configurator mode which has support for Turbopack. See https://serwist.pages.dev/docs/next/config for more information. Follow https://github.com/serwist/serwist/issues/54 for progress on Serwist + Turbopack. You can also suppress this warning by setting SERWIST_SUPPRESS_TURBOPACK_WARNING=1.\n`); } return (nextConfig = {})=>({ ...nextConfig, webpack (config, options) { const webpack = options.webpack; const { dev } = options; const basePath = options.config.basePath || "/"; const tsConfigJson = loadTSConfig(options.dir, nextConfig?.typescript?.tsconfigPath); const { cacheOnNavigation, disable, scope = basePath, swUrl, register, reloadOnOnline, globPublicPatterns, ...buildOptions } = validateInjectManifestOptions(userOptions); if (typeof nextConfig.webpack === "function") { config = nextConfig.webpack(config, options); } if (disable) { options.isServer && info("Serwist is disabled."); return config; } if (!config.plugins) { config.plugins = []; } const _sw = path.posix.join(basePath, swUrl); const _scope = path.posix.join(scope, "/"); config.plugins.push(new webpack.DefinePlugin({ "self.__SERWIST_SW_ENTRY.sw": `'${_sw}'`, "self.__SERWIST_SW_ENTRY.scope": `'${_scope}'`, "self.__SERWIST_SW_ENTRY.cacheOnNavigation": `${cacheOnNavigation}`, "self.__SERWIST_SW_ENTRY.register": `${register}`, "self.__SERWIST_SW_ENTRY.reloadOnOnline": `${reloadOnOnline}` })); const swEntryJs = path.join(dirname, "sw-entry.js"); const entry = config.entry; config.entry = async ()=>{ const entries = await entry(); if (entries["main.js"] && !entries["main.js"].includes(swEntryJs)) { if (Array.isArray(entries["main.js"])) { entries["main.js"].unshift(swEntryJs); } else if (typeof entries["main.js"] === "string") { entries["main.js"] = [ swEntryJs, entries["main.js"] ]; } } if (entries["main-app"] && !entries["main-app"].includes(swEntryJs)) { if (Array.isArray(entries["main-app"])) { entries["main-app"].unshift(swEntryJs); } else if (typeof entries["main-app"] === "string") { entries["main-app"] = [ swEntryJs, entries["main-app"] ]; } } return entries; }; if (!options.isServer) { if (!register) { info("The service worker will not be automatically registered, please call 'window.serwist.register()' in 'componentDidMount' or 'useEffect'."); if (!tsConfigJson?.compilerOptions?.types?.includes("@serwist/next/typings")) { info("You may also want to add '@serwist/next/typings' to your TypeScript/JavaScript configuration file at 'compilerOptions.types'."); } } const { swSrc: userSwSrc, swDest: userSwDest, additionalPrecacheEntries, exclude, manifestTransforms = [], ...otherBuildOptions } = buildOptions; let swSrc = userSwSrc; let swDest = userSwDest; if (!path.isAbsolute(swSrc)) { swSrc = path.join(options.dir, swSrc); } if (!path.isAbsolute(swDest)) { swDest = path.join(options.dir, swDest); } const publicDir = path.resolve(options.dir, "public"); const { dir: destDir, base: destBase } = path.parse(swDest); const cleanUpList = globSync([ "swe-worker-*.js", "swe-worker-*.js.map", destBase, `${destBase}.map` ], { absolute: true, nodir: true, follow: true, cwd: destDir }); for (const file of cleanUpList){ fs.rmSync(file, { force: true }); } const shouldBuildSWEntryWorker = cacheOnNavigation; let swEntryPublicPath; let swEntryWorkerDest; if (shouldBuildSWEntryWorker) { const swEntryWorkerSrc = path.join(dirname, "sw-entry-worker.js"); const swEntryName = `swe-worker-${getContentHash(swEntryWorkerSrc, dev)}.js`; swEntryPublicPath = path.posix.join(basePath, swEntryName); swEntryWorkerDest = path.join(destDir, swEntryName); config.plugins.push(new ChildCompilationPlugin({ src: swEntryWorkerSrc, dest: swEntryWorkerDest })); } config.plugins.push(new webpack.DefinePlugin({ "self.__SERWIST_SW_ENTRY.swEntryWorker": swEntryPublicPath && `'${swEntryPublicPath}'` })); event(`Bundling the service worker script with the URL '${_sw}' and the scope '${_scope}'...`); let resolvedManifestEntries = additionalPrecacheEntries; if (!resolvedManifestEntries) { const publicScan = globSync(globPublicPatterns, { nodir: true, follow: true, cwd: publicDir, ignore: [ "swe-worker-*.js", destBase, `${destBase}.map` ] }); resolvedManifestEntries = publicScan.map((f)=>({ url: path.posix.join(basePath, f), revision: getFileHash(path.join(publicDir, f)) })); } const publicPath = config.output?.publicPath; config.plugins.push(new InjectManifest({ swSrc, swDest, disablePrecacheManifest: dev, additionalPrecacheEntries: dev ? [] : resolvedManifestEntries, exclude: [ ...exclude, ({ asset, compilation })=>{ const swDestRelativeOutput = relativeToOutputPath(compilation, swDest); const swAsset = compilation.getAsset(swDestRelativeOutput); return asset.name === swAsset?.name || asset.name.startsWith("server/") || /^[^/]*\.json$/.test(asset.name) || dev && !asset.name.startsWith("static/runtime/"); } ], manifestTransforms: [ ...manifestTransforms, async (manifestEntries, compilation)=>{ const publicDirRelativeOutput = relativeToOutputPath(compilation, publicDir); const publicFilesPrefix = `${publicPath}${publicDirRelativeOutput}`; const manifest = manifestEntries.map((m)=>{ m.url = m.url.replace("/_next//static/image", "/_next/static/image").replace("/_next//static/media", "/_next/static/media"); if (m.url.startsWith(publicFilesPrefix)) { m.url = path.posix.join(basePath, m.url.replace(publicFilesPrefix, "")); } m.url = m.url.replace(/\[/g, "%5B").replace(/\]/g, "%5D").replace(/@/g, "%40"); return m; }); return { manifest, warnings: [] }; } ], ...otherBuildOptions })); } return config; } }); }; export { withSerwistInit as default, validateInjectManifestOptions };