UNPKG

@serwist/next

Version:

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

242 lines (236 loc) 10.1 kB
import { t as injectManifestOptions } from "./chunks/schema-BhRhcBIb.js"; import { createRequire } from "node:module"; 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 { bold, green, red, white, yellow } from "kolorist"; import semver from "semver"; import { SerwistConfigError, validationErrorMap } from "@serwist/build/schema"; import { z } from "zod"; //#region src/lib/find-first-truthy.ts /** * Find the first truthy value in an array. * @param arr * @param fn * @returns */ const findFirstTruthy = (arr, fn) => { for (const i of arr) { const resolved = fn(i); if (resolved) return resolved; } }; //#endregion //#region src/lib/get-file-hash.ts const getFileHash = (file) => crypto.createHash("md5").update(fs.readFileSync(file)).digest("hex"); //#endregion //#region src/lib/get-content-hash.ts const getContentHash = (file, isDev) => { if (isDev) return "development"; return getFileHash(file).slice(0, 16); }; createRequire(import.meta.url); //#endregion //#region src/lib/load-tsconfig.ts 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 : void 0; }); if (!tsConfigPath) return; return JSON.parse(fs.readFileSync(tsConfigPath, "utf-8")); } catch { return; } }; //#endregion //#region src/lib/logger.ts const require = createRequire(import.meta.url); const LOGGING_SPACE_PREFIX = semver.gte(require("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] === void 0) && 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); //#endregion //#region src/lib/validator.ts 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; }; //#endregion //#region src/index.ts const dirname = "__dirname" in globalThis ? __dirname : fileURLToPath(new URL(".", import.meta.url)); /** * Integrates Serwist into your Next.js app. * @param userOptions * @returns */ 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.mjs"); 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.mjs"); 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) resolvedManifestEntries = globSync(globPublicPatterns, { nodir: true, follow: true, cwd: publicDir, ignore: [ "swe-worker-*.js", destBase, `${destBase}.map` ] }).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 publicFilesPrefix = `${publicPath}${relativeToOutputPath(compilation, publicDir)}`; return { 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; }), warnings: [] }; }], ...otherBuildOptions })); } return config; } }); }; //#endregion export { withSerwistInit as default, validateInjectManifestOptions }; //# sourceMappingURL=index.mjs.map