@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 +6 -4
- package/dist/index.cjs +41 -28
- package/dist/index.js +44 -31
- package/package.json +3 -3
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
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
(
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
121
|
-
buildId
|
|
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,
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { collectJsFiles, collectSourceMaps, collectStylesheets, createTelemetryReporter, passSettings, resolveClientIdentity, resolvePluginConfig, runObfuscationPass,
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
afterpackDir: join(projectDir, ".afterpack")
|
|
90
|
-
},
|
|
92
|
+
combinedProtectionMap: { buildDir: distDir },
|
|
93
|
+
sourceMapSweepDir: join(distDir, "static"),
|
|
91
94
|
...passSettings(resolved),
|
|
92
95
|
postMinify: true,
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
98
|
-
buildId
|
|
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.
|
|
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.
|
|
32
|
-
"@afterpack/integration-utils": "0.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",
|