@afterpack/parcel-optimizer 0.2.0-rc.20260930T1421 → 0.2.1

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
@@ -28,6 +28,24 @@ that way.
28
28
  Keep the `"..."`: it runs Parcel's own minifier first and AfterPack last. If obfuscation fails, the
29
29
  build fails.
30
30
 
31
+ ## Check a deploy
32
+
33
+ Add the plugin's reporter to the build command, and each build writes `.afterpack-protection.json`
34
+ into the target's output directory, a receipt with a hash per obfuscated bundle:
35
+
36
+ ```sh
37
+ parcel build index.html --reporter @afterpack/parcel-optimizer/reporter
38
+ npx afterpack verify dist # in the deploy step, before the upload
39
+ ```
40
+
41
+ `afterpack verify` fails if a file changed after it was obfuscated, or if there is no receipt. The
42
+ reporter writes none when a bundle it shipped was not obfuscated in that build: the optimizer is
43
+ missing from `.parcelrc`, `build.autorun` is off, or Parcel reused the bundle from `.parcel-cache`.
44
+ Parcel's naming rule for plugins in `.parcelrc` leaves no place there for a reporter in this
45
+ package, so it goes on the command line. The optimizer leaves the reporter one small record per
46
+ bundle in `.afterpack/parcel/`. The reporter clears them as each build starts; without it they stay
47
+ there, ignored by git.
48
+
31
49
  ## Configuration
32
50
 
33
51
  `.parcelrc` cannot pass options, so put them in `afterpack.json` at your project root, or in
@@ -81,8 +99,7 @@ source, so never deploy or commit it.
81
99
  `"preset": "minify"`. Parcel's default ES module output is not affected.
82
100
  - **Cache.** Parcel caches optimizer output. Clear `.parcel-cache` for a fresh seed on an unchanged
83
101
  build.
84
- - **Not available here:** [`build.backup`][build.backup] and [`paths.include`][paths.include], and there is no protection receipt, so
85
- `afterpack verify` has nothing to check on a Parcel build.
102
+ - **Not available here:** [`build.backup`][build.backup] and [`paths.include`][paths.include].
86
103
  - Parcel may print that ES module dependencies are experimental and that the plugin has
87
104
  non-statically analyzable dependencies. Both are harmless.
88
105
 
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { clearBundleRecord, escapeParcelMarkdown, writeBundleRecord } from "./records-Bgj9LonR.js";
1
2
  import { join } from "node:path";
2
3
  import { text } from "node:stream/consumers";
3
4
  import { processBatch, version } from "@afterpack/core";
@@ -19,9 +20,6 @@ async function blobToString(blob) {
19
20
  if (Buffer.isBuffer(blob)) return blob.toString("utf8");
20
21
  return await text(blob);
21
22
  }
22
- function escapeParcelMarkdown(message) {
23
- return message.replace(/[\\*_`~]/g, (c) => `\\${c}`);
24
- }
25
23
  const CONTENT_HASH_REF = /HASH_REF_\w{16}/g;
26
24
  function contentHashRefsExcludingSourceMapComment(code) {
27
25
  const refs = new Set(code.match(CONTENT_HASH_REF) ?? []);
@@ -61,6 +59,7 @@ var src_default = new Optimizer({
61
59
  map
62
60
  };
63
61
  if (bundle.type !== "js") return unchanged;
62
+ clearBundleRecord(options.projectRoot, bundle.id);
64
63
  if (!bundle.env.shouldOptimize) return unchanged;
65
64
  const autorun = config.options.build?.autorun ?? true;
66
65
  if (!autorun) return unchanged;
@@ -138,6 +137,15 @@ var src_default = new Optimizer({
138
137
  origin: PLUGIN_NAME,
139
138
  hints: ["Run `parcel build --no-content-hash` (chunk URLs stop being content-addressed).", "Or set `\"preset\": \"minify\"` in afterpack.json to ship minify-only."]
140
139
  } });
140
+ const receipt = result.deferredReceipt;
141
+ if (receipt) writeBundleRecord(options.projectRoot, bundle.id, {
142
+ tool: receipt.tool,
143
+ engine: receipt.engine,
144
+ engineVersion: receipt.engineVersion,
145
+ seed: String(receipt.seed),
146
+ seedOrigin: receipt.seedOrigin,
147
+ transformed: receipt.transformed.includes(projectedFilePath)
148
+ });
141
149
  let outCode = out.code;
142
150
  let originalRelativeSourceMap = null;
143
151
  if (result.policy.sourceMap && out.sourceMap != null) {
@@ -0,0 +1,49 @@
1
+ import { join } from "node:path";
2
+ import { ensureAfterpackGitignore } from "@afterpack/integration-utils";
3
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+
5
+ //#region src/markdown.ts
6
+ function escapeParcelMarkdown(message) {
7
+ return message.replace(/[\\*_`~]/g, (c) => `\\${c}`);
8
+ }
9
+
10
+ //#endregion
11
+ //#region src/records.ts
12
+ function afterpackDirOf(projectRoot) {
13
+ return join(projectRoot, ".afterpack");
14
+ }
15
+ function recordsDirOf(projectRoot) {
16
+ return join(afterpackDirOf(projectRoot), "parcel");
17
+ }
18
+ function recordPathOf(projectRoot, bundleId) {
19
+ return join(recordsDirOf(projectRoot), `${bundleId}.json`);
20
+ }
21
+ function writeBundleRecord(projectRoot, bundleId, record) {
22
+ const path = recordPathOf(projectRoot, bundleId);
23
+ mkdirSync(recordsDirOf(projectRoot), { recursive: true });
24
+ ensureAfterpackGitignore(afterpackDirOf(projectRoot));
25
+ writeFileSync(path, `${JSON.stringify(record)}\n`);
26
+ }
27
+ function clearBundleRecord(projectRoot, bundleId) {
28
+ rmSync(recordPathOf(projectRoot, bundleId), { force: true });
29
+ }
30
+ function clearBundleRecords(projectRoot) {
31
+ rmSync(recordsDirOf(projectRoot), {
32
+ recursive: true,
33
+ force: true
34
+ });
35
+ }
36
+ function readBundleRecord(projectRoot, bundleId) {
37
+ try {
38
+ const raw = JSON.parse(readFileSync(recordPathOf(projectRoot, bundleId), "utf8"));
39
+ if (raw === null || typeof raw !== "object") return null;
40
+ const record = raw;
41
+ if (typeof record.tool !== "string" || typeof record.transformed !== "boolean") return null;
42
+ return record;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ //#endregion
49
+ export { clearBundleRecord, clearBundleRecords, escapeParcelMarkdown, readBundleRecord, writeBundleRecord };
@@ -0,0 +1,6 @@
1
+ import { Reporter } from "@parcel/plugin";
2
+
3
+ //#region src/reporter.d.ts
4
+ declare const _default: Reporter;
5
+ //#endregion
6
+ export { _default as default };
@@ -0,0 +1,74 @@
1
+ import { clearBundleRecords, escapeParcelMarkdown, readBundleRecord } from "./records-Bgj9LonR.js";
2
+ import { join, relative } from "node:path";
3
+ import { PROTECTION_RECEIPT_FILE, writeProtectionReceipt } from "@afterpack/integration-utils";
4
+ import { Reporter } from "@parcel/plugin";
5
+ import { rmSync } from "node:fs";
6
+
7
+ //#region src/reporter.ts
8
+ const LABEL = "afterpack-parcel";
9
+ function shipsObfuscatedJs(bundle) {
10
+ return bundle.type === "js" && bundle.env.shouldOptimize && bundle.bundleBehavior !== "inline";
11
+ }
12
+ function agreed(values) {
13
+ const distinct = [...new Set(values)];
14
+ return distinct.length === 1 ? distinct[0] : null;
15
+ }
16
+ function joined(values) {
17
+ return [...new Set(values)].join(",");
18
+ }
19
+ function collectDistTrees(bundles, projectRoot) {
20
+ const trees = new Map();
21
+ for (const bundle of bundles) {
22
+ if (!shipsObfuscatedJs(bundle)) continue;
23
+ const dir = bundle.target.distDir;
24
+ let tree = trees.get(dir);
25
+ if (!tree) {
26
+ tree = {
27
+ files: [],
28
+ records: [],
29
+ transformed: [],
30
+ unrecorded: []
31
+ };
32
+ trees.set(dir, tree);
33
+ }
34
+ const record = readBundleRecord(projectRoot, bundle.id);
35
+ if (!record) {
36
+ tree.unrecorded.push(relative(projectRoot, bundle.filePath));
37
+ continue;
38
+ }
39
+ tree.files.push(bundle.filePath);
40
+ tree.records.push(record);
41
+ if (record.transformed) tree.transformed.push(bundle.filePath);
42
+ }
43
+ return trees;
44
+ }
45
+ var reporter_default = new Reporter({ report({ event, options, logger }) {
46
+ if (event.type === "buildStart") clearBundleRecords(options.projectRoot);
47
+ if (event.type !== "buildSuccess") return;
48
+ const trees = collectDistTrees(event.bundleGraph.getBundles(), options.projectRoot);
49
+ for (const [dir, tree] of trees) {
50
+ const receiptPath = join(dir, PROTECTION_RECEIPT_FILE);
51
+ if (tree.unrecorded.length > 0) {
52
+ rmSync(receiptPath, { force: true });
53
+ logger.warn({ message: escapeParcelMarkdown(`[${LABEL}] no protection receipt for ${relative(options.projectRoot, dir)}: AfterPack has no record of obfuscating ${tree.unrecorded.join(", ")} in this build. Keep @afterpack/parcel-optimizer in .parcelrc and build.autorun on; a bundle Parcel reused from .parcel-cache has no record either, so clear .parcel-cache and rebuild.`) });
54
+ continue;
55
+ }
56
+ const records = tree.records;
57
+ writeProtectionReceipt({
58
+ dir,
59
+ tool: records[0].tool,
60
+ engine: agreed(records.map((r) => r.engine)),
61
+ engineVersion: agreed(records.map((r) => r.engineVersion)),
62
+ seed: joined(records.map((r) => r.seed)),
63
+ seedOrigin: joined(records.map((r) => r.seedOrigin)),
64
+ bundler: "parcel",
65
+ buildId: null,
66
+ files: tree.files,
67
+ transformed: tree.transformed
68
+ });
69
+ logger.info({ message: escapeParcelMarkdown(`[${LABEL}] wrote protection receipt -> ${receiptPath} (afterpack verify)`) });
70
+ }
71
+ } });
72
+
73
+ //#endregion
74
+ export { reporter_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterpack/parcel-optimizer",
3
- "version": "0.2.0-rc.20260930T1421",
3
+ "version": "0.2.1",
4
4
  "description": "Parcel 2 optimizer for AfterPack: obfuscate each JavaScript bundle in memory during parcel build",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./reporter": {
15
+ "types": "./dist/reporter.d.ts",
16
+ "import": "./dist/reporter.js",
17
+ "default": "./dist/reporter.js"
13
18
  }
14
19
  },
15
20
  "files": [
@@ -21,11 +26,11 @@
21
26
  "@parcel/core": ">=2.9.0"
22
27
  },
23
28
  "dependencies": {
24
- "@afterpack/core": "~0.2.0",
29
+ "@afterpack/core": "~0.2.2",
25
30
  "@parcel/diagnostic": "^2.9.0",
26
31
  "@parcel/plugin": "^2.9.0",
27
32
  "@parcel/source-map": "^2.1.1",
28
- "@afterpack/integration-utils": "0.2.0-rc.20260930T1421"
33
+ "@afterpack/integration-utils": "0.2.1"
29
34
  },
30
35
  "devDependencies": {
31
36
  "@biomejs/biome": "2.4.4",
@@ -68,8 +73,8 @@
68
73
  "url": "https://github.com/afterpack-dev/afterpack/issues"
69
74
  },
70
75
  "scripts": {
71
- "build": "tsdown src/index.ts --format esm --dts",
72
- "dev": "tsdown src/index.ts --format esm --watch",
76
+ "build": "tsdown src/index.ts src/reporter.ts --format esm --dts",
77
+ "dev": "tsdown src/index.ts src/reporter.ts --format esm --watch",
73
78
  "lint": "biome check .",
74
79
  "lint:fix": "biome check --write .",
75
80
  "typecheck": "tsc --noEmit",