Skip to content

Commit da035fa

Browse files
committed
feat: integrate unplugin-strip for improved function call stripping and update related build processes
1 parent 723e1ca commit da035fa

6 files changed

Lines changed: 142 additions & 143 deletions

File tree

package-lock.json

Lines changed: 74 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@
104104
"typedoc": "0.28.17",
105105
"typedoc-plugin-mdn-links": "5.1.1",
106106
"typedoc-plugin-missing-exports": "4.1.2",
107-
"typescript": "5.9.3"
107+
"typescript": "5.9.3",
108+
"unplugin-strip": "^0.2.1"
108109
},
109110
"optionalDependencies": {
110111
"canvas": "3.2.1"

utils/esbuild-build-target.mjs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'url';
88

99
import { importValidationPlugin } from './plugins/esbuild-import-validation.mjs';
1010
import {
11-
applyTransforms, buildStripPattern, transformPipelinePlugin
11+
applyTransforms, createStripTransform, transformPipelinePlugin
1212
} from './plugins/esbuild-transform-pipeline.mjs';
1313
import { version, revision } from './rollup-version-revision.mjs';
1414
import { getBanner } from './rollup-get-banner.mjs';
@@ -142,7 +142,8 @@ async function buildBundled({
142142
stripFunctions: !isDebug ? STRIP_FUNCTIONS : null,
143143
processShaders: !isDebug,
144144
dynamicImportLegacy: isUMD,
145-
dynamicImportSuppress: !isUMD
145+
dynamicImportSuppress: !isUMD,
146+
stripComments: !isDebug
146147
})
147148
];
148149

@@ -178,6 +179,8 @@ async function buildBundled({
178179
'(function (root, factory) {',
179180
'\tif (typeof module !== \'undefined\' && module.exports) {',
180181
'\t\tmodule.exports = factory();',
182+
'\t} else if (typeof define === \'function\' && define.amd) {',
183+
'\t\tdefine(factory);',
181184
'\t} else {',
182185
'\t\troot.pc = factory();',
183186
'\t}',
@@ -212,8 +215,8 @@ async function buildUnbundled({
212215
const outDir = `${dir}/${prefix}`;
213216
const effectiveBuildType = buildType === 'min' ? 'release' : buildType;
214217
const jsccConfig = getJSCCConfig(effectiveBuildType);
215-
const stripPattern =
216-
!isDebug ? buildStripPattern(STRIP_FUNCTIONS) : null;
218+
const strip =
219+
!isDebug ? createStripTransform(STRIP_FUNCTIONS) : null;
217220

218221
const srcFiles = collectJSFiles(input);
219222

@@ -223,10 +226,11 @@ async function buildUnbundled({
223226
source = applyTransforms(source, {
224227
jsccValues: jsccConfig.values,
225228
jsccKeepLines: jsccConfig.keepLines,
226-
stripPattern,
229+
strip,
227230
processShaders: !isDebug,
228231
dynamicImportLegacy: false,
229-
dynamicImportSuppress: true
232+
dynamicImportSuppress: true,
233+
stripComments: !isDebug
230234
});
231235

232236
// Rewrite bare 'fflate' import to relative modules path
@@ -243,6 +247,19 @@ async function buildUnbundled({
243247
);
244248
}
245249

250+
// Skip files that have no meaningful content after transforms
251+
if (!isDebug) {
252+
const meaningful = source
253+
.replace(/\/\*[\s\S]*?\*\//g, '')
254+
.replace(/\/\/.*/g, '')
255+
.replace(/^\s*import\s.*$/gm, '')
256+
.replace(/^\s*export\s.*$/gm, '')
257+
.trim();
258+
if (meaningful.length === 0) {
259+
return;
260+
}
261+
}
262+
246263
const destFile = pathJoin(outDir, srcFile);
247264
await fs.promises.mkdir(dirname(destFile), { recursive: true });
248265
await fs.promises.writeFile(destFile, source);

utils/plugins/esbuild-jscc.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ import jscc from 'jscc';
1212
* @returns {string} Processed source.
1313
*/
1414
export function processJSCC(source, values, keepLines) {
15-
const result = jscc(source, null, { values, keepLines, sourceMap: false });
15+
const result = jscc(source, null, { values, keepLines, sourceMap: false, prefixes: ['// '] });
1616
return result.code;
1717
}

utils/plugins/esbuild-strip.mjs

Lines changed: 26 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,33 @@
1-
/**
2-
* Build a regex that matches the start of a strip-target call.
3-
* Matches at line start with optional whitespace and optional label prefix (e.g. `default:`).
4-
* The actual parenthesis matching is done procedurally to avoid catastrophic backtracking.
5-
*
6-
* @param {string[]} functions - Function names to strip.
7-
* @returns {RegExp} Pattern that matches `FuncName(` in strippable positions.
8-
*/
9-
export function buildStripPattern(functions) {
10-
const escaped = functions.map(f => f.replace(/\./g, '\\.')).join('|');
11-
return new RegExp(`^([ \\t]*(?:\\w+:[ \\t]*)?)(${escaped})\\(`, 'gm');
12-
}
1+
import { parse } from 'acorn';
2+
import { unpluginFactory } from 'unplugin-strip';
133

144
/**
15-
* Strip specified function calls from source text.
16-
* Uses parenthesis counting to find the end of the call, avoiding regex backtracking.
5+
* Create a strip transform function for the given function names.
6+
* Uses unplugin-strip (AST-based) to reliably remove function calls
7+
* in all positions — statement-level, inline, inside template literals, etc.
178
*
18-
* @param {string} source - Source code.
19-
* @param {RegExp} pattern - Compiled strip pattern from buildStripPattern.
20-
* @returns {string} Processed source.
9+
* @param {string[]} functions - Function names to strip (e.g. 'Debug.assert').
10+
* @returns {(source: string) => string} Transform function.
2111
*/
22-
export function applyStrip(source, pattern) {
23-
pattern.lastIndex = 0;
24-
25-
const removals = []; // [startIndex, endIndex] pairs to remove
26-
let match;
27-
28-
while ((match = pattern.exec(source)) !== null) {
29-
const linePrefix = match[1]; // leading whitespace + optional label
30-
const funcNameStart = match.index + linePrefix.length;
31-
const parenStart = match.index + match[0].length - 1;
32-
let depth = 1;
33-
let i = parenStart + 1;
34-
let inString = false;
35-
let stringChar = '';
36-
let inTemplate = false;
37-
let templateDepth = 0;
38-
39-
while (i < source.length && depth > 0) {
40-
const ch = source[i];
41-
42-
// Skip single-line comments
43-
if (ch === '/' && i + 1 < source.length && source[i + 1] === '/') {
44-
while (i < source.length && source[i] !== '\n') i++;
45-
continue;
46-
}
47-
48-
// Skip multi-line comments
49-
if (ch === '/' && i + 1 < source.length && source[i + 1] === '*') {
50-
i += 2;
51-
while (i < source.length - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++;
52-
i += 2;
53-
continue;
54-
}
55-
56-
if (inString) {
57-
if (ch === stringChar && source[i - 1] !== '\\') {
58-
inString = false;
59-
}
60-
i++;
61-
continue;
62-
}
63-
64-
if (inTemplate) {
65-
if (ch === '`' && source[i - 1] !== '\\') {
66-
inTemplate = false;
67-
} else if (ch === '$' && i + 1 < source.length && source[i + 1] === '{') {
68-
templateDepth++;
69-
i += 2;
70-
continue;
71-
} else if (ch === '}' && templateDepth > 0) {
72-
templateDepth--;
73-
}
74-
i++;
75-
continue;
76-
}
77-
78-
if (ch === '\'' || ch === '"') {
79-
inString = true;
80-
stringChar = ch;
81-
} else if (ch === '`') {
82-
inTemplate = true;
83-
templateDepth = 0;
84-
} else if (ch === '(') {
85-
depth++;
86-
} else if (ch === ')') {
87-
depth--;
88-
}
89-
90-
i++;
12+
export function createStripTransform(functions) {
13+
const plugin = unpluginFactory({
14+
functions,
15+
sourceMap: false,
16+
debugger: false,
17+
include: '**/*.js'
18+
});
19+
20+
const ctx = {
21+
parse(code) {
22+
return parse(code, {
23+
ecmaVersion: 'latest',
24+
sourceType: 'module'
25+
});
9126
}
27+
};
9228

93-
if (depth === 0) {
94-
// i is now right after the closing paren
95-
let end = i;
96-
// Skip optional semicolon and trailing whitespace/newline
97-
while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++;
98-
if (end < source.length && source[end] === ';') end++;
99-
while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++;
100-
if (end < source.length && source[end] === '\n') end++;
101-
else if (end < source.length && source[end] === '\r') {
102-
end++;
103-
if (end < source.length && source[end] === '\n') end++;
104-
}
105-
106-
// The match starts at the beginning of the line (anchored with ^).
107-
// If the prefix is only whitespace, remove the entire line.
108-
// If the prefix includes a label (e.g. "default: "), remove only the call.
109-
const start = match.index;
110-
if (/^[ \t]*$/.test(linePrefix)) {
111-
removals.push([start, end]);
112-
} else {
113-
removals.push([funcNameStart, end]);
114-
}
115-
}
116-
}
117-
118-
if (removals.length === 0) return source;
119-
120-
// Build result by skipping removed ranges, handling overlapping/nested ranges
121-
let result = '';
122-
let pos = 0;
123-
for (const [start, end] of removals) {
124-
if (start < pos) continue; // skip ranges nested inside an already-removed range
125-
result += source.slice(pos, start);
126-
pos = end;
127-
}
128-
result += source.slice(pos);
129-
return result;
29+
return function applyStrip(source) {
30+
const result = plugin.transform.call(ctx, source, 'file.js');
31+
return result ? result.code : source;
32+
};
13033
}

0 commit comments

Comments
 (0)