Skip to content

Commit 4785269

Browse files
authored
Merge pull request #168 from z0ffy/feature/string-array
feature/add stringArray
2 parents eba6f60 + aa0dcb5 commit 4785269

File tree

6 files changed

+254
-16
lines changed

6 files changed

+254
-16
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ JavaScript `obfuscator` plugin for `Vite` environments
5252

5353
## ⚠️ Notice
5454

55-
- If the obfuscation option `stringArray` is `true`.
56-
- Your results may lose some bundles (in `__vite__mapDeps` array).
57-
- I'm looking for an accurate case.
5855
- If a memory overflow occurs, modify the packaging command to
5956
`"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build"`, where `max-old-space-size` is set according
6057
to the configuration.
@@ -121,8 +118,9 @@ const allObfuscatorConfig = {
121118
selfDefending: true,
122119
simplify: true,
123120
splitStrings: false,
124-
stringArray: false,
125-
stringArrayCallsTransform: false,
121+
ignoreImports: true,
122+
stringArray: true,
123+
stringArrayCallsTransform: true,
126124
stringArrayCallsTransformThreshold: 0.5,
127125
stringArrayEncoding: [],
128126
stringArrayIndexShift: true,

README.zh-CN.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@
5252

5353
## ⚠️ 注意
5454

55-
- 如果混淆选项`stringArray``true`
56-
- 您的结果可能会丢失一些捆绑包(在`__vite__mapDeps`数组中)。
57-
- 我正在寻找一个准确的案例。
5855
- 如果遇到内存溢出,修改打包命令为`"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build"`,
5956
`max-old-space-size`的值根据配置自行设置。
6057
- 在设置`node_modules`分包时,请把准确的包名前置。例如:["vue-router", "vue"]`"vue"`可以同时匹配到`vue`以及`vue-router`
@@ -119,8 +116,9 @@ const allObfuscatorConfig = {
119116
selfDefending: true,
120117
simplify: true,
121118
splitStrings: false,
122-
stringArray: false,
123-
stringArrayCallsTransform: false,
119+
ignoreImports: true,
120+
stringArray: true,
121+
stringArrayCallsTransform: true,
124122
stringArrayCallsTransformThreshold: 0.5,
125123
stringArrayEncoding: [],
126124
stringArrayIndexShift: true,

src/__tests__/plugin.spec.ts

Lines changed: 242 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,44 @@
1-
import {expect, describe, it, vi} from "vitest";
2-
import {formatTime, getThreadPoolSize, getValidBundleList, Log} from "../utils";
1+
import {expect, describe, it, vi, beforeEach, afterEach} from "vitest";
2+
import type { Rollup } from 'vite';
3+
4+
vi.mock('node:worker_threads');
5+
6+
import {
7+
formatTime,
8+
getThreadPoolSize,
9+
getValidBundleList,
10+
Log,
11+
isEnabledFeature,
12+
isEnableThreadPool,
13+
isEnableAutoExcludesNodeModules,
14+
getChunkName,
15+
CodeSizeAnalyzer,
16+
obfuscateBundle,
17+
createWorkerTask
18+
} from "../utils";
319
import {BundleList, Config} from "../type";
20+
import {isArray, isFunction, isFileNameExcluded} from '../utils/is';
21+
import { Worker } from 'node:worker_threads';
22+
23+
// Mock WORKER_FILE_PATH
24+
vi.stubGlobal('WORKER_FILE_PATH', './worker.js');
25+
26+
// Mock javascript-obfuscator
27+
vi.mock('javascript-obfuscator', () => ({
28+
default: {
29+
obfuscate: () => ({
30+
getObfuscatedCode: () => 'obfuscated code'
31+
})
32+
}
33+
}));
34+
35+
vi.mock('node:worker_threads', () => ({
36+
Worker: vi.fn(() => ({
37+
postMessage: vi.fn(),
38+
on: vi.fn(),
39+
unref: vi.fn()
40+
}))
41+
}));
442

543
const defaultConfig: Config = {
644
enable: true,
@@ -148,3 +186,205 @@ describe('getValidBundleList', () => {
148186
expect(result).toEqual(expected);
149187
});
150188
});
189+
190+
describe('isEnabledFeature', () => {
191+
it('should return boolean value directly when input is boolean', () => {
192+
expect(isEnabledFeature(true)).toBe(true);
193+
expect(isEnabledFeature(false)).toBe(false);
194+
});
195+
196+
it('should return enable property value when input is object', () => {
197+
expect(isEnabledFeature({ enable: true })).toBe(true);
198+
expect(isEnabledFeature({ enable: false })).toBe(false);
199+
});
200+
201+
it('should return false for invalid input', () => {
202+
expect(isEnabledFeature({} as any)).toBe(false);
203+
expect(isEnabledFeature(null as any)).toBe(false);
204+
expect(isEnabledFeature(undefined as any)).toBe(false);
205+
});
206+
});
207+
208+
describe('isEnableThreadPool', () => {
209+
it('should correctly determine thread pool status', () => {
210+
expect(isEnableThreadPool({ ...defaultConfig, threadPool: true })).toBe(true);
211+
expect(isEnableThreadPool({ ...defaultConfig, threadPool: false })).toBe(false);
212+
expect(isEnableThreadPool({ ...defaultConfig, threadPool: { enable: true, size: 4 } })).toBe(true);
213+
expect(isEnableThreadPool({ ...defaultConfig, threadPool: { enable: false } })).toBe(false);
214+
});
215+
});
216+
217+
describe('isEnableAutoExcludesNodeModules', () => {
218+
it('should correctly determine node modules exclusion status', () => {
219+
expect(isEnableAutoExcludesNodeModules({ ...defaultConfig, autoExcludeNodeModules: true })).toBe(true);
220+
expect(isEnableAutoExcludesNodeModules({ ...defaultConfig, autoExcludeNodeModules: false })).toBe(false);
221+
expect(isEnableAutoExcludesNodeModules({ ...defaultConfig, autoExcludeNodeModules: { enable: true, manualChunks: [] } })).toBe(true);
222+
expect(isEnableAutoExcludesNodeModules({ ...defaultConfig, autoExcludeNodeModules: { enable: false } })).toBe(false);
223+
});
224+
});
225+
226+
describe('getChunkName', () => {
227+
it('should return modified chunk name when id includes chunk name', () => {
228+
const manualChunks = ['react', 'vue'];
229+
expect(getChunkName('node_modules/react/index.js', manualChunks)).toBe('vendor-react');
230+
expect(getChunkName('node_modules/vue/dist/vue.js', manualChunks)).toBe('vendor-vue');
231+
});
232+
233+
it('should return vendor modules when no match found', () => {
234+
const manualChunks = ['react', 'vue'];
235+
expect(getChunkName('node_modules/lodash/index.js', manualChunks)).toBe('vendor-modules');
236+
});
237+
});
238+
239+
describe('CodeSizeAnalyzer', () => {
240+
let analyzer: CodeSizeAnalyzer;
241+
const mockLog = new Log(true);
242+
let logSpy: any;
243+
244+
beforeEach(() => {
245+
analyzer = new CodeSizeAnalyzer(mockLog);
246+
logSpy = vi.spyOn(console, 'log');
247+
// Mock performance.now() to return predictable values
248+
let currentTime = 0;
249+
vi.spyOn(performance, 'now').mockImplementation(() => {
250+
currentTime += 1000;
251+
return currentTime;
252+
});
253+
});
254+
255+
afterEach(() => {
256+
vi.restoreAllMocks();
257+
});
258+
259+
it('should analyze bundle size correctly', () => {
260+
const originalBundle: BundleList = [
261+
['test.js', { code: 'console.log("test");' } as any]
262+
];
263+
264+
const obfuscatedBundle: BundleList = [
265+
['test.js', { code: 'var _0x123456=function(){console.log("test")};' } as any]
266+
];
267+
268+
analyzer.start(originalBundle);
269+
analyzer.end(obfuscatedBundle);
270+
271+
const lastCallArgs = logSpy.mock.lastCall;
272+
expect(lastCallArgs[0]).toBe('\x1b[32m%s\x1b[0m');
273+
expect(lastCallArgs[1]).toMatch(/obfuscated in \ds/);
274+
});
275+
276+
it('should handle empty bundles', () => {
277+
const emptyBundle: BundleList = [];
278+
279+
analyzer.start(emptyBundle);
280+
analyzer.end(emptyBundle);
281+
282+
const lastCallArgs = logSpy.mock.lastCall;
283+
expect(lastCallArgs[0]).toBe('\x1b[32m%s\x1b[0m');
284+
expect(lastCallArgs[1]).toContain('0B');
285+
});
286+
287+
it('should format sizes with appropriate units', () => {
288+
const largeBundle: BundleList = [
289+
['test.js', { code: 'a'.repeat(1024 * 1024) } as any]
290+
];
291+
292+
analyzer.start(largeBundle);
293+
analyzer.end(largeBundle);
294+
295+
const lastCallArgs = logSpy.mock.lastCall;
296+
expect(lastCallArgs[0]).toBe('\x1b[32m%s\x1b[0m');
297+
expect(lastCallArgs[1]).toContain('MB');
298+
});
299+
});
300+
301+
describe('is utils', () => {
302+
describe('isArray', () => {
303+
it('should correctly identify arrays', () => {
304+
expect(isArray([])).toBe(true);
305+
expect(isArray([1, 2, 3])).toBe(true);
306+
expect(isArray(new Array())).toBe(true);
307+
expect(isArray(null)).toBe(false);
308+
expect(isArray(undefined)).toBe(false);
309+
expect(isArray({})).toBe(false);
310+
expect(isArray('array')).toBe(false);
311+
});
312+
});
313+
314+
describe('isFunction', () => {
315+
it('should correctly identify functions', () => {
316+
expect(isFunction(() => {})).toBe(true);
317+
expect(isFunction(function() {})).toBe(true);
318+
expect(isFunction(async () => {})).toBe(true);
319+
expect(isFunction(null)).toBe(false);
320+
expect(isFunction(undefined)).toBe(false);
321+
expect(isFunction({})).toBe(false);
322+
expect(isFunction('function')).toBe(false);
323+
});
324+
});
325+
326+
describe('isFileNameExcluded', () => {
327+
it('should handle RegExp excludes correctly', () => {
328+
const excludes = [/\.test\.js$/, 'vendor'];
329+
expect(isFileNameExcluded('app.test.js', excludes)).toBe(true);
330+
expect(isFileNameExcluded('app.js', excludes)).toBe(false);
331+
});
332+
333+
it('should handle string excludes correctly', () => {
334+
const excludes = ['vendor', '.min.js'];
335+
expect(isFileNameExcluded('vendor/lib.js', excludes)).toBe(true);
336+
expect(isFileNameExcluded('app.min.js', excludes)).toBe(true);
337+
expect(isFileNameExcluded('app.js', excludes)).toBe(false);
338+
});
339+
340+
it('should handle mixed excludes correctly', () => {
341+
const excludes = [/\.spec\.js$/, 'test'];
342+
expect(isFileNameExcluded('app.spec.js', excludes)).toBe(true);
343+
expect(isFileNameExcluded('test/app.js', excludes)).toBe(true);
344+
expect(isFileNameExcluded('app.js', excludes)).toBe(false);
345+
});
346+
});
347+
});
348+
349+
describe('obfuscateBundle', () => {
350+
it('should obfuscate bundle and log progress', () => {
351+
const finalConfig: Config = {
352+
...defaultConfig,
353+
log: true
354+
};
355+
const fileName = 'test.js';
356+
const bundleItem = {
357+
code: 'console.log("test")'
358+
} as Rollup.OutputChunk;
359+
360+
const logSpy = vi.spyOn(console, 'log');
361+
362+
const result = obfuscateBundle(finalConfig, fileName, bundleItem);
363+
364+
expect(result).toBe('obfuscated code');
365+
expect(logSpy).toHaveBeenCalledWith('obfuscating test.js...');
366+
expect(logSpy).toHaveBeenCalledWith('obfuscation complete for test.js.');
367+
});
368+
});
369+
370+
describe('createWorkerTask', () => {
371+
it('should call worker methods properly', () => {
372+
const finalConfig: Config = {
373+
...defaultConfig
374+
};
375+
const chunk: BundleList = [
376+
['test.js', { code: 'console.log("test")' } as Rollup.OutputChunk]
377+
];
378+
379+
createWorkerTask(finalConfig, chunk);
380+
381+
expect(Worker).toHaveBeenCalled();
382+
383+
const mockWorkerInstance = vi.mocked(Worker).mock.results[0].value;
384+
385+
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith({ config: finalConfig, chunk });
386+
387+
expect(mockWorkerInstance.on).toHaveBeenCalledWith('message', expect.any(Function));
388+
expect(mockWorkerInstance.on).toHaveBeenCalledWith('error', expect.any(Function));
389+
});
390+
});

src/utils/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export const defaultConfig: Readonly<Config> = {
2222
selfDefending: true,
2323
simplify: true,
2424
splitStrings: false,
25-
stringArray: false,
26-
stringArrayCallsTransform: false,
25+
ignoreImports: true,
26+
stringArray: true,
27+
stringArrayCallsTransform: true,
2728
stringArrayCallsTransformThreshold: 0.5,
2829
stringArrayEncoding: [],
2930
stringArrayIndexShift: true,

src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function formatTime(ms: number): string {
4747

4848
export function isEnabledFeature(featureConfig: boolean | { enable: boolean }): boolean {
4949
if (isBoolean(featureConfig)) return featureConfig;
50-
if (isObject(featureConfig)) return featureConfig.enable;
50+
if (isObject(featureConfig) && 'enable' in featureConfig) return featureConfig.enable;
5151
return false;
5252
}
5353

src/utils/is.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export function isArray(input: any): input is any[] {
1515
}
1616

1717
export function isFunction(input: any): input is Function {
18-
return Object.prototype.toString.call(input) === '[object Function]';
18+
const type = Object.prototype.toString.call(input);
19+
return type === '[object Function]' || type === '[object AsyncFunction]';
1920
}
2021

2122
export function isBoolean(input: any): input is boolean {

0 commit comments

Comments
 (0)