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
161 changes: 161 additions & 0 deletions packages/devextreme/build/vite-plugin-devextreme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { transformSync } from '@babel/core';

import {
moveFieldInitializersToConstructor,
removeUninitializedClassFields,
} from './vite-plugin-devextreme';

function transform(code: string, plugin: unknown): string {
const result = transformSync(code, {
babelrc: false,
configFile: false,
parserOpts: {
plugins: ['classProperties'],
},
plugins: [
plugin,
],
});
return result!.code!;
}

describe('moveFieldInitializersToConstructor', () => {
test('inserts field initializer after super() in derived class', () => {
const input = `
class Derived extends Base {
foo = 42;
constructor() {
super();
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/super\(\);\s*this\.foo\s*=\s*42/);
expect(out).not.toMatch(/this\.foo\s*=\s*42;\s*super\(\)/);
});

test('inserts field initializer after super() when pre-super statement exists', () => {
const input = `
class Derived extends Base {
foo = 42;
constructor() {
const y = 1;
super(y);
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/super\(y\);\s*this\.foo\s*=\s*42/);
expect(out).not.toMatch(/this\.foo\s*=\s*42;[\s\S]*super\(y\)/);
});

test('inserts after parameter-property assignments', () => {
const input = `
class A {
foo = 42;
constructor(x, y) {
this.x = x;
this.y = y;
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/this\.y\s*=\s*y;\s*this\.foo\s*=\s*42/);
});

test('inserts at start when no super and no param props', () => {
const input = `
class A {
foo = 42;
constructor() {
doSomething();
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/constructor\(\)\s*\{\s*this\.foo\s*=\s*42;\s*doSomething\(\)/);
});

test('preserves field order', () => {
const input = `
class A extends Base {
a = 1;
b = 2;
c = 3;
constructor() {
super();
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/this\.a\s*=\s*1;\s*this\.b\s*=\s*2;\s*this\.c\s*=\s*3/);
});

test('does not move static fields', () => {
const input = `
class A {
static shared = 1;
foo = 42;
constructor() {}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/static\s+shared\s*=\s*1/);
expect(out).toMatch(/this\.foo\s*=\s*42/);
});

test('does nothing when class has no constructor', () => {
const input = `
class A {
foo = 42;
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/foo\s*=\s*42/);
expect(out).not.toMatch(/constructor/);
});

test('does not touch this.x = x where x is not a constructor param', () => {
const input = `
class A {
foo = 42;
constructor() {
const x = 1;
this.x = x;
}
}
`;
const out = transform(input, moveFieldInitializersToConstructor);
expect(out).toMatch(/constructor\(\)\s*\{\s*this\.foo\s*=\s*42;\s*const\s+x\s*=\s*1;\s*this\.x\s*=\s*x/);
});
});

describe('removeUninitializedClassFields', () => {
test('removes declared but uninitialized fields', () => {
const input = `
class A {
foo;
bar = 1;
}
`;
const out = transform(input, removeUninitializedClassFields);
expect(out).not.toMatch(/\bfoo\b/);
expect(out).toMatch(/bar\s*=\s*1/);
});

test('keeps fields initialized with falsy values', () => {
const input = `
class A {
a = 0;
b = false;
c = null;
d = '';
}
`;
const out = transform(input, removeUninitializedClassFields);
expect(out).toMatch(/a\s*=\s*0/);
expect(out).toMatch(/b\s*=\s*false/);
expect(out).toMatch(/c\s*=\s*null/);
expect(out).toMatch(/d\s*=\s*''/);
});
});
166 changes: 166 additions & 0 deletions packages/devextreme/build/vite-plugin-devextreme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { transformAsync } from '@babel/core';
import type { PluginOption } from 'vite';

export function removeUninitializedClassFields(): unknown {
return {
visitor: {
ClassProperty(path: { node: { value: unknown }; remove: () => void }) {
if (path.node.value === null || path.node.value === undefined) {
path.remove();
}
},
},
};
}

export function moveFieldInitializersToConstructor(): unknown {
return {
visitor: {
Class(path: { node: { body: { body: unknown[] } } }) {
const body = path.node.body.body;

type ClassMember = {
type: string;
kind?: string;
key?: { name: string };
value?: unknown;
static?: boolean;
body?: { body: unknown[] };
};

const fieldsToMove: ClassMember[] = [];
const remaining: unknown[] = [];

for (const member of body as ClassMember[]) {
if (
member.type === 'ClassProperty'
&& member.value != null
&& !member.static
) {
fieldsToMove.push(member);
} else {
remaining.push(member);
}
}

if (fieldsToMove.length === 0) return;

const ctor = (remaining as ClassMember[]).find(
(m) => m.type === 'ClassMethod' && m.kind === 'constructor',
);

if (!ctor) return;

const assignments = fieldsToMove.map((field) => ({
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
object: { type: 'ThisExpression' },
property: { type: 'Identifier', name: field.key!.name },
computed: false,
},
right: field.value,
},
}));

type Stmt = {
type: string;
expression?: {
type: string;
operator?: string;
callee?: { type: string };
left?: { type: string; object?: { type: string }; property?: { name: string } };
right?: { type: string; name?: string };
};
};

type Param = { type: string; name?: string; left?: { name?: string } };
const paramNames = new Set<string>();
for (const param of ((ctor as unknown as { params: Param[] }).params ?? [])) {
if (param.type === 'Identifier' && param.name) {
paramNames.add(param.name);
} else if (param.type === 'AssignmentPattern' && param.left?.name) {
paramNames.add(param.left.name);
}
}

const ctorBody = ctor.body!.body as Stmt[];

let insertAt = 0;
for (let i = 0; i < ctorBody.length; i += 1) {
const stmt = ctorBody[i];
const isSuperCall = stmt.type === 'ExpressionStatement'
&& stmt.expression?.type === 'CallExpression'
&& stmt.expression.callee?.type === 'Super';
const isParamPropertyAssignment = stmt.type === 'ExpressionStatement'
&& stmt.expression?.type === 'AssignmentExpression'
&& stmt.expression.operator === '='
&& stmt.expression.left?.type === 'MemberExpression'
&& stmt.expression.left.object?.type === 'ThisExpression'
&& stmt.expression.right?.type === 'Identifier'
&& stmt.expression.left.property?.name === stmt.expression.right.name
&& paramNames.has(stmt.expression.right.name!);

if (isSuperCall || isParamPropertyAssignment) insertAt = i + 1;
}

ctorBody.splice(insertAt, 0, ...(assignments as Stmt[]));

path.node.body.body = remaining;
},
},
};
}

export default function devextremeVitePlugin(): PluginOption {
return {
name: 'vite-plugin-devextreme',
enforce: 'pre',

async transform(code: string, id: string) {
const cleanId = id.split('?')[0].split('#')[0];
if (!/\.[jt]sx?$/.test(cleanId) || cleanId.includes('node_modules')) {
return null;
}

const isTSX = cleanId.endsWith('.tsx');
const isTS = cleanId.endsWith('.ts') || isTSX;

const plugins: unknown[] = [];

if (isTS) {
plugins.push([
'@babel/plugin-transform-typescript',
{
isTSX,
allExtensions: true,
allowDeclareFields: true,
optimizeConstEnums: true,
},
]);
}

plugins.push(
removeUninitializedClassFields,
moveFieldInitializersToConstructor,
['@babel/plugin-proposal-decorators', { legacy: true }],
'babel-plugin-inferno',
);

const result = await transformAsync(code, {
filename: cleanId,
plugins,
sourceMaps: true,
});

if (!result?.code) {
return null;
}

return { code: result.code, map: result.map };
},
};
}
1 change: 1 addition & 0 deletions packages/devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default [
'themebuilder-scss/src/data/metadata/*',
'js/bundles/dx.custom.js',
'testing/jest/utils/transformers/*',
'vite.config.ts',
'**/ts/',
'js/common/core/localization/cldr-data/*',
'js/common/core/localization/default_messages.js',
Expand Down
14 changes: 12 additions & 2 deletions packages/devextreme/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,21 @@ module.exports = {
'__test-artifacts__'
],
roots: ['<rootDir>/build', '<rootDir>/eslint_plugins'],
moduleFileExtensions: ['js'],
moduleFileExtensions: ['ts', 'js'],
testMatch: [
'<rootDir>/build/**/*.test.js',
'<rootDir>/build/**/*.test.(ts|js)',
'<rootDir>/eslint_plugins/**/*.test.js',
],
preset: 'ts-jest',
transform: {
// eslint-disable-next-line spellcheck/spell-checker
'\\.ts$': ['ts-jest', {
// eslint-disable-next-line spellcheck/spell-checker
tsconfig: { module: 'commonjs', target: 'es2020', esModuleInterop: true },
diagnostics: false,
isolatedModules: true,
}],
},
}
]
};
Expand Down
6 changes: 5 additions & 1 deletion packages/devextreme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/eslint-parser": "catalog:",
"@babel/parser": "7.29.0",
"@babel/parser": "7.29.2",
"@babel/plugin-proposal-decorators": "7.29.0",
"@babel/plugin-transform-modules-commonjs": "7.28.6",
"@babel/plugin-transform-typescript": "7.28.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@devextreme-generator/angular": "3.0.12",
Expand Down Expand Up @@ -211,6 +213,7 @@
"typescript-min": "npm:typescript@4.9.5",
"uuid": "9.0.1",
"vinyl": "2.2.1",
"vite": "8.0.8",
"vinyl-named": "1.1.0",
"webpack": "5.105.4",
"webpack-stream": "7.0.0",
Expand Down Expand Up @@ -248,6 +251,7 @@
"validate-ts": "gulp validate-ts",
"validate-declarations": "dx-tools validate-declarations --sources ./js --exclude \"js/(renovation|__internal|.eslintrc.js)\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"",
"testcafe-in-docker": "docker build -f ./testing/testcafe/docker/Dockerfile -t testcafe-testing . && docker run -it testcafe-testing",
"dev:playground": "vite",
"test-jest": "cross-env NODE_OPTIONS='--expose-gc' jest --no-coverage --runInBand --selectProjects jsdom-tests",
"test-jest:watch": "jest --watch",
"test-jest:node": "jest --no-coverage --runInBand --selectProjects node-tests",
Expand Down
Loading
Loading