@afterpack/next 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,9 +37,11 @@ the comments that point at them, since a served source map gives your source awa
37
37
 
38
38
  ## Check a deploy
39
39
 
40
- Each build writes `.next/.afterpack-protection.json`, a receipt with a hash per obfuscated file.
41
- Run `npx afterpack verify .` before you deploy. It fails when the receipt is missing, belongs to a
42
- different build, or a file changed after obfuscation.
40
+ Each build writes `.afterpack/receipts/.next.json`, a receipt with a hash per obfuscated file.
41
+ `.afterpack/` sits next to your project's lockfile, `afterpack.json` or `.git` (failing those, its
42
+ `package.json`), out of `.next/`, so it never ships with the build. Run `npx afterpack verify .`
43
+ before you deploy. It fails when the receipt is missing, belongs to a different build, or a file
44
+ changed after obfuscation.
43
45
 
44
46
  ## Options
45
47
 
@@ -51,7 +53,7 @@ Pass them as the second argument: `withAfterpack(nextConfig, { preset: "hard", s
51
53
  | [`seed`][seed] | a number or string; `"git"` uses the current commit | a new random seed per build |
52
54
  | [`identifiers.reserved`][identifiers.reserved] | names never to rename | none |
53
55
  | [`paths.exclude`][paths.exclude] | globs for files to leave untouched | none |
54
- | [`sourceMap.enabled`][sourceMap.enabled] | write source maps for the obfuscated output | on in development when an input map exists, off in production |
56
+ | [`sourceMap.enabled`][sourceMap.enabled] | write source maps for the obfuscated output | off; on in development mode (`build.mode` or `NODE_ENV=development`) when an input map exists |
55
57
  | [`protectionMap.enabled`][protectionMap.enabled] | write the Protection Map | on when Next emits browser source maps |
56
58
  | [`build.autorun`][build.autorun] | `false` turns AfterPack off | `true` |
57
59
 
package/dist/index.cjs CHANGED
@@ -45,28 +45,30 @@ function detectBundler(distDir) {
45
45
  if ((0, node_fs.existsSync)((0, node_path.join)(distDir, "server", "webpack-runtime.js"))) return "webpack";
46
46
  return "unknown";
47
47
  }
48
- function stripTrailer(path, strip) {
49
- try {
50
- const code = (0, node_fs.readFileSync)(path, "utf8");
51
- const stripped = strip(code);
52
- if (stripped === code) return false;
53
- (0, node_fs.writeFileSync)(path, stripped);
54
- return true;
55
- } catch {
56
- return false;
57
- }
48
+ const TURBOPACK_CHUNK_RE = /^(?:turbopack-)?([0-9a-z~._-]{13})\.(?:js|css)$/;
49
+ const WEBPACK_CHUNK_RE = /[-.]([0-9a-f]{16})\.js$/;
50
+ function nextChunkToken(bundler) {
51
+ const pattern = bundler === "turbopack" ? TURBOPACK_CHUNK_RE : bundler === "webpack" ? WEBPACK_CHUNK_RE : null;
52
+ return (relativePath) => {
53
+ if (pattern === null || !relativePath.startsWith("static/chunks/")) return null;
54
+ return pattern.exec((0, node_path.basename)(relativePath))?.[1] ?? null;
55
+ };
56
+ }
57
+ function skipNextInternals(relativePath) {
58
+ return relativePath === "cache" || relativePath.startsWith("trace");
59
+ }
60
+ function stripTrailer(path) {
61
+ const code = (0, node_fs.readFileSync)(path, "utf8");
62
+ const stripped = (0, __afterpack_integration_utils.withSourceMappingURL)(code, null);
63
+ if (stripped === code) return false;
64
+ (0, node_fs.writeFileSync)(path, stripped);
65
+ return true;
58
66
  }
59
67
  function stripServedSourceMaps(chunks, servedDir, log = (message) => console.log(message)) {
60
- let removed = 0;
61
- for (const mapPath of (0, __afterpack_integration_utils.collectSourceMaps)(servedDir)) try {
62
- (0, node_fs.unlinkSync)(mapPath);
63
- removed++;
64
- } catch (e) {
65
- console.warn(`[${LABEL}] could not remove served source map ${mapPath}: ${e}`);
66
- }
68
+ const removed = (0, __afterpack_integration_utils.removeSourceMaps)((0, __afterpack_integration_utils.collectSourceMaps)(servedDir), LABEL);
67
69
  let trailers = 0;
68
- for (const js of chunks) if (stripTrailer(js, (code) => (0, __afterpack_integration_utils.withSourceMappingURL)(code, null))) trailers++;
69
- for (const css of (0, __afterpack_integration_utils.collectStylesheets)(servedDir)) if (stripTrailer(css, __afterpack_integration_utils.withoutCssSourceMappingURL)) trailers++;
70
+ for (const js of chunks) if (stripTrailer(js)) trailers++;
71
+ trailers += (0, __afterpack_integration_utils.stripCssSourceMappingURLs)((0, __afterpack_integration_utils.collectStylesheets)(servedDir));
70
72
  if (removed > 0 || trailers > 0) {
71
73
  const tail = trailers > 0 ? ` + ${trailers} sourceMappingURL trailer(s)` : "";
72
74
  log(`[${LABEL}] stripped ${removed} served source map(s)${tail} from the client tree`);
@@ -99,7 +101,10 @@ async function runAfterpackHook(input) {
99
101
  const files = (0, __afterpack_integration_utils.collectJsFiles)(chunksDir, { include: settings.paths?.include });
100
102
  if (files.length === 0) throw new Error(`[${LABEL}] no client JS found under ${chunksDir} — refusing to report a protected build for a tree nothing was obfuscated in.`);
101
103
  const engine = input.engine ?? await loadEngineOnDemand();
102
- await (0, __afterpack_integration_utils.runObfuscationPass)({
104
+ const bundler = detectBundler(distDir);
105
+ if (bundler === "unknown") console.warn(`[${LABEL}] could not tell whether this build used Turbopack or webpack, so no content-hashed chunk name was changed. The obfuscated chunks keep the plain build's names, and an immutable cache may serve stale bytes. Report this with your Next.js version.`);
106
+ const buildId = readBuildIdFromManifest(distDir);
107
+ const result = await (0, __afterpack_integration_utils.runObfuscationPass)({
103
108
  files,
104
109
  engine,
105
110
  client: (0, __afterpack_integration_utils.resolveClientIdentity)(require("url").pathToFileURL(__filename).href),
@@ -107,24 +112,32 @@ async function runAfterpackHook(input) {
107
112
  label: LABEL,
108
113
  cwd: projectDir,
109
114
  startedAt,
110
- combinedProtectionMap: {
111
- buildDir: distDir,
112
- afterpackDir: (0, node_path.join)(projectDir, ".afterpack")
113
- },
115
+ combinedProtectionMap: { buildDir: distDir },
116
+ sourceMapSweepDir: (0, node_path.join)(distDir, "static"),
114
117
  ...(0, __afterpack_integration_utils.passSettings)(resolved),
115
118
  postMinify: true,
116
- afterWrite: () => {
117
- stripServedSourceMaps(files, (0, node_path.join)(distDir, "static"), settings.diagnostics?.level === "none" ? () => {} : (message) => console.log(message));
119
+ hashedNames: {
120
+ root: distDir,
121
+ tokenOf: nextChunkToken(bundler),
122
+ skip: skipNextInternals
123
+ },
124
+ afterWrite: (written) => {
125
+ stripServedSourceMaps(written, (0, node_path.join)(distDir, "static"), settings.diagnostics?.level === "none" ? () => {} : (message) => console.log(message));
118
126
  },
119
127
  receipt: {
120
- bundler: detectBundler(distDir),
121
- buildId: readBuildIdFromManifest(distDir)
128
+ bundler,
129
+ buildId
122
130
  },
123
131
  messages: {
124
132
  autoEnableBundlerSourcemap: "protectionMap:true but no bundler sourcemap was found. Set `productionBrowserSourceMaps: true` in next.config so the Protection Map can render your original pre-bundle source.",
125
133
  directivesNeedClientMaps: "directives are enabled but no client source maps were found. Set `productionBrowserSourceMaps: true` in next.config so `/* @afterpack ... */` directives can be recovered from the emitted chunks' sourcesContent."
126
134
  }
127
135
  });
136
+ if (buildId !== null && result.transformedFiles.length > 0) warnIfBuildIdPinned(buildId, settings.diagnostics?.level);
137
+ }
138
+ function warnIfBuildIdPinned(buildId, diagnosticsLevel) {
139
+ if (diagnosticsLevel === "none") return;
140
+ if (!/^[0-9a-z]{16,}$/i.test(buildId)) console.warn(`[${LABEL}] this build's id "${buildId}" is not a random Next id, so \`static/${buildId}/_buildManifest.js\` and \`_ssgManifest.js\` keep one immutable URL across releases while their bytes change. AfterPack cannot rename the build id — Next writes into static/${buildId}/ after the build hook runs. Leave \`generateBuildId\` unset so Next randomizes it per build, or bump it every release.`);
128
141
  }
129
142
 
130
143
  //#endregion
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { collectJsFiles, collectSourceMaps, collectStylesheets, createTelemetryReporter, passSettings, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, withSourceMappingURL, withoutCssSourceMappingURL } from "@afterpack/integration-utils";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { collectJsFiles, collectSourceMaps, collectStylesheets, createTelemetryReporter, passSettings, removeSourceMaps, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, stripCssSourceMappingURLs, withSourceMappingURL } from "@afterpack/integration-utils";
4
4
 
5
5
  //#region src/hook.ts
6
6
  const LABEL = "afterpack-next";
@@ -22,28 +22,30 @@ function detectBundler(distDir) {
22
22
  if (existsSync(join(distDir, "server", "webpack-runtime.js"))) return "webpack";
23
23
  return "unknown";
24
24
  }
25
- function stripTrailer(path, strip) {
26
- try {
27
- const code = readFileSync(path, "utf8");
28
- const stripped = strip(code);
29
- if (stripped === code) return false;
30
- writeFileSync(path, stripped);
31
- return true;
32
- } catch {
33
- return false;
34
- }
25
+ const TURBOPACK_CHUNK_RE = /^(?:turbopack-)?([0-9a-z~._-]{13})\.(?:js|css)$/;
26
+ const WEBPACK_CHUNK_RE = /[-.]([0-9a-f]{16})\.js$/;
27
+ function nextChunkToken(bundler) {
28
+ const pattern = bundler === "turbopack" ? TURBOPACK_CHUNK_RE : bundler === "webpack" ? WEBPACK_CHUNK_RE : null;
29
+ return (relativePath) => {
30
+ if (pattern === null || !relativePath.startsWith("static/chunks/")) return null;
31
+ return pattern.exec(basename(relativePath))?.[1] ?? null;
32
+ };
33
+ }
34
+ function skipNextInternals(relativePath) {
35
+ return relativePath === "cache" || relativePath.startsWith("trace");
36
+ }
37
+ function stripTrailer(path) {
38
+ const code = readFileSync(path, "utf8");
39
+ const stripped = withSourceMappingURL(code, null);
40
+ if (stripped === code) return false;
41
+ writeFileSync(path, stripped);
42
+ return true;
35
43
  }
36
44
  function stripServedSourceMaps(chunks, servedDir, log = (message) => console.log(message)) {
37
- let removed = 0;
38
- for (const mapPath of collectSourceMaps(servedDir)) try {
39
- unlinkSync(mapPath);
40
- removed++;
41
- } catch (e) {
42
- console.warn(`[${LABEL}] could not remove served source map ${mapPath}: ${e}`);
43
- }
45
+ const removed = removeSourceMaps(collectSourceMaps(servedDir), LABEL);
44
46
  let trailers = 0;
45
- for (const js of chunks) if (stripTrailer(js, (code) => withSourceMappingURL(code, null))) trailers++;
46
- for (const css of collectStylesheets(servedDir)) if (stripTrailer(css, withoutCssSourceMappingURL)) trailers++;
47
+ for (const js of chunks) if (stripTrailer(js)) trailers++;
48
+ trailers += stripCssSourceMappingURLs(collectStylesheets(servedDir));
47
49
  if (removed > 0 || trailers > 0) {
48
50
  const tail = trailers > 0 ? ` + ${trailers} sourceMappingURL trailer(s)` : "";
49
51
  log(`[${LABEL}] stripped ${removed} served source map(s)${tail} from the client tree`);
@@ -76,7 +78,10 @@ async function runAfterpackHook(input) {
76
78
  const files = collectJsFiles(chunksDir, { include: settings.paths?.include });
77
79
  if (files.length === 0) throw new Error(`[${LABEL}] no client JS found under ${chunksDir} — refusing to report a protected build for a tree nothing was obfuscated in.`);
78
80
  const engine = input.engine ?? await loadEngineOnDemand();
79
- await runObfuscationPass({
81
+ const bundler = detectBundler(distDir);
82
+ if (bundler === "unknown") console.warn(`[${LABEL}] could not tell whether this build used Turbopack or webpack, so no content-hashed chunk name was changed. The obfuscated chunks keep the plain build's names, and an immutable cache may serve stale bytes. Report this with your Next.js version.`);
83
+ const buildId = readBuildIdFromManifest(distDir);
84
+ const result = await runObfuscationPass({
80
85
  files,
81
86
  engine,
82
87
  client: resolveClientIdentity(import.meta.url),
@@ -84,24 +89,32 @@ async function runAfterpackHook(input) {
84
89
  label: LABEL,
85
90
  cwd: projectDir,
86
91
  startedAt,
87
- combinedProtectionMap: {
88
- buildDir: distDir,
89
- afterpackDir: join(projectDir, ".afterpack")
90
- },
92
+ combinedProtectionMap: { buildDir: distDir },
93
+ sourceMapSweepDir: join(distDir, "static"),
91
94
  ...passSettings(resolved),
92
95
  postMinify: true,
93
- afterWrite: () => {
94
- stripServedSourceMaps(files, join(distDir, "static"), settings.diagnostics?.level === "none" ? () => {} : (message) => console.log(message));
96
+ hashedNames: {
97
+ root: distDir,
98
+ tokenOf: nextChunkToken(bundler),
99
+ skip: skipNextInternals
100
+ },
101
+ afterWrite: (written) => {
102
+ stripServedSourceMaps(written, join(distDir, "static"), settings.diagnostics?.level === "none" ? () => {} : (message) => console.log(message));
95
103
  },
96
104
  receipt: {
97
- bundler: detectBundler(distDir),
98
- buildId: readBuildIdFromManifest(distDir)
105
+ bundler,
106
+ buildId
99
107
  },
100
108
  messages: {
101
109
  autoEnableBundlerSourcemap: "protectionMap:true but no bundler sourcemap was found. Set `productionBrowserSourceMaps: true` in next.config so the Protection Map can render your original pre-bundle source.",
102
110
  directivesNeedClientMaps: "directives are enabled but no client source maps were found. Set `productionBrowserSourceMaps: true` in next.config so `/* @afterpack ... */` directives can be recovered from the emitted chunks' sourcesContent."
103
111
  }
104
112
  });
113
+ if (buildId !== null && result.transformedFiles.length > 0) warnIfBuildIdPinned(buildId, settings.diagnostics?.level);
114
+ }
115
+ function warnIfBuildIdPinned(buildId, diagnosticsLevel) {
116
+ if (diagnosticsLevel === "none") return;
117
+ if (!/^[0-9a-z]{16,}$/i.test(buildId)) console.warn(`[${LABEL}] this build's id "${buildId}" is not a random Next id, so \`static/${buildId}/_buildManifest.js\` and \`_ssgManifest.js\` keep one immutable URL across releases while their bytes change. AfterPack cannot rename the build id — Next writes into static/${buildId}/ after the build hook runs. Leave \`generateBuildId\` unset so Next randomizes it per build, or bump it every release.`);
105
118
  }
106
119
 
107
120
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterpack/next",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Next.js integration for AfterPack: obfuscate the client JavaScript of a Next.js build inside next build",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,8 +28,8 @@
28
28
  "next": ">=15.4.0"
29
29
  },
30
30
  "dependencies": {
31
- "@afterpack/core": "~0.2.3",
32
- "@afterpack/integration-utils": "0.2.2"
31
+ "@afterpack/core": "~0.2.5",
32
+ "@afterpack/integration-utils": "0.2.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@biomejs/biome": "2.4.4",