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
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
* found in the LICENSE file at https://angular.dev/license
*/

import type { PluginObj } from '@babel/core';
import type { NodePath, PluginObj, PluginPass, types } from '@babel/core';
import annotateAsPure from '@babel/helper-annotate-as-pure';
import * as tslib from 'tslib';

/**
* A cached set of TypeScript helper function names used by the helper name matcher utility function.
* A set of constructor names that are considered to be side-effect free.
*/
const sideEffectFreeConstructors = new Set<string>(['InjectionToken']);

/**
* A set of TypeScript helper function names used by the helper name matcher utility function.
*/
const tslibHelpers = new Set<string>(Object.keys(tslib).filter((h) => h.startsWith('__')));

Expand Down Expand Up @@ -44,15 +49,23 @@ function isBabelHelperName(name: string): boolean {
return babelHelpers.has(name);
}

interface ExtendedPluginPass extends PluginPass {
opts: { topLevelSafeMode?: boolean };
}

/**
* A babel plugin factory function for adding the PURE annotation to top-level new and call expressions.
*
* @returns A babel plugin object instance.
*/
export default function (): PluginObj {
return {
visitor: {
CallExpression(path) {
CallExpression(path: NodePath<types.CallExpression>, state: ExtendedPluginPass) {
const { topLevelSafeMode = false } = state.opts;
if (topLevelSafeMode) {
return;
}

// If the expression has a function parent, it is not top-level
if (path.getFunctionParent()) {
return;
Expand All @@ -65,6 +78,7 @@ export default function (): PluginObj {
) {
return;
}

// Do not annotate TypeScript helpers emitted by the TypeScript compiler or Babel helpers.
// They are intended to cause side effects.
if (
Expand All @@ -76,9 +90,22 @@ export default function (): PluginObj {

annotateAsPure(path);
},
NewExpression(path) {
NewExpression(path: NodePath<types.NewExpression>, state: ExtendedPluginPass) {
// If the expression has a function parent, it is not top-level
if (!path.getFunctionParent()) {
if (path.getFunctionParent()) {
return;
}

const { topLevelSafeMode = false } = state.opts;

if (!topLevelSafeMode) {
annotateAsPure(path);

return;
}

const callee = path.get('callee');
if (callee.isIdentifier() && sideEffectFreeConstructors.has(callee.node.name)) {
annotateAsPure(path);
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
*/

import { transformSync } from '@babel/core';
// eslint-disable-next-line import/no-extraneous-dependencies
import { format } from 'prettier';
import pureTopLevelPlugin from './pure-toplevel-functions';

function testCase({
input,
expected,
options,
}: {
input: string;
expected: string;
options?: { topLevelSafeMode: boolean };
}): jasmine.ImplementationCallback {
return async () => {
const result = transformSync(input, {
configFile: false,
babelrc: false,
compact: true,
plugins: [pureTopLevelPlugin],
plugins: [[pureTopLevelPlugin, options]],
});
if (!result?.code) {
fail('Expected babel to return a transform result.');
Expand Down Expand Up @@ -152,4 +153,33 @@ describe('pure-toplevel-functions Babel plugin', () => {
};
`),
);

describe('topLevelSafeMode: true', () => {
it(
'annotates top-level `new InjectionToken` expressions',
testCase({
input: `const result = new InjectionToken('abc');`,
expected: `const result = /*#__PURE__*/ new InjectionToken('abc');`,
options: { topLevelSafeMode: true },
}),
);

it(
'does not annotate other top-level `new` expressions',
testCase({
input: 'const result = new SomeClass();',
expected: 'const result = new SomeClass();',
options: { topLevelSafeMode: true },
}),
);

it(
'does not annotate top-level function calls',
testCase({
input: 'const result = someCall();',
expected: 'const result = someCall();',
options: { topLevelSafeMode: true },
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,19 @@ async function transformWithBabel(
}

if (options.advancedOptimizations) {
const sideEffectFree = options.sideEffects === false;
const safeAngularPackage =
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);

const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } =
await import('../babel/plugins');

if (safeAngularPackage) {
plugins.push(markTopLevelPure);
}
const sideEffectFree = options.sideEffects === false;
const safeAngularPackage =
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);

plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [
adjustStaticMembers,
{ wrapDecorators: sideEffectFree },
]);
plugins.push(
[markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }],
elideAngularMetadata,
adjustTypeScriptEnums,
[adjustStaticMembers, { wrapDecorators: sideEffectFree }],
);
}

// If no additional transformations are needed, return the data directly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface ApplicationPresetOptions {
inputSourceMap: unknown;
};
optimize?: {
pureTopLevel: boolean;
topLevelSafeMode: boolean;
wrapDecorators: boolean;
};

Expand Down Expand Up @@ -220,14 +220,12 @@ export default function (api: unknown, options: ApplicationPresetOptions) {
elideAngularMetadata,
markTopLevelPure,
} = require('@angular/build/private');
if (options.optimize.pureTopLevel) {
plugins.push(markTopLevelPure);
}

plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [
adjustStaticMembers,
{ wrapDecorators: options.optimize.wrapDecorators },
]);
plugins.push(
[markTopLevelPure, { topLevelSafeMode: options.optimize.topLevelSafeMode }],
elideAngularMetadata,
adjustTypeScriptEnums,
[adjustStaticMembers, { wrapDecorators: options.optimize.wrapDecorators }],
);
}

if (options.instrumentCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default custom<ApplicationPresetOptions>(() => {
customOptions.optimize = {
// Angular packages provide additional tested side effects guarantees and can use
// otherwise unsafe optimizations. (@angular/platform-server/init) however has side-effects.
pureTopLevel: AngularPackage && sideEffectFree,
topLevelSafeMode: !(AngularPackage && sideEffectFree),
// JavaScript modules that are marked as side effect free are considered to have
// no decorators that contain non-local effects.
wrapDecorators: sideEffectFree,
Expand Down