Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/vite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ it standalone for diagnostics, but do **not** run it alongside `ns run`/`ns debu
for the same platform — both would try to bind the same port. CLI-managed is the
supported default.

To leave `@NativeClass` for the runtime instead of downleveling it at build time,
pass `--env.disableNativeClassTransformer` or set `NS_DISABLE_NATIVE_CLASS_TRANSFORMER=1`.

## Usage

1) Create `vite.config.mts` (the `.mts` extension keeps the config ESM without setting `"type": "module"` in the app's package.json, avoiding Vite's `configLoader: 'native'` warning):
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/helpers/nativeclass-esbuild-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin as EsbuildPlugin } from 'esbuild';
import { isNativeESClassesEnabled, transformNativeClassSource } from './nativeclass-transform.js';
import { shouldSkipNativeClassTransform, transformNativeClassSource } from './nativeclass-transform.js';
import type { Platform } from './platform-types.js';

/**
Expand All @@ -15,7 +15,7 @@ export function createNativeClassEsbuildPlugin(platform: Platform): EsbuildPlugi
setup(build) {
// Native ES class mode (Apple targets only): the runtime handles ES classes and
// the NativeClass decorator directly, so vendor bundles need no rewriting either.
if (isNativeESClassesEnabled(platform)) {
if (shouldSkipNativeClassTransform(platform)) {
return;
}
// We need to use onLoad to transform the file contents
Expand Down
43 changes: 38 additions & 5 deletions packages/vite/helpers/nativeclass-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import ts from 'typescript';
import { getCliFlags } from './cli-flags.js';
import type { Platform } from './platform-types.js';

function isTruthyFlagValue(value: unknown): boolean {
if (value === true) return true;
if (typeof value !== 'string') return false;
const normalized = value.toLowerCase();
return normalized !== '' && normalized !== '0' && normalized !== 'false';
}

/**
* Skip the NativeClass ES5 downlevel on every platform so the runtime can consume
* `@NativeClass` directly. Enabled via `--env.disableNativeClassTransformer`
* (alias: `--env.disableNativeTransformer`) or `NS_DISABLE_NATIVE_CLASS_TRANSFORMER`
* (set to `0`/`false` to force-disable).
*/
export function isNativeClassTransformerDisabled(): boolean {
const envValue = process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
if (envValue !== undefined) {
return isTruthyFlagValue(envValue);
}
try {
const flags = getCliFlags();
return isTruthyFlagValue(flags.disableNativeClassTransformer) || isTruthyFlagValue(flags.disableNativeTransformer);
} catch (e) {
return false;
}
}

/**
* Opt-in: skip the NativeClass ES5 downlevel entirely and let the iOS runtime handle plain
* ES `class X extends NativeBase {}` declarations natively (the runtime registers the
Expand All @@ -20,16 +46,24 @@ export function isNativeESClassesEnabled(platform?: Platform): boolean {
if (platform === 'android') return false;
const envValue = process.env.NS_NATIVE_ES_CLASSES;
if (envValue !== undefined) {
return envValue !== '0' && envValue.toLowerCase() !== 'false';
return isTruthyFlagValue(envValue);
}
try {
const flags = getCliFlags();
return !!flags.nativeESClasses;
return isTruthyFlagValue(flags.nativeESClasses);
} catch (e) {
return false;
}
}

/**
* Returns true when NativeClass sources should be left untouched.
* @param platform Optional build platform used by the Apple-only native ES class mode.
*/
export function shouldSkipNativeClassTransform(platform?: Platform): boolean {
return isNativeClassTransformerDisabled() || isNativeESClassesEnabled(platform);
}

/**
* Apply the NativeClass transformer to a source string. Returns null if no change performed.
*/
Expand All @@ -46,9 +80,8 @@ export function transformNativeClassSource(code: string, fileName: string) {
// If cli flags cannot be read for any reason, fall back to original behavior.
}

// Native ES class mode (Apple targets only): leave sources untouched - the runtime
// understands ES classes extending native types and the NativeClass decorator itself.
if (isNativeESClassesEnabled(platform)) return null;
// Skip downlevel when the runtime should handle NativeClass.
if (shouldSkipNativeClassTransform(platform)) return null;

// If this is JS and we see a __decorate* call that references NativeClass, strip it safely.
const isJS = /\.(js|mjs|cjs)$/.test(fileName);
Expand Down
8 changes: 6 additions & 2 deletions packages/vite/helpers/nativeclass-transformer-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Plugin } from 'vite';
import ts from 'typescript';
import { isNativeESClassesEnabled, transformNativeClassSource } from './nativeclass-transform.js';
import { isNativeClassTransformerDisabled, shouldSkipNativeClassTransform, transformNativeClassSource } from './nativeclass-transform.js';
import { resolvePlatform } from './cli-flags.js';

/**
Expand Down Expand Up @@ -277,6 +277,10 @@ export function postCleanupNativeClass(code: string, bareId: string, verbose = f
* Wraps NativeClass TS transformer into a Vite plugin.
*/
export function createNativeClassTransformerPlugin(): Plugin[] {
if (isNativeClassTransformerDisabled()) {
return [];
}

const verbose = !!process.env.NS_DEBUG_NATIVECLASS;

return [
Expand Down Expand Up @@ -335,7 +339,7 @@ export function createNativeClassTransformerPlugin(): Plugin[] {
transform(code: string, id: string) {
// Native ES class mode (Apple targets only): the runtime handles ES classes and
// the NativeClass decorator directly, so no post-phase strip/downlevel either.
if (isNativeESClassesEnabled(resolvePlatform())) return null;
if (shouldSkipNativeClassTransform(resolvePlatform())) return null;
const bareId = id.split('?')[0];
if (!/\.(ts|tsx|js|mjs)$/.test(bareId)) return null;
return postCleanupNativeClass(code, bareId, verbose);
Expand Down
35 changes: 33 additions & 2 deletions packages/vite/helpers/nativeclass-transformer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, afterEach } from 'vitest';
import ts from 'typescript';
import { isNativeESClassesEnabled, transformNativeClassSource } from '../helpers/nativeclass-transform.js';
import { postCleanupNativeClass } from '../helpers/nativeclass-transformer-plugin.js';
import { isNativeClassTransformerDisabled, isNativeESClassesEnabled, transformNativeClassSource } from '../helpers/nativeclass-transform.js';
import { createNativeClassTransformerPlugin, postCleanupNativeClass } from '../helpers/nativeclass-transformer-plugin.js';

const SAMPLE_TS = `

Expand Down Expand Up @@ -348,6 +348,37 @@ export class FooImpl extends NSObject {
expect(hasTopLevelReturn).toBe(false);
});

describe('disable NativeClass transformer (NS_DISABLE_NATIVE_CLASS_TRANSFORMER)', () => {
afterEach(() => {
delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
});

it('is off by default so the transformer still runs', () => {
expect(isNativeClassTransformerDisabled()).toBe(false);
const res = transformNativeClassSource(SAMPLE_TS, '/app/src/sample.ts');
expect(res).not.toBeNull();
expect(res?.code).not.toContain('@NativeClass');
});

it('skips the transform on every platform when enabled', () => {
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1';
expect(isNativeClassTransformerDisabled()).toBe(true);
expect(transformNativeClassSource(SAMPLE_TS, '/app/src/sample.ts')).toBeNull();
});

it('can be force-disabled with 0/false', () => {
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '0';
expect(isNativeClassTransformerDisabled()).toBe(false);
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = 'false';
expect(isNativeClassTransformerDisabled()).toBe(false);
});

it('does not register the Vite transformer plugins when disabled', () => {
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1';
expect(createNativeClassTransformerPlugin()).toEqual([]);
});
});

describe('native ES class mode (NS_NATIVE_ES_CLASSES)', () => {
afterEach(() => {
delete process.env.NS_NATIVE_ES_CLASSES;
Expand Down
1 change: 1 addition & 0 deletions packages/webpack5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Useful globally available variables in your app:
| `--env.report` | Generate bundle analysis report |
| `--env.verbose` | Print verbose logs and internal config |
| `--env.e2e` | Enable E2E mode (enables `testID` property) |
| `--env.disableNativeClassTransformer` | Leave `@NativeClass` for the runtime (skip the bundler downlevel). Alias: `--env.disableNativeTransformer` |

## 📖 Documentation

Expand Down
13 changes: 13 additions & 0 deletions packages/webpack5/__tests__/configuration/angular.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ describe('angular configuration', () => {
});
}

it('skips NativeClass platformTransformers when disableNativeClassTransformer is set', () => {
init({
ios: true,
disableNativeClassTransformer: true,
});
const config = angular(new Config());

config.plugin('AngularCompilerPlugin').tap((args) => {
expect(args[0].platformTransformers).toEqual([]);
return args;
});
});

it(`loads polyfills.ts into the bundle entry if it exists `, () => {
polyfillsPath = `__jest__/src/polyfills.ts`;

Expand Down
33 changes: 33 additions & 0 deletions packages/webpack5/__tests__/configuration/base.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,39 @@ describe('base configuration', () => {
});
});

it('skips NativeClass transformer and loaders when disableNativeClassTransformer is set', () => {
init({
ios: true,
disableNativeClassTransformer: true,
});
const config = base(new Config());
const tsRule = config.module.rule('ts');

expect(tsRule.uses.has('native-class-downlevel-loader')).toBe(false);
expect(tsRule.uses.has('native-class-strip-loader')).toBe(false);

tsRule.use('ts-loader').tap((options) => {
expect(options.getCustomTransformers()).toEqual({ before: [] });
return options;
});
});

it('keeps NativeClass transformer and loaders enabled by default', () => {
init({
ios: true,
});
const config = base(new Config());
const tsRule = config.module.rule('ts');

expect(tsRule.uses.has('native-class-downlevel-loader')).toBe(true);
expect(tsRule.uses.has('native-class-strip-loader')).toBe(true);

tsRule.use('ts-loader').tap((options) => {
expect(options.getCustomTransformers().before.length).toBe(1);
return options;
});
});

it('supports --env.profile', () => {
init({
platform: 'ios',
Expand Down
48 changes: 48 additions & 0 deletions packages/webpack5/__tests__/helpers/nativeClassTransformer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { isNativeClassTransformerDisabled } from '../../src/helpers/nativeClassTransformer';

describe('isNativeClassTransformerDisabled', () => {
const originalEnv = process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;

afterEach(() => {
if (originalEnv === undefined) {
delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
} else {
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = originalEnv;
}
});

it('is off by default', () => {
delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
expect(isNativeClassTransformerDisabled({})).toBe(false);
});

it('honors --env.disableNativeClassTransformer', () => {
delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
expect(
isNativeClassTransformerDisabled({
disableNativeClassTransformer: true,
}),
).toBe(true);
expect(
isNativeClassTransformerDisabled({
disableNativeClassTransformer: false,
}),
).toBe(false);
});

it('honors --env.disableNativeTransformer as an alias', () => {
delete process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER;
expect(
isNativeClassTransformerDisabled({
disableNativeTransformer: true,
}),
).toBe(true);
});

it('honors NS_DISABLE_NATIVE_CLASS_TRANSFORMER', () => {
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '1';
expect(isNativeClassTransformerDisabled({})).toBe(true);
process.env.NS_DISABLE_NATIVE_CLASS_TRANSFORMER = '0';
expect(isNativeClassTransformerDisabled({})).toBe(false);
});
});
51 changes: 29 additions & 22 deletions packages/webpack5/src/configuration/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getTypescript, readTsConfig } from '../helpers/typescript';
import { getDependencyVersion } from '../helpers/dependencies';
import { getProjectTSConfigPath } from '../helpers/project';
import { env as _env, IWebpackEnv } from '../index';
import { isNativeClassTransformerDisabled } from '../helpers/nativeClassTransformer';
import { warnOnce } from '../helpers/log';
import {
getEntryDirPath,
Expand All @@ -32,6 +33,10 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
const platform = getPlatformName();
const tsConfigPath = getProjectTSConfigPath();
const disableAOT = !!env.disableAOT;
const skipNativeClassTransformer = isNativeClassTransformerDisabled(env);
const nativeClassTransformers = skipNativeClassTransformer
? []
: [require('../transformers/NativeClass').default];

// remove default ts rule
config.module.rules.delete('ts');
Expand Down Expand Up @@ -140,7 +145,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// just return the original path otherwise
return path;
},
platformTransformers: [require('../transformers/NativeClass').default],
platformTransformers: nativeClassTransformers,
},
]);
}
Expand All @@ -150,27 +155,29 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// angular no longer supports transformers.
// so we patch their method until they do
// https://github.com/angular/angular-cli/pull/21046
const originalCreateFileEmitter =
angularWebpackPlugin.prototype.createFileEmitter;
angularWebpackPlugin.prototype.createFileEmitter = function (
...args: any[]
) {
let transformers = args[1] || {};
if (!transformers.before) {
transformers.before = [];
}
if (this.pluginOptions.jitMode) {
transformers.before.unshift(
require('../transformers/NativeClass').default,
);
} else {
transformers.before.push(
require('../transformers/NativeClass').default,
);
}
args[1] = transformers;
return originalCreateFileEmitter.apply(this, args);
};
if (!skipNativeClassTransformer) {
const originalCreateFileEmitter =
angularWebpackPlugin.prototype.createFileEmitter;
angularWebpackPlugin.prototype.createFileEmitter = function (
...args: any[]
) {
let transformers = args[1] || {};
if (!transformers.before) {
transformers.before = [];
}
if (this.pluginOptions.jitMode) {
transformers.before.unshift(
require('../transformers/NativeClass').default,
);
} else {
transformers.before.push(
require('../transformers/NativeClass').default,
);
}
args[1] = transformers;
return originalCreateFileEmitter.apply(this, args);
};
}
config.plugin('AngularWebpackPlugin').use(angularWebpackPlugin, [
{
tsconfig: tsConfigPath,
Expand Down
Loading
Loading