Skip to content

Commit 6a0ba97

Browse files
authored
feat: Normalize asset file names (#27)
Normalize asset filenames by stripping out the hashes from the file names with a wildcard.
1 parent 78966dc commit 6a0ba97

File tree

14 files changed

+375
-29
lines changed

14 files changed

+375
-29
lines changed

examples/next-js/next.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ const { codecovWebpackPlugin } = require("@codecov/webpack-plugin");
33
/** @type {import('next').NextConfig} */
44
const nextConfig = {
55
webpack: (config, options) => {
6-
config.plugins.push(codecovWebpackPlugin({ enableBundleAnalysis: true }));
6+
config.plugins.push(
7+
codecovWebpackPlugin({ enableBundleAnalysis: true, dryRun: true }),
8+
);
79

810
return config;
911
},

examples/rollup/rollup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export default defineConfig({
1919
resolve(), // tells Rollup how to find date-fns in node_modules
2020
commonjs(), // converts date-fns to ES modules
2121
production && terser(), // minify, but only in production
22-
codecovRollupPlugin({ enableBundleAnalysis: true }),
22+
codecovRollupPlugin({ enableBundleAnalysis: true, dryRun: true }),
2323
],
2424
});

examples/vite/vite.config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,20 @@ import { codecovVitePlugin } from "@codecov/vite-plugin";
44

55
// https://vitejs.dev/config/
66
export default defineConfig({
7-
plugins: [react(), codecovVitePlugin({ enableBundleAnalysis: true })],
7+
build: {
8+
rollupOptions: {
9+
output: {
10+
assetFileNames: "[name].[hash].js",
11+
chunkFileNames: "[name]-[hash].js",
12+
},
13+
},
14+
},
15+
plugins: [
16+
react(),
17+
codecovVitePlugin({
18+
enableBundleAnalysis: true,
19+
dryRun: true,
20+
globalUploadToken: "super-cool-token",
21+
}),
22+
],
823
});

examples/webpack/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
plugins: [
1212
codecovWebpackPlugin({
1313
enableBundleAnalysis: true,
14+
dryRun: true,
1415
}),
1516
],
1617
};

packages/bundler-plugin-core/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
type UploadOverrides,
1212
type Output,
1313
} from "./types.ts";
14-
1514
import { red } from "./utils/logging.ts";
15+
import { normalizePath } from "./utils/normalizePath.ts";
1616
import { bundleAnalysisPluginFactory } from "./bundle-analysis/bundleAnalysisPluginFactory.ts";
1717

1818
const NODE_VERSION_RANGE = ">=18.18.0";
@@ -48,8 +48,6 @@ function codecovUnpluginFactory({
4848
});
4949
}
5050

51-
export { red, codecovUnpluginFactory };
52-
5351
export type {
5452
BundleAnalysisUploadPlugin,
5553
Asset,
@@ -60,3 +58,5 @@ export type {
6058
UploadOverrides,
6159
Output,
6260
};
61+
62+
export { normalizePath, codecovUnpluginFactory, red };

packages/bundler-plugin-core/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ export interface Dependency {
88
export interface Asset {
99
name: string;
1010
size: number;
11+
normalized: string;
1112
}
1213

1314
export interface Chunk {
1415
id: string;
1516
uniqueId: string;
1617
entry: boolean;
1718
initial: boolean;
18-
files: string[];
1919
names: string[];
20+
files: string[];
2021
}
2122

2223
export interface Module {
2324
name: string;
2425
size?: number;
25-
chunks: (string | number)[];
2626
chunkUniqueIds: string[];
2727
}
2828

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { normalizePath } from "../normalizePath";
2+
3+
interface Test {
4+
name: string;
5+
input: {
6+
path: string;
7+
format: string;
8+
};
9+
expected: string;
10+
}
11+
12+
const tests: Test[] = [
13+
{
14+
name: "should replace '[hash]' with '*'",
15+
input: {
16+
path: "test.123.chunk.js",
17+
format: "[name].[hash].chunk.js",
18+
},
19+
expected: "test.*.chunk.js",
20+
},
21+
{
22+
name: "should replace '[contenthash]' with '*'",
23+
input: {
24+
path: "test.123.chunk.js",
25+
format: "[name].[contenthash].chunk.js",
26+
},
27+
expected: "test.*.chunk.js",
28+
},
29+
{
30+
name: "should replace '[fullhash]' with '*'",
31+
input: {
32+
path: "test.123.chunk.js",
33+
format: "[name].[fullhash].chunk.js",
34+
},
35+
expected: "test.*.chunk.js",
36+
},
37+
{
38+
name: "should replace '[chunkhash]' with '*'",
39+
input: {
40+
path: "test.123.chunk.js",
41+
format: "[name].[chunkhash].chunk.js",
42+
},
43+
expected: "test.*.chunk.js",
44+
},
45+
{
46+
name: "should replace multiple hash format occurrences '*'",
47+
input: {
48+
path: "test.123.456.chunk.js",
49+
format: "[name].[hash].[chunkhash].chunk.js",
50+
},
51+
expected: "test.*.*.chunk.js",
52+
},
53+
{
54+
name: "should brute force wildcard if no hash format is found",
55+
input: {
56+
path: "test.12345678.chunk.js",
57+
format: "[name].chunk.js",
58+
},
59+
expected: "test.*.chunk.js",
60+
},
61+
];
62+
63+
describe("normalizePath", () => {
64+
it.each(tests)("$name", ({ input, expected }) => {
65+
const expectation = normalizePath(input.path, input.format);
66+
expect(expectation).toEqual(expected);
67+
});
68+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const HASH_REGEX = /[a-f0-9]{8,}/i;
2+
const POTENTIAL_HASHES = [
3+
"[hash]",
4+
"[contenthash]",
5+
"[fullhash]",
6+
"[chunkhash]",
7+
];
8+
9+
const escapeRegex = (string: string): string =>
10+
string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
11+
12+
interface HashMatch {
13+
hashString: string;
14+
hashIndex: number;
15+
}
16+
17+
export const normalizePath = (path: string, format: string): string => {
18+
// grab all potential hashes in the format string
19+
const matches: HashMatch[] = [];
20+
for (const hash of POTENTIAL_HASHES) {
21+
const index = format.indexOf(hash);
22+
if (index !== -1) {
23+
matches.push({ hashString: hash, hashIndex: index });
24+
}
25+
}
26+
27+
let normalizedPath = path;
28+
// loop through all the matches and replace the hash with a wildcard
29+
for (const match of matches) {
30+
// grab the leading delimiter and create a regex group for it
31+
const leadingDelimiter = format.at(match.hashIndex - 1) ?? "";
32+
const leadingRegex = `(?<leadingDelimiter>${escapeRegex(
33+
leadingDelimiter,
34+
)})`;
35+
36+
// grab the ending delimiter and create a regex group for it
37+
const endingDelimiter =
38+
format.at(match.hashIndex + match.hashString.length) ?? "";
39+
const endingRegex = `(?<endingDelimiter>${escapeRegex(endingDelimiter)})`;
40+
41+
// create a regex that will match the hash
42+
const regexString = `(${leadingRegex}(?<hash>[0-9a-f]+)${endingRegex})`;
43+
const HASH_REPLACE_REGEX = new RegExp(regexString, "i");
44+
45+
// replace the hash with a wildcard and the delimiters
46+
normalizedPath = normalizedPath.replace(
47+
HASH_REPLACE_REGEX,
48+
"$<leadingDelimiter>*$<endingDelimiter>",
49+
);
50+
}
51+
52+
// if the path is the same as the normalized path, and the path contains a
53+
// hash, then we can assume that something went wrong and we should just
54+
// replace/brute force the hash with a wildcard
55+
if (normalizedPath === path && HASH_REGEX.test(normalizedPath)) {
56+
return normalizedPath.replace(HASH_REGEX, "*");
57+
}
58+
59+
return normalizedPath;
60+
};

packages/rollup-plugin/src/rollup-bundle-analysis/rollupBundleAnalysisPlugin.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type Module,
66
type BundleAnalysisUploadPlugin,
77
red,
8+
normalizePath,
89
} from "@codecov/bundler-plugin-core";
910

1011
const PLUGIN_NAME = "codecov-rollup-bundle-analysis-plugin";
@@ -31,17 +32,25 @@ export const rollupBundleAnalysisPlugin: BundleAnalysisUploadPlugin = ({
3132
output.bundleName = `${userOptions.bundleName}-${options.name}`;
3233
}
3334

35+
const cwd = process.cwd();
36+
const assets: Asset[] = [];
37+
const chunks: Chunk[] = [];
38+
const moduleByFileName = new Map<string, Module>();
39+
const items = Object.values(bundle);
3440
const customOptions = {
3541
moduleOriginalSize: false,
3642
...options,
3743
};
3844

39-
const assets: Asset[] = [];
40-
const chunks: Chunk[] = [];
41-
const moduleByFileName = new Map<string, Module>();
42-
const items = Object.values(bundle);
45+
let assetFormatString = "";
46+
if (typeof customOptions.assetFileNames === "string") {
47+
assetFormatString = customOptions.assetFileNames;
48+
}
4349

44-
const cwd = process.cwd();
50+
let chunkFormatString = "";
51+
if (typeof customOptions.chunkFileNames === "string") {
52+
chunkFormatString = customOptions.chunkFileNames;
53+
}
4554

4655
let counter = 0;
4756
for (const item of items) {
@@ -53,6 +62,7 @@ export const rollupBundleAnalysisPlugin: BundleAnalysisUploadPlugin = ({
5362
assets.push({
5463
name: fileName,
5564
size: size,
65+
normalized: normalizePath(fileName, assetFormatString),
5666
});
5767
} else {
5868
const fileName = item?.fileName ?? "";
@@ -61,6 +71,7 @@ export const rollupBundleAnalysisPlugin: BundleAnalysisUploadPlugin = ({
6171
assets.push({
6272
name: fileName,
6373
size: size,
74+
normalized: normalizePath(fileName, assetFormatString),
6475
});
6576
}
6677
}
@@ -75,14 +86,15 @@ export const rollupBundleAnalysisPlugin: BundleAnalysisUploadPlugin = ({
7586
assets.push({
7687
name: fileName,
7788
size: size,
89+
normalized: normalizePath(fileName, chunkFormatString),
7890
});
7991

8092
chunks.push({
8193
id: chunkId,
8294
uniqueId: uniqueId,
8395
entry: item?.isEntry,
8496
initial: item?.isDynamicEntry,
85-
files: [item?.fileName],
97+
files: [fileName],
8698
names: [item?.name],
8799
});
88100

@@ -103,17 +115,15 @@ export const rollupBundleAnalysisPlugin: BundleAnalysisUploadPlugin = ({
103115
// if the modules exists append chunk ids to the grabbed module
104116
// else create a new module and create a new entry in the map
105117
if (moduleEntry) {
106-
moduleEntry.chunks.push(chunkId);
107118
moduleEntry.chunkUniqueIds.push(uniqueId);
108119
} else {
109120
const size = customOptions.moduleOriginalSize
110121
? moduleInfo.originalLength
111122
: moduleInfo.renderedLength;
112123

113-
const module = {
124+
const module: Module = {
114125
name: relativeModulePathWithPrefix,
115126
size: size,
116-
chunks: [chunkId],
117127
chunkUniqueIds: [uniqueId],
118128
};
119129

0 commit comments

Comments
 (0)