Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions build/next/private-to-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function convertPrivateFields(code: string, filename: string): ConvertPri
lastEnd = edit.end;
}
parts.push(code.substring(lastEnd));
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };

// --- AST walking ---

Expand Down Expand Up @@ -209,10 +209,15 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
if (ts.isPrivateIdentifier(child)) {
const resolved = resolvePrivateName(child.text);
if (resolved !== undefined) {
const start = child.getStart(sourceFile);
edits.push({
start: child.getStart(sourceFile),
start,
end: child.getEnd(),
newText: resolved
// In minified code, `async#run()` has no space before `#`.
// The `#` naturally starts a new token, but `$` does not —
// `async$a` would fuse into one identifier. Insert a space
// when the preceding character is an identifier character.
newText: (start > 0 && isIdentifierChar(code.charCodeAt(start - 1))) ? ' ' + resolved : resolved
});
}
return;
Expand All @@ -234,6 +239,11 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
}
}

function isIdentifierChar(ch: number): boolean {
// a-z, A-Z, 0-9, _, $
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || (ch >= 48 && ch <= 57) || ch === 95 || ch === 36;
}

/**
* Adjusts a source map to account for text edits applied to the generated JS.
*
Expand Down
48 changes: 48 additions & 0 deletions build/next/test/private-to-property.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,54 @@ suite('convertPrivateFields', () => {
assert.deepStrictEqual(result.edits, []);
});

test('async private method — replacement must not merge with async keyword', async () => {
// In minified output, there is no space between `async` and `#method`:
// class Foo{async#run(){await Promise.resolve(1)}}
// Replacing `#run` with `$a` naively produces `async$a()` which is a
// single identifier, not `async $a()`. The `await` inside then becomes
// invalid because the method is no longer async.
const code = 'class Foo{async#run(){return await Promise.resolve(1)}call(){return this.#run()}}';
const result = convertPrivateFields(code, 'test.js');
assert.ok(!result.code.includes('#run'), 'should replace #run');
// The replacement must NOT fuse with `async` into a single token
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
// Verify it actually executes (the async method should still work)
const exec = new Function(`
${result.code}
return new Foo().call();
`);
const val = await exec();
assert.strictEqual(val, 1);
});

test('async private method — space inserted in declaration and not in usage', () => {
// More readable version: ensure that `async #method()` becomes
// `async $a()` (with space), while `this.#method()` becomes
// `this.$a()` (no extra space needed since `.` separates tokens).
const code = [
'class Foo {',
' async #doWork() { return await 42; }',
' run() { return this.#doWork(); }',
'}',
].join('\n');
const result = convertPrivateFields(code, 'test.js');
assert.ok(!result.code.includes('#doWork'), 'should replace #doWork');
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
});

test('static async private method — no token fusion', async () => {
const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}';
const result = convertPrivateFields(code, 'test.js');
assert.doesNotThrow(() => new Function(result.code),
'static async private method must produce valid JS, got:\n' + result.code);
const exec = new Function(`
${result.code}
return Foo.go();
`);
const value = await exec();
assert.strictEqual(value, 1);
});

test('heritage clause — extends expression resolves outer private field, not inner', () => {
const code = [
'class Outer {',
Expand Down
Loading
Loading