Skip to content

Commit 8635cc0

Browse files
authored
Merge pull request #231 from z0ffy/feature/support-nuxt
feat: add support for Nuxt.js
2 parents 011ae73 + 2baa849 commit 8635cc0

File tree

7 files changed

+162
-57
lines changed

7 files changed

+162
-57
lines changed

src/__tests__/plugin.spec.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ vi.stubGlobal('WORKER_FILE_PATH', './worker.js');
2727
vi.mock('javascript-obfuscator', () => ({
2828
default: {
2929
obfuscate: () => ({
30-
getObfuscatedCode: () => 'obfuscated code'
30+
getObfuscatedCode: () => 'obfuscated code',
31+
getSourceMap: () => JSON.stringify({ version: 3, sources: [], names: [], mappings: '' })
3132
})
3233
}
3334
}));
@@ -361,13 +362,19 @@ describe('obfuscateBundle', () => {
361362

362363
const result = obfuscateBundle(finalConfig, fileName, bundleItem);
363364

364-
expect(result).toBe('obfuscated code');
365+
expect(result.code).toBe('obfuscated code');
365366
expect(logSpy).toHaveBeenCalledWith('obfuscating test.js...');
366367
expect(logSpy).toHaveBeenCalledWith('obfuscation complete for test.js.');
367368
});
368369
});
369370

371+
import { ObfuscatedFilesRegistry } from '../utils';
372+
370373
describe('createWorkerTask', () => {
374+
beforeEach(() => {
375+
ObfuscatedFilesRegistry.getInstance().clear();
376+
});
377+
371378
it('should call worker methods properly', () => {
372379
const finalConfig: Config = {
373380
...defaultConfig
@@ -382,7 +389,7 @@ describe('createWorkerTask', () => {
382389

383390
const mockWorkerInstance = vi.mocked(Worker).mock.results[0].value;
384391

385-
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith({ config: finalConfig, chunk });
392+
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith({ config: finalConfig, chunk, registryState: [] });
386393

387394
expect(mockWorkerInstance.on).toHaveBeenCalledWith('message', expect.any(Function));
388395
expect(mockWorkerInstance.on).toHaveBeenCalledWith('error', expect.any(Function));

src/index.ts

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IndexHtmlTransformHook, PluginOption, Rollup } from 'vite';
1+
import type { PluginOption, Rollup, ResolvedConfig, IndexHtmlTransformHook } from 'vite';
22
import { BundleList, Config, ViteConfigFn } from './type';
33
import {
44
CodeSizeAnalyzer,
@@ -15,18 +15,52 @@ import {
1515
obfuscateBundle,
1616
obfuscateLibBundle,
1717
} from './utils';
18-
import { isArray, isFunction, isObject } from './utils/is';
18+
import { isArray, isFunction, isLibMode, isNuxtProject, isObject } from './utils/is';
1919
import { defaultConfig, LOG_COLOR, NODE_MODULES, VENDOR_MODULES } from './utils/constants';
2020

2121
export default function viteBundleObfuscator(config?: Partial<Config>): PluginOption {
2222
const finalConfig = { ...defaultConfig, ...config };
2323
const _log = new Log(finalConfig.log);
24-
let isLibMode = false;
24+
let _isLibMode = false;
25+
let _isNuxtProject = false;
26+
let _isSsrBuild = false;
2527

26-
const modifyConfigHandler: ViteConfigFn = (config) => {
27-
isLibMode = !!config.build?.lib;
28+
const obfuscateAllChunks = async (bundle: Rollup.OutputBundle) => {
29+
_log.forceLog(LOG_COLOR.info + '\nstarting obfuscation process...');
30+
const analyzer = new CodeSizeAnalyzer(_log);
31+
const bundleList = getValidBundleList(finalConfig, bundle);
32+
analyzer.start(bundleList);
33+
34+
if (isEnableThreadPool(finalConfig)) {
35+
const poolSize = Math.min(getThreadPoolSize(finalConfig), bundleList.length);
36+
const chunkSize = Math.ceil(bundleList.length / poolSize);
37+
const workerPromises = [];
2838

29-
if (!finalConfig.enable || !isEnableAutoExcludesNodeModules(finalConfig) || isLibMode) return;
39+
for (let i = 0; i < poolSize; i++) {
40+
const chunk = bundleList.slice(i * chunkSize, (i + 1) * chunkSize);
41+
workerPromises.push(createWorkerTask(finalConfig, chunk));
42+
}
43+
44+
await Promise.all(workerPromises);
45+
} else {
46+
bundleList.forEach(([fileName, bundleItem]) => {
47+
const { code, map } = obfuscateBundle(finalConfig, fileName, bundleItem);
48+
bundleItem.code = code;
49+
bundleItem.map = map as any;
50+
});
51+
}
52+
53+
analyzer.end(bundleList);
54+
};
55+
56+
const modifyConfigHandler: ViteConfigFn = (config, env) => {
57+
_isSsrBuild = !!env.isSsrBuild;
58+
_isLibMode = isLibMode(config);
59+
_isNuxtProject = isNuxtProject(config);
60+
61+
if (!finalConfig.enable || !isEnableAutoExcludesNodeModules(finalConfig) || _isLibMode || _isNuxtProject) {
62+
return;
63+
}
3064

3165
config.build = config.build || {};
3266
config.build.rollupOptions = config.build.rollupOptions || {};
@@ -71,38 +105,30 @@ export default function viteBundleObfuscator(config?: Partial<Config>): PluginOp
71105
}
72106
};
73107

74-
const transformIndexHtmlHandler: IndexHtmlTransformHook = async (html, { bundle }) => {
75-
if (!finalConfig.enable || !bundle) return html;
76-
77-
_log.forceLog(LOG_COLOR.info + '\nstarting obfuscation process...');
78-
const analyzer = new CodeSizeAnalyzer(_log);
79-
const bundleList = getValidBundleList(finalConfig, bundle);
80-
analyzer.start(bundleList);
81-
82-
if (isEnableThreadPool(finalConfig)) {
83-
const poolSize = Math.min(getThreadPoolSize(finalConfig), bundleList.length);
84-
const chunkSize = Math.ceil(bundleList.length / poolSize);
85-
const workerPromises = [];
86-
87-
for (let i = 0; i < poolSize; i++) {
88-
const chunk = bundleList.slice(i * chunkSize, (i + 1) * chunkSize);
89-
workerPromises.push(createWorkerTask(finalConfig, chunk));
90-
}
91-
92-
await Promise.all(workerPromises);
93-
} else {
94-
bundleList.forEach(([fileName, bundleItem]) => {
95-
bundleItem.code = obfuscateBundle(finalConfig, fileName, bundleItem);
96-
});
108+
const configResolvedHandler: (resolvedConfig: ResolvedConfig) => void | Promise<void> = (resolvedConfig) => {
109+
const sourcemap = resolvedConfig.build.sourcemap;
110+
if (sourcemap) {
111+
finalConfig.options = {
112+
...finalConfig.options,
113+
sourceMap: true,
114+
sourceMapMode: sourcemap === 'inline' ? 'inline' : 'separate',
115+
};
97116
}
117+
};
98118

99-
analyzer.end(bundleList);
100-
119+
const transformIndexHtmlHandler: IndexHtmlTransformHook = async (html, { bundle }) => {
120+
if (!finalConfig.enable || !bundle || _isNuxtProject || _isSsrBuild) return html;
121+
await obfuscateAllChunks(bundle);
101122
return html;
102123
};
103124

104-
const renderChunkHandler: Rollup.RenderChunkHook = (code: string, chunk: Rollup.RenderedChunk) => {
105-
if (!finalConfig.enable || !isLibMode) return code;
125+
const generateBundleHandler: Rollup.Plugin['generateBundle'] = async (_, bundle) => {
126+
if (!finalConfig.enable || !bundle || _isLibMode || !_isNuxtProject || _isSsrBuild) return;
127+
await obfuscateAllChunks(bundle);
128+
};
129+
130+
const renderChunkHandler: Rollup.RenderChunkHook = (code, chunk) => {
131+
if (!finalConfig.enable || !_isLibMode || _isSsrBuild) return null;
106132

107133
const analyzer = new CodeSizeAnalyzer(_log);
108134
const bundleList = [[chunk.name, { code }]] as BundleList;
@@ -137,7 +163,9 @@ export default function viteBundleObfuscator(config?: Partial<Config>): PluginOp
137163
name: 'vite-plugin-bundle-obfuscator',
138164
apply: finalConfig.apply,
139165
config: modifyConfigHandler,
166+
configResolved: configResolvedHandler,
140167
renderChunk: renderChunkHandler,
141168
transformIndexHtml: getTransformIndexHtml(),
169+
generateBundle: generateBundleHandler,
142170
};
143171
}

src/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface FormatSizeResult {
2727
export interface ObfuscationResult {
2828
fileName: string;
2929
obfuscatedCode: string;
30+
map?: Rollup.SourceMapInput;
3031
}
3132

3233
export interface Config {

src/utils/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,23 +151,35 @@ export class ObfuscatedFilesRegistry {
151151
}
152152
}
153153

154-
export function obfuscateBundle(finalConfig: Config, fileName: string, bundleItem: Rollup.OutputChunk): string {
154+
export function obfuscateBundle(finalConfig: Config, fileName: string, bundleItem: Rollup.OutputChunk): { code: string; map: Rollup.SourceMapInput } {
155155
const _log = new Log(finalConfig.log);
156156
const registry = ObfuscatedFilesRegistry.getInstance();
157157

158158
if (registry.isObfuscated(fileName)) {
159159
_log.info(`skipping ${fileName} (already in obfuscated registry)`);
160-
return bundleItem.code;
160+
return { code: bundleItem.code, map: bundleItem.map };
161161
}
162162

163163
_log.info(`obfuscating ${fileName}...`);
164-
const obfuscatedCode = javascriptObfuscator.obfuscate(bundleItem.code, finalConfig.options).getObfuscatedCode();
164+
const fileSpecificOptions = finalConfig.options.sourceMap
165+
? {
166+
...finalConfig.options,
167+
inputFileName: fileName,
168+
sourceMapFileName: `${fileName}.map`,
169+
}
170+
: finalConfig.options;
171+
const obfuscationResult = javascriptObfuscator.obfuscate(bundleItem.code, fileSpecificOptions);
165172
_log.info(`obfuscation complete for ${fileName}.`);
166173

167174
registry.markAsObfuscated(fileName);
168175
_log.info(`added ${fileName} to obfuscated files registry`);
169176

170-
return obfuscatedCode;
177+
const sourceMap = obfuscationResult.getSourceMap();
178+
179+
return {
180+
code: obfuscationResult.getObfuscatedCode(),
181+
map: sourceMap ? JSON.parse(sourceMap) : null,
182+
};
171183
}
172184

173185
export function obfuscateLibBundle(finalConfig: Config, fileName: string, code: string): { code: string; map: Rollup.SourceMapInput } {
@@ -180,15 +192,22 @@ export function obfuscateLibBundle(finalConfig: Config, fileName: string, code:
180192
}
181193

182194
_log.info(`obfuscating ${fileName}...`);
183-
const obfuscated = javascriptObfuscator.obfuscate(code, finalConfig.options);
195+
const fileSpecificOptions = finalConfig.options.sourceMap
196+
? {
197+
...finalConfig.options,
198+
inputFileName: fileName,
199+
sourceMapFileName: `${fileName}.map`,
200+
}
201+
: finalConfig.options;
202+
const obfuscated = javascriptObfuscator.obfuscate(code, fileSpecificOptions);
184203
_log.info(`obfuscation complete for ${fileName}.`);
185204

186205
registry.markAsObfuscated(fileName);
187206
_log.info(`added ${fileName} to obfuscated files registry`);
188207

189208
return {
190209
code: obfuscated.getObfuscatedCode(),
191-
map: obfuscated.getSourceMap(),
210+
map: JSON.parse(obfuscated.getSourceMap() || 'null'),
192211
};
193212
}
194213

@@ -209,6 +228,7 @@ export function createWorkerTask(finalConfig: Config, chunk: BundleList) {
209228
const result = value.results.find((i: ObfuscationResult) => i.fileName === fileName);
210229
if (result && result.obfuscatedCode) {
211230
bundleItem.code = result.obfuscatedCode;
231+
bundleItem.map = result.map || null;
212232
}
213233
});
214234
}

src/utils/is.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,82 @@
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
4+
const { toString } = Object.prototype;
5+
16
export function isRegExp(input: any): input is RegExp {
2-
return Object.prototype.toString.call(input) === '[object RegExp]';
7+
return toString.call(input) === '[object RegExp]';
38
}
49

510
export function isString(input: any): input is string {
6-
return Object.prototype.toString.call(input) === '[object String]';
11+
return toString.call(input) === '[object String]';
712
}
813

914
export function isObject(input: any): input is object {
10-
return Object.prototype.toString.call(input) === '[object Object]';
15+
return toString.call(input) === '[object Object]';
1116
}
1217

1318
export function isArray(input: any): input is any[] {
1419
return Array.isArray(input);
1520
}
1621

1722
export function isFunction(input: any): input is Function {
18-
const type = Object.prototype.toString.call(input);
23+
const type = toString.call(input);
1924
return type === '[object Function]' || type === '[object AsyncFunction]';
2025
}
2126

2227
export function isBoolean(input: any): input is boolean {
23-
return Object.prototype.toString.call(input) === '[object Boolean]';
28+
return toString.call(input) === '[object Boolean]';
2429
}
2530

2631
export function isFileNameExcluded(fileName: string, excludes: (RegExp | string)[] | RegExp | string): boolean {
2732
if (!excludes) return false;
2833

29-
if (Array.isArray(excludes)) {
30-
return excludes.some((exclude) => {
34+
if (isArray(excludes)) {
35+
return (excludes as (RegExp | string)[]).some((exclude) => {
3136
if (isString(exclude)) {
32-
return fileName.includes(exclude);
37+
return fileName.includes(exclude as string);
3338
} else if (isRegExp(exclude)) {
34-
return exclude.test(fileName);
39+
return (exclude as RegExp).test(fileName);
3540
}
3641
return false;
3742
});
38-
} else if (isString(excludes)) {
39-
return fileName.includes(excludes);
40-
} else if (isRegExp(excludes)) {
41-
return excludes.test(fileName);
43+
}
44+
45+
if (isString(excludes)) {
46+
return fileName.includes(excludes as string);
47+
}
48+
49+
if (isRegExp(excludes)) {
50+
return (excludes as RegExp).test(fileName);
4251
}
4352

4453
return false;
4554
}
55+
56+
export function isLibMode(config: { build?: { lib?: any } }): boolean {
57+
return !!config.build?.lib;
58+
}
59+
60+
export function isNuxtProject(config: { root?: string }): boolean {
61+
const root = config.root || process.cwd();
62+
const packageJsonPath = resolve(root, 'package.json');
63+
64+
if (existsSync(packageJsonPath)) {
65+
try {
66+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
67+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
68+
if (dependencies.nuxt) return true;
69+
} catch {
70+
/* empty */
71+
}
72+
}
73+
74+
const nuxtPaths = [
75+
resolve(root, 'nuxt.config.js'),
76+
resolve(root, 'nuxt.config.ts'),
77+
resolve(root, '.nuxt'),
78+
resolve(root, '.output'),
79+
];
80+
81+
return nuxtPaths.some(path => existsSync(path));
82+
}

src/worker/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { parentPort } from 'node:worker_threads';
2-
import javascriptObfuscator from 'javascript-obfuscator';
2+
import javascriptObfuscator, { ObfuscatorOptions } from 'javascript-obfuscator';
33
import { Log, ObfuscatedFilesRegistry } from '../utils';
44
import type { ObfuscationResult, WorkerMessage } from '../type';
55

@@ -24,7 +24,14 @@ if (parentPort) {
2424
}
2525

2626
_log.info(`worker obfuscating ${fileName}...`);
27-
const obfuscated = javascriptObfuscator.obfuscate(bundleItem.code, message.config.options);
27+
const fileSpecificOptions: ObfuscatorOptions = message.config.options.sourceMap
28+
? {
29+
...message.config.options,
30+
inputFileName: fileName,
31+
sourceMapFileName: `${fileName}.map`,
32+
}
33+
: message.config.options;
34+
const obfuscated = javascriptObfuscator.obfuscate(bundleItem.code, fileSpecificOptions);
2835
_log.info(`worker obfuscation complete for ${fileName}.`);
2936

3037
registry.markAsObfuscated(fileName);
@@ -33,6 +40,7 @@ if (parentPort) {
3340
results.push({
3441
fileName,
3542
obfuscatedCode: obfuscated.getObfuscatedCode(),
43+
map: JSON.parse(obfuscated.getSourceMap() || 'null'),
3644
});
3745
}
3846

tsup.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const baseConfig: Options = {
1414
sourcemap: false,
1515
clean: true,
1616
minify: true,
17+
esbuildOptions(options) {
18+
options.legalComments = 'none';
19+
return options;
20+
},
1721
};
1822

1923
export default defineConfig([

0 commit comments

Comments
 (0)