Skip to content

Commit

Permalink
refactor: allow tree-shaking of dev-only code (#2259)
Browse files Browse the repository at this point in the history
In this commit, we're removing all const `NG_DEV_MODE` declarations and inlining
`typeof ngDevMode !== 'undefined' && ngDevMode` conditions.

This change is **required** for ESBuild tree-shaking to work properly.
It has been observed that development-only code is not being dropped in production builds.

For example, this code:

```js
const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;
if (NG_DEV_MODE) {
  ensureStateNameIsValid(stateName);
}
```

Becomes the following after the build:

```js
var NG_DEV_MODE$4 = !1;
NG_DEV_MODE$4 && ensureStateNameIsValid(stateName);
```

Terser was able to inline `NG_DEV_MODE` variables, because it was able to evaluate their
expressions during the build (that it's _falsy_) and remove them entirely.
  • Loading branch information
arturovt authored Nov 18, 2024
1 parent 14021c0 commit e14b71b
Show file tree
Hide file tree
Showing 20 changed files with 42 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ $ npm install @ngxs/store@dev

### To become next patch version

- Refactor: Allow tree-shaking of dev-only code [#2259](https://github.com/ngxs/store/pull/2259)
- Refactor: Use field initializers for injectees [#2258](https://github.com/ngxs/store/pull/2258)
- Fix(store): Run plugins in injection context [#2256](https://github.com/ngxs/store/pull/2256)
- Fix(websocket-plugin): Do not dispatch action when root injector is destroyed [#2257](https://github.com/ngxs/store/pull/2257)
Expand Down
6 changes: 2 additions & 4 deletions packages/router-plugin/internals/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { InjectionToken } from '@angular/core';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export const enum NavigationActionTiming {
PreActivation = 1,
PostActivation = 2
Expand All @@ -14,12 +12,12 @@ export interface NgxsRouterPluginOptions {
}

export const ɵUSER_OPTIONS = new InjectionToken<NgxsRouterPluginOptions | undefined>(
NG_DEV_MODE ? 'USER_OPTIONS' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'USER_OPTIONS' : '',
{ providedIn: 'root', factory: () => undefined }
);

export const ɵNGXS_ROUTER_PLUGIN_OPTIONS = new InjectionToken<NgxsRouterPluginOptions>(
NG_DEV_MODE ? 'NGXS_ROUTER_PLUGIN_OPTIONS' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_ROUTER_PLUGIN_OPTIONS' : '',
{ providedIn: 'root', factory: () => ({}) }
);

Expand Down
10 changes: 4 additions & 6 deletions packages/storage-plugin/internals/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export const ɵDEFAULT_STATE_KEY = '@@STATE';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export const enum StorageOption {
LocalStorage,
SessionStorage
Expand Down Expand Up @@ -88,12 +86,12 @@ export interface ɵNgxsTransformedStoragePluginOptions extends NgxsStoragePlugin
}

export const ɵUSER_OPTIONS = new InjectionToken<NgxsStoragePluginOptions>(
NG_DEV_MODE ? 'USER_OPTIONS' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'USER_OPTIONS' : ''
);

// Determines whether all states in the NGXS registry should be persisted or not.
export const ɵALL_STATES_PERSISTED = new InjectionToken<boolean>(
NG_DEV_MODE ? 'ALL_STATES_PERSISTED' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'ALL_STATES_PERSISTED' : '',
{
providedIn: 'root',
factory: () => inject(ɵUSER_OPTIONS).keys === '*'
Expand All @@ -102,11 +100,11 @@ export const ɵALL_STATES_PERSISTED = new InjectionToken<boolean>(

export const ɵNGXS_STORAGE_PLUGIN_OPTIONS =
new InjectionToken<ɵNgxsTransformedStoragePluginOptions>(
NG_DEV_MODE ? 'NGXS_STORAGE_PLUGIN_OPTIONS' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_STORAGE_PLUGIN_OPTIONS' : ''
);

export const STORAGE_ENGINE = new InjectionToken<StorageEngine>(
NG_DEV_MODE ? 'STORAGE_ENGINE' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'STORAGE_ENGINE' : ''
);

export interface StorageEngine {
Expand Down
6 changes: 2 additions & 4 deletions packages/storage-plugin/src/engines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ import { StorageEngine } from '@ngxs/storage-plugin/internals';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export const LOCAL_STORAGE_ENGINE = /* @__PURE__ */ new InjectionToken<StorageEngine | null>(
NG_DEV_MODE ? 'LOCAL_STORAGE_ENGINE' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'LOCAL_STORAGE_ENGINE' : '',
{
providedIn: 'root',
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : null)
}
);

export const SESSION_STORAGE_ENGINE = /* @__PURE__ */ new InjectionToken<StorageEngine | null>(
NG_DEV_MODE ? 'SESSION_STORAGE_ENGINE' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'SESSION_STORAGE_ENGINE' : '',
{
providedIn: 'root',
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? sessionStorage : null)
Expand Down
7 changes: 3 additions & 4 deletions packages/storage-plugin/src/storage.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import { ɵNgxsStoragePluginKeysManager } from './keys-manager';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

@Injectable()
export class NgxsStoragePlugin implements NgxsPlugin {
private _keysManager = inject(ɵNgxsStoragePluginKeysManager);
Expand Down Expand Up @@ -71,7 +69,8 @@ export class NgxsStoragePlugin implements NgxsPlugin {
const newVal = this._options.deserialize!(storedValue);
storedValue = this._options.afterDeserialize!(newVal, key);
} catch {
NG_DEV_MODE &&
typeof ngDevMode !== 'undefined' &&
ngDevMode &&
console.error(
`Error ocurred while deserializing the ${storageKey} store value, falling back to empty object, the value obtained from the store: `,
storedValue
Expand Down Expand Up @@ -120,7 +119,7 @@ export class NgxsStoragePlugin implements NgxsPlugin {
const newStoredValue = this._options.beforeSerialize!(storedValue, key);
engine.setItem(storageKey, this._options.serialize!(newStoredValue));
} catch (error: any) {
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
if (
error &&
(error.name === 'QuotaExceededError' ||
Expand Down
4 changes: 1 addition & 3 deletions packages/storage-plugin/src/with-storage-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import { ɵNgxsStoragePluginKeysManager } from './keys-manager';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export function withStorageFeature(storageKeys: StorageKey[]): EnvironmentProviders {
return makeEnvironmentProviders([
{
Expand All @@ -21,7 +19,7 @@ export function withStorageFeature(storageKeys: StorageKey[]): EnvironmentProvid
const allStatesPersisted = inject(ɵALL_STATES_PERSISTED);

if (allStatesPersisted) {
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
const message =
'The NGXS storage plugin is currently persisting all states because the `keys` ' +
'option was explicitly set to `*` at the root level. To selectively persist states, ' +
Expand Down
4 changes: 1 addition & 3 deletions packages/store/internals/src/initial-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { ɵPlainObject } from './symbols';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export class ɵInitialState {
private static _value: ɵPlainObject = {};

Expand All @@ -21,7 +19,7 @@ export class ɵInitialState {
}

export const ɵINITIAL_STATE_TOKEN = new InjectionToken<ɵPlainObject>(
NG_DEV_MODE ? 'INITIAL_STATE_TOKEN' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'INITIAL_STATE_TOKEN' : '',
{
providedIn: 'root',
factory: () => ɵInitialState.pop()
Expand Down
6 changes: 2 additions & 4 deletions packages/store/internals/src/internal-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import { InjectionToken } from '@angular/core';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

// These tokens are internal and can change at any point.

export const ɵNGXS_STATE_FACTORY = /* @__PURE__ */ new InjectionToken<any>(
NG_DEV_MODE ? 'ɵNGXS_STATE_FACTORY' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'ɵNGXS_STATE_FACTORY' : ''
);

export const ɵNGXS_STATE_CONTEXT_FACTORY = /* @__PURE__ */ new InjectionToken<any>(
NG_DEV_MODE ? 'ɵNGXS_STATE_CONTEXT_FACTORY' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'ɵNGXS_STATE_CONTEXT_FACTORY' : ''
);
6 changes: 3 additions & 3 deletions packages/store/plugins/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { InjectionToken } from '@angular/core';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

// The injection token is used to resolve to custom NGXS plugins provided
// at the root level through either `{provide}` scheme or `withNgxsPlugin`.
export const NGXS_PLUGINS = new InjectionToken(NG_DEV_MODE ? 'NGXS_PLUGINS' : '');
export const NGXS_PLUGINS = new InjectionToken(
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_PLUGINS' : ''
);

export type NgxsPluginFn = (state: any, mutation: any, next: NgxsNextPluginFn) => any;

Expand Down
4 changes: 1 addition & 3 deletions packages/store/src/dev-features/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { InjectionToken } from '@angular/core';

import { ActionType } from '../actions/symbols';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export interface NgxsDevelopmentOptions {
// This allows setting only `true` because there's no reason to set `false`.
// Developers may just skip importing the development module at all.
Expand All @@ -15,7 +13,7 @@ export interface NgxsDevelopmentOptions {
}

export const NGXS_DEVELOPMENT_OPTIONS = new InjectionToken<NgxsDevelopmentOptions>(
NG_DEV_MODE ? 'NGXS_DEVELOPMENT_OPTIONS' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_DEVELOPMENT_OPTIONS' : '',
{
providedIn: 'root',
factory: () => ({ warnOnUnhandledActions: true })
Expand Down
6 changes: 2 additions & 4 deletions packages/store/src/execution/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@ import { InjectionToken, inject, INJECTOR, Type, NgZone } from '@angular/core';
import { NoopNgxsExecutionStrategy } from './noop-ngxs-execution-strategy';
import { DispatchOutsideZoneNgxsExecutionStrategy } from './dispatch-outside-zone-ngxs-execution-strategy';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

/**
* Consumers have the option to utilize the execution strategy provided by
* `NgxsModule.forRoot({executionStrategy})` or `provideStore([], {executionStrategy})`.
*/
export const CUSTOM_NGXS_EXECUTION_STRATEGY = new InjectionToken<
Type<NgxsExecutionStrategy> | undefined
>(NG_DEV_MODE ? 'CUSTOM_NGXS_EXECUTION_STRATEGY' : '');
>(typeof ngDevMode !== 'undefined' && ngDevMode ? 'CUSTOM_NGXS_EXECUTION_STRATEGY' : '');

/**
* The injection token is used internally to resolve an instance of the execution
* strategy. It checks whether consumers have provided their own `executionStrategy`
* and also verifies if we are operating in a zone-aware environment.
*/
export const NGXS_EXECUTION_STRATEGY = new InjectionToken<NgxsExecutionStrategy>(
NG_DEV_MODE ? 'NGXS_EXECUTION_STRATEGY' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_EXECUTION_STRATEGY' : '',
{
providedIn: 'root',
factory: () => {
Expand Down
8 changes: 3 additions & 5 deletions packages/store/src/internal/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { NgxsConfig } from '../symbols';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export type StateKeyGraph = ɵPlainObjectOf<string[]>;
export type StatesByName = ɵPlainObjectOf<ɵStateClassInternal>;

Expand Down Expand Up @@ -108,7 +106,7 @@ export function propGetter(paths: string[], config: NgxsConfig) {
// We've been trying to deprecate the `Select` decorator because it's unstable with
// server-side rendering and micro-frontend applications.
export const ɵPROP_GETTER = new InjectionToken<(paths: string[]) => (x: any) => any>(
NG_DEV_MODE ? 'PROP_GETTER' : '',
typeof ngDevMode !== 'undefined' && ngDevMode ? 'PROP_GETTER' : '',
{
providedIn: 'root',
factory: () =>
Expand Down Expand Up @@ -140,7 +138,7 @@ export function buildGraph(stateClasses: ɵStateClassInternal[]): StateKeyGraph
const findName = (stateClass: ɵStateClassInternal) => {
const meta = stateClasses.find(g => g === stateClass);

if (NG_DEV_MODE && !meta) {
if (typeof ngDevMode !== 'undefined' && ngDevMode && !meta) {
throw new Error(
`Child state not found: ${stateClass}. \r\nYou may have forgotten to add states to module`
);
Expand Down Expand Up @@ -258,7 +256,7 @@ export function topologicalSort(graph: StateKeyGraph): string[] {
visited[name] = true;

graph[name].forEach((dep: string) => {
if (NG_DEV_MODE && ancestors.indexOf(dep) >= 0) {
if (typeof ngDevMode !== 'undefined' && ngDevMode && ancestors.indexOf(dep) >= 0) {
throw new Error(
`Circular dependency '${dep}' is required by '${name}': ${ancestors.join(' -> ')}`
);
Expand Down
4 changes: 1 addition & 3 deletions packages/store/src/internal/lifecycle-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { MappedStore, StatesAndDefaults } from './internals';
import { NgxsLifeCycle, NgxsSimpleChange, StateContext } from '../symbols';
import { getInvalidInitializationOrderMessage } from '../configs/messages.config';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

@Injectable({ providedIn: 'root' })
export class LifecycleStateManager implements OnDestroy {
private _store = inject(Store);
Expand All @@ -32,7 +30,7 @@ export class LifecycleStateManager implements OnDestroy {
action: InitState | UpdateState,
results: StatesAndDefaults | undefined
): void {
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
if (action instanceof InitState) {
this._initStateHasBeenDispatched = true;
} else if (
Expand Down
10 changes: 4 additions & 6 deletions packages/store/src/internal/state-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ import { assignUnhandledCallback } from './unhandled-rxjs-error-callback';
import { StateContextFactory } from './state-context-factory';
import { ofActionDispatched } from '../operators/of-action';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

function cloneDefaults(defaults: any): any {
let value = defaults === undefined ? {} : defaults;

Expand Down Expand Up @@ -158,7 +156,7 @@ export class StateFactory implements OnDestroy {
* Add a new state to the global defs.
*/
add(stateClasses: ɵStateClassInternal[]): MappedStore[] {
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
ensureStatesAreDecorated(stateClasses);
}

Expand All @@ -182,7 +180,7 @@ export class StateFactory implements OnDestroy {
// `State` decorator. This check is moved here because the `ɵprov` property
// will not exist on the class in JIT mode (because it's set asynchronously
// during JIT compilation through `Object.defineProperty`).
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
ensureStateClassIsInjectable(stateClass);
}

Expand Down Expand Up @@ -289,7 +287,7 @@ export class StateFactory implements OnDestroy {

// The `NgxsUnhandledActionsLogger` is a tree-shakable class which functions
// only during development.
if (NG_DEV_MODE && !actionHasBeenHandled) {
if (typeof ngDevMode !== 'undefined' && ngDevMode && !actionHasBeenHandled) {
const unhandledActionsLogger = this._injector.get(NgxsUnhandledActionsLogger, null);
// The `NgxsUnhandledActionsLogger` will not be resolved by the injector if the
// `NgxsDevelopmentModule` is not provided. It's enough to check whether the `injector.get`
Expand All @@ -312,7 +310,7 @@ export class StateFactory implements OnDestroy {

for (const stateClass of stateClasses) {
const stateName = ɵgetStoreMetadata(stateClass).name!;
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
ensureStateNameIsUnique(stateName, stateClass, statesMap);
}
const unmountedState = !statesMap[stateName];
Expand Down
4 changes: 1 addition & 3 deletions packages/store/src/selectors/selector-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { CreationMetadata, RuntimeSelectorInfo } from './selector-models';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

export function createRootSelectorFactory<T extends (...args: any[]) => any>(
selectorMetaData: ɵSelectorMetaDataModel,
selectors: any[] | undefined,
Expand Down Expand Up @@ -45,7 +43,7 @@ export function createRootSelectorFactory<T extends (...args: any[]) => any>(
// We're logging an error in this function because it may be used by `select`,
// `selectSignal`, and `selectSnapshot`. Therefore, there's no need to catch
// exceptions there to log errors.
if (NG_DEV_MODE) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
const message =
'The selector below has thrown an error upon invocation. ' +
'Please check for any unsafe property access that may result in null ' +
Expand Down
6 changes: 2 additions & 4 deletions packages/store/src/standalone-features/initializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { SelectFactory } from '../decorators/select/select-factory';
import { InternalStateOperations } from '../internal/state-operations';
import { LifecycleStateManager } from '../internal/lifecycle-state-manager';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

/**
* This function is shared by both NgModule and standalone features.
* When using `NgxsModule.forRoot` and `provideStore`, we can depend on the
Expand Down Expand Up @@ -78,14 +76,14 @@ export function featureStatesInitializer(): void {
* InjectionToken that registers the global Store.
*/
export const NGXS_ROOT_STORE_INITIALIZER = new InjectionToken<void>(
NG_DEV_MODE ? 'NGXS_ROOT_STORE_INITIALIZER' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_ROOT_STORE_INITIALIZER' : ''
);

/**
* InjectionToken that registers feature states.
*/
export const NGXS_FEATURE_STORE_INITIALIZER = new InjectionToken<void>(
NG_DEV_MODE ? 'NGXS_FEATURE_STORE_INITIALIZER' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_FEATURE_STORE_INITIALIZER' : ''
);

export const NGXS_ROOT_ENVIRONMENT_INITIALIZER: Provider[] = [
Expand Down
4 changes: 1 addition & 3 deletions packages/store/src/standalone-features/preboot.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { InjectionToken, makeEnvironmentProviders } from '@angular/core';

const NG_DEV_MODE = typeof ngDevMode !== 'undefined' && ngDevMode;

/**
* InjectionToken that registers preboot functions (called before the root initializer).
*/
export const NGXS_PREBOOT_FNS = new InjectionToken<VoidFunction[]>(
NG_DEV_MODE ? 'NGXS_PREBOOT_FNS' : ''
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_PREBOOT_FNS' : ''
);

/**
Expand Down
Loading

0 comments on commit e14b71b

Please sign in to comment.