fix: add strict types to build scripts and remove all any usage

Replace all implicit and explicit any types in build scripts with
proper TypeScript Compiler API types (Decorator, ClassDeclaration,
MethodDeclaration, Identifier, SourceFile, etc.). Add PackageJson
and InjectableClassEntry interfaces. Fix return types, null checks,
and type assertions throughout all transformer scripts.
This commit is contained in:
Daniel Sogl
2026-03-21 16:11:27 -07:00
parent 6453f2ab78
commit 62956e429c
9 changed files with 160 additions and 108 deletions

View File

@@ -1,5 +1,4 @@
import { readdirSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { readdirSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import {
ArrayLiteralExpression,
@@ -20,35 +19,43 @@ export const ROOT = resolve(__dirname, '../../');
export const TS_CONFIG = JSON.parse(readFileSync(resolve(ROOT, 'tsconfig.json'), 'utf-8'));
export const COMPILER_OPTIONS = TS_CONFIG.compilerOptions;
export const PLUGINS_ROOT = join(ROOT, 'src/@awesome-cordova-plugins/plugins/');
export const PLUGIN_PATHS = readdirSync(PLUGINS_ROOT).map((d) => join(PLUGINS_ROOT, d, 'index.ts'));
export const PLUGIN_PATHS = readdirSync(PLUGINS_ROOT).map((d: string) => join(PLUGINS_ROOT, d, 'index.ts'));
export function getDecorator(node: Node, index = 0): Decorator {
export function getDecorator(node: Node, index = 0): Decorator | undefined {
const decorators = canHaveDecorators(node) ? tsGetDecorators(node) : undefined;
if (decorators && decorators[index]) {
return decorators[index];
}
return undefined;
}
export function hasDecorator(decoratorName: string, node: Node): boolean {
const decorators = canHaveDecorators(node) ? tsGetDecorators(node) : undefined;
return decorators && decorators.length > 0 && decorators.findIndex((d) => getDecoratorName(d) === decoratorName) > -1;
return (
!!decorators && decorators.length > 0 && decorators.findIndex((d) => getDecoratorName(d) === decoratorName) > -1
);
}
export function getDecoratorName(decorator: any) {
return decorator.expression.expression.text;
export function getDecoratorName(decorator: Decorator): string {
return (decorator.expression as unknown as { expression: { text: string } }).expression.text;
}
export function getRawDecoratorArgs(decorator: any): any[] {
if (decorator.expression.arguments.length === 0) return [];
return decorator.expression.arguments[0].properties;
export function getRawDecoratorArgs(
decorator: Decorator
): Array<{ name: { text: string }; initializer: { kind: number; text: string; elements: Array<{ text: string }> } }> {
const expr = decorator.expression as unknown as {
arguments: Array<{ properties: ReturnType<typeof getRawDecoratorArgs> }>;
};
if (expr.arguments.length === 0) return [];
return expr.arguments[0].properties;
}
export function getDecoratorArgs(decorator: any) {
const properties: any[] = getRawDecoratorArgs(decorator);
const args = {};
export function getDecoratorArgs(decorator: Decorator): Record<string, string | number | boolean | string[]> {
const properties = getRawDecoratorArgs(decorator);
const args: Record<string, string | number | boolean | string[]> = {};
properties.forEach((prop) => {
let val: number | boolean;
let val: string | number | boolean | string[];
switch (prop.initializer.kind) {
case SyntaxKind.StringLiteral:
@@ -57,7 +64,7 @@ export function getDecoratorArgs(decorator: any) {
break;
case SyntaxKind.ArrayLiteralExpression:
val = prop.initializer.elements.map((e: any) => e.text);
val = prop.initializer.elements.map((e) => e.text);
break;
case SyntaxKind.TrueKeyword:
@@ -84,17 +91,16 @@ export function getDecoratorArgs(decorator: any) {
}
/**
* FROM STENCIL
* Convert a js value into typescript AST
* @param val array, object, string, boolean, or number
* @returns Typescript Object Literal, Array Literal, String Literal, Boolean Literal, Numeric Literal
*/
export function convertValueToLiteral(val: any) {
export function convertValueToLiteral(
val: string | number | boolean | string[] | Record<string, unknown> | unknown[]
): Expression {
if (Array.isArray(val)) {
return arrayToArrayLiteral(val);
}
if (typeof val === 'object') {
return objectToObjectLiteral(val);
if (typeof val === 'object' && val !== null) {
return objectToObjectLiteral(val as Record<string, unknown>);
}
if (typeof val === 'number') {
return factory.createNumericLiteral(val);
@@ -105,37 +111,26 @@ export function convertValueToLiteral(val: any) {
if (typeof val === 'boolean') {
return val ? factory.createTrue() : factory.createFalse();
}
throw new Error('Unexpected value type: ' + typeof val);
}
/**
* FROM STENCIL
* Convert a js object into typescript AST
* @param obj key value object
* @returns Typescript Object Literal Expression
*/
function objectToObjectLiteral(obj: { [key: string]: any }): ObjectLiteralExpression {
function objectToObjectLiteral(obj: Record<string, unknown>): ObjectLiteralExpression {
const newProperties: ObjectLiteralElementLike[] = Object.keys(obj).map((key: string): ObjectLiteralElementLike => {
return factory.createPropertyAssignment(
factory.createStringLiteral(key),
convertValueToLiteral(obj[key]) as Expression
convertValueToLiteral(obj[key] as string | number | boolean | string[])
);
});
return factory.createObjectLiteralExpression(newProperties);
}
/**
* FROM STENCIL
* Convert a js array into typescript AST
* @param list arrayÏ
* @returns Typescript Array Literal Expression
*/
function arrayToArrayLiteral(list: any[]): ArrayLiteralExpression {
const newList: any[] = list.map(convertValueToLiteral);
function arrayToArrayLiteral(list: unknown[]): ArrayLiteralExpression {
const newList = list.map((item) => convertValueToLiteral(item as string | number | boolean));
return factory.createArrayLiteralExpression(newList);
}
export function getMethodsForDecorator(decoratorName: string) {
export function getMethodsForDecorator(decoratorName: string): string[] {
switch (decoratorName) {
case 'CordovaProperty':
return ['cordovaPropertyGet', 'cordovaPropertySet'];

View File

@@ -23,7 +23,7 @@ export const EMIT_PATH = resolve(ROOT, 'injectable-classes.json');
*/
export function extractInjectables() {
return (ctx: TransformationContext) => {
return (tsSourceFile) => {
return (tsSourceFile: any) => {
if (tsSourceFile.fileName.indexOf('src/@awesome-cordova-plugins/plugins') > -1) {
visitEachChild(
tsSourceFile,
@@ -36,7 +36,7 @@ export function extractInjectables() {
if (isInjectable) {
injectableClasses.push({
file: tsSourceFile.path,
className: (node as ClassDeclaration).name.text,
className: (node as ClassDeclaration).name!.text,
dirName: tsSourceFile.path.split(/[\\\/]+/).reverse()[1],
});
}

View File

@@ -1,20 +1,24 @@
import { factory, SourceFile, SyntaxKind, TransformationContext } from 'typescript';
import { factory, Identifier, ImportSpecifier, SourceFile, SyntaxKind, TransformationContext } from 'typescript';
import { getMethodsForDecorator } from '../helpers';
function transformImports(file: SourceFile, ctx: TransformationContext, ngcBuild?: boolean) {
function transformImports(file: SourceFile, _ctx: TransformationContext, ngcBuild?: boolean) {
// remove angular imports
if (!ngcBuild) {
// @ts-expect-error
file.statements = (file.statements as any).filter(
(s: any) => !(s.kind === SyntaxKind.ImportDeclaration && s.moduleSpecifier.text === '@angular/core')
);
// @ts-expect-error — mutating readonly statements for transformer pipeline
file.statements = (
file.statements as unknown as Array<{ kind: number; moduleSpecifier?: { text: string } }>
).filter((s) => !(s.kind === SyntaxKind.ImportDeclaration && s.moduleSpecifier?.text === '@angular/core'));
}
// find the @awesome-cordova-plugins/core import statement
const importStatement = (file.statements as any).find((s: any) => {
return s.kind === SyntaxKind.ImportDeclaration && s.moduleSpecifier.text === '@awesome-cordova-plugins/core';
});
const importStatement = (file.statements as unknown as Array<Record<string, unknown>>).find((s) => {
return (
(s as { kind: number }).kind === SyntaxKind.ImportDeclaration &&
(s as { moduleSpecifier: { text: string } }).moduleSpecifier.text === '@awesome-cordova-plugins/core'
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as Record<string, any> | undefined;
// we're only interested in files containing @awesome-cordova-plugins/core import statement
if (!importStatement) return file;
@@ -27,7 +31,7 @@ function transformImports(file: SourceFile, ctx: TransformationContext, ngcBuild
const keep: string[] = ['getPromise', 'checkAvailability'];
let m;
let m: RegExpExecArray | null;
while ((m = decoratorRegex.exec(file.text)) !== null) {
if (m.index === decoratorRegex.lastIndex) {
@@ -37,27 +41,27 @@ function transformImports(file: SourceFile, ctx: TransformationContext, ngcBuild
}
if (decorators.length) {
let methods = [];
let methods: string[] = [];
decorators.forEach((d) => (methods = getMethodsForDecorator(d).concat(methods)));
const methodElements = methods.map((m) => factory.createIdentifier(m));
const methodNames = methodElements.map((el) => el.escapedText);
const methodElements = methods.map((name: string) => factory.createIdentifier(name));
const methodNames = methodElements.map((el: Identifier) => el.escapedText);
importStatement.importClause.namedBindings.elements = [
factory.createIdentifier('AwesomeCordovaNativePlugin'),
...methodElements,
...importStatement.importClause.namedBindings.elements.filter(
(el) => keep.indexOf(el.name.text) !== -1 && methodNames.indexOf(el.name.text) === -1
(el: ImportSpecifier) => keep.indexOf(el.name.text) !== -1 && methodNames.indexOf(el.name.text) === -1
),
];
if (ngcBuild) {
importStatement.importClause.namedBindings.elements = importStatement.importClause.namedBindings.elements.map(
(binding) => {
(binding: Identifier & { name?: { text: string } }) => {
if (binding.escapedText) {
binding.name = {
text: binding.escapedText,
text: binding.escapedText as string,
};
}
return binding;
@@ -71,7 +75,7 @@ function transformImports(file: SourceFile, ctx: TransformationContext, ngcBuild
export function importsTransformer(ngcBuild?: boolean) {
return (ctx: TransformationContext) => {
return (tsSourceFile) => {
return (tsSourceFile: SourceFile) => {
return transformImports(tsSourceFile, ctx, ngcBuild);
};
};

View File

@@ -1,32 +1,45 @@
import { canHaveDecorators, ClassDeclaration, factory, getDecorators as tsGetDecorators, SyntaxKind } from 'typescript';
import {
canHaveDecorators,
ClassDeclaration,
ClassElement,
ConstructorDeclaration,
factory,
getDecorators as tsGetDecorators,
MethodDeclaration,
SyntaxKind,
} from 'typescript';
import { transformMethod } from './methods';
import { transformProperty } from './properties';
export function transformMembers(cls: ClassDeclaration) {
export function transformMembers(cls: ClassDeclaration): ClassElement[] {
const propertyIndices: number[] = [];
const members = cls.members.map((member: any, index: number) => {
// only process decorated members
const members = cls.members.map((member, index) => {
const memberDecorators = canHaveDecorators(member) ? tsGetDecorators(member) : undefined;
if (!memberDecorators || !memberDecorators.length) return member;
switch (member.kind) {
case SyntaxKind.MethodDeclaration:
return transformMethod(member);
return transformMethod(member as MethodDeclaration) ?? member;
case SyntaxKind.PropertyDeclaration:
propertyIndices.push(index);
return member;
case SyntaxKind.Constructor:
return factory.createConstructorDeclaration(undefined, member.parameters, member.body);
case SyntaxKind.Constructor: {
const ctor = member as ConstructorDeclaration;
return factory.createConstructorDeclaration(undefined, ctor.parameters, ctor.body);
}
default:
return member; // in case anything gets here by accident...
return member;
}
});
}) as ClassElement[];
propertyIndices.forEach((i: number) => {
const [getter, setter] = transformProperty(members, i) as any;
members.push(getter, setter);
const result = transformProperty(members, i);
if (Array.isArray(result)) {
const [getter, setter] = result;
members.push(getter, setter);
}
});
propertyIndices.reverse().forEach((i) => members.splice(i, 1));

View File

@@ -1,4 +1,4 @@
import { Expression, factory, MethodDeclaration, SyntaxKind } from 'typescript';
import { Expression, factory, Identifier, MethodDeclaration, SyntaxKind } from 'typescript';
import { Logger } from '../../logger';
import {
@@ -12,9 +12,11 @@ import {
export function transformMethod(method: MethodDeclaration) {
if (!method) return;
const decorator = getDecorator(method),
decoratorName = getDecoratorName(decorator),
decoratorArgs = getDecoratorArgs(decorator);
const decorator = getDecorator(method);
if (!decorator) return;
const decoratorName = getDecoratorName(decorator);
const decoratorArgs = getDecoratorArgs(decorator);
try {
return factory.createMethodDeclaration(
@@ -27,13 +29,17 @@ export function transformMethod(method: MethodDeclaration) {
method.type,
factory.createBlock([factory.createReturnStatement(getMethodBlock(method, decoratorName, decoratorArgs))])
);
} catch (e) {
Logger.error('Error transforming method: ' + (method.name as any).text);
Logger.error(e.message);
} catch (e: unknown) {
Logger.error('Error transforming method: ' + (method.name as Identifier).text);
Logger.error(e instanceof Error ? e.message : String(e));
}
}
function getMethodBlock(method: MethodDeclaration, decoratorName: string, decoratorArgs: any): Expression {
function getMethodBlock(
method: MethodDeclaration,
decoratorName: string,
decoratorArgs: Record<string, string | number | boolean | string[]>
): Expression {
const decoratorMethod = getMethodsForDecorator(decoratorName)[0];
switch (decoratorName) {
@@ -46,14 +52,14 @@ function getMethodBlock(method: MethodDeclaration, decoratorName: string, decora
SyntaxKind.EqualsEqualsEqualsToken,
factory.createTrue()
),
method.body
method.body!
),
]);
default:
return factory.createCallExpression(factory.createIdentifier(decoratorMethod), undefined, [
factory.createThis(),
factory.createStringLiteral(decoratorArgs?.methodName || (method.name as any).text),
factory.createStringLiteral((decoratorArgs?.methodName as string) || (method.name as Identifier).text),
convertValueToLiteral(decoratorArgs),
factory.createIdentifier('arguments'),
]);

View File

@@ -1,10 +1,12 @@
import {
canHaveDecorators,
canHaveModifiers,
ClassDeclaration,
Decorator,
factory,
getDecorators as tsGetDecorators,
getModifiers as tsGetModifiers,
Identifier,
SourceFile,
SyntaxKind,
TransformationContext,
@@ -16,16 +18,15 @@ import { Logger } from '../../logger';
import { convertValueToLiteral, getDecorator, getDecoratorArgs, getDecoratorName } from '../helpers';
import { transformMembers } from './members';
function transformClass(cls: any, ngcBuild?: boolean) {
Logger.profile('transformClass: ' + cls.name.text);
function transformClass(cls: ClassDeclaration, ngcBuild?: boolean) {
Logger.profile('transformClass: ' + cls.name!.text);
const pluginStatics = [];
const dec: Decorator = getDecorator(cls);
const dec = getDecorator(cls);
if (dec) {
const pluginDecoratorArgs = getDecoratorArgs(dec);
// add plugin decorator args as static properties of the plugin's class
for (const prop in pluginDecoratorArgs) {
pluginStatics.push(
factory.createPropertyDeclaration(
@@ -45,7 +46,7 @@ function transformClass(cls: any, ngcBuild?: boolean) {
? clsDecorators.filter((d: Decorator) => getDecoratorName(d) === 'Injectable')
: [];
cls = factory.createClassDeclaration(
const result = factory.createClassDeclaration(
[...keepDecorators, factory.createToken(SyntaxKind.ExportKeyword)],
cls.name,
cls.typeParameters,
@@ -53,8 +54,8 @@ function transformClass(cls: any, ngcBuild?: boolean) {
[...transformMembers(cls), ...pluginStatics]
);
Logger.profile('transformClass: ' + cls.name.text);
return cls;
Logger.profile('transformClass: ' + (result.name as Identifier).text);
return result;
}
function transformClasses(file: SourceFile, ctx: TransformationContext, ngcBuild?: boolean) {
@@ -68,7 +69,7 @@ function transformClasses(file: SourceFile, ctx: TransformationContext, ngcBuild
) {
return node;
}
return transformClass(node, ngcBuild);
return transformClass(node as ClassDeclaration, ngcBuild);
},
ctx
);
@@ -76,7 +77,7 @@ function transformClasses(file: SourceFile, ctx: TransformationContext, ngcBuild
export function pluginClassTransformer(ngcBuild?: boolean): TransformerFactory<SourceFile> {
return (ctx: TransformationContext) => {
return (tsSourceFile) => {
return (tsSourceFile: SourceFile) => {
if (tsSourceFile.fileName.indexOf('src/@awesome-cordova-plugins/plugins') > -1) {
return transformClasses(tsSourceFile, ctx, ngcBuild);
}

View File

@@ -1,11 +1,21 @@
import { factory, PropertyDeclaration } from 'typescript';
import {
ClassElement,
factory,
GetAccessorDeclaration,
Identifier,
PropertyDeclaration,
SetAccessorDeclaration,
} from 'typescript';
import { getDecorator, getDecoratorName } from '../helpers';
export function transformProperty(members: any[], index: number) {
export function transformProperty(
members: ClassElement[],
index: number
): [GetAccessorDeclaration, SetAccessorDeclaration] | PropertyDeclaration {
const property = members[index] as PropertyDeclaration,
decorator = getDecorator(property),
decoratorName = getDecoratorName(decorator);
decoratorName = decorator ? getDecoratorName(decorator) : undefined;
let type: 'cordova' | 'instance';
@@ -22,6 +32,8 @@ export function transformProperty(members: any[], index: number) {
return property;
}
const propertyName = (property.name as Identifier).text;
const getter = factory.createGetAccessorDeclaration(
undefined,
property.name,
@@ -31,7 +43,7 @@ export function transformProperty(members: any[], index: number) {
factory.createReturnStatement(
factory.createCallExpression(factory.createIdentifier(type + 'PropertyGet'), undefined, [
factory.createThis(),
factory.createStringLiteral((property.name as any).text),
factory.createStringLiteral(propertyName),
])
),
])
@@ -45,7 +57,7 @@ export function transformProperty(members: any[], index: number) {
factory.createExpressionStatement(
factory.createCallExpression(factory.createIdentifier(type + 'PropertySet'), undefined, [
factory.createThis(),
factory.createStringLiteral((property.name as any).text),
factory.createStringLiteral(propertyName),
factory.createIdentifier('value'),
])
),

View File

@@ -2,16 +2,16 @@ import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { PLUGIN_PATHS, ROOT } from '../build/helpers';
import { EMIT_PATH } from '../build/transformers/extract-injectables';
import { InjectableClassEntry, EMIT_PATH } from '../build/transformers/extract-injectables';
import { generateDeclarations, transpile } from '../build/transpile';
generateDeclarations();
transpile();
const outDirs = PLUGIN_PATHS.map((p) => p.replace(join(ROOT, 'src'), join(ROOT, 'dist')).replace(/[\\/]index.ts/, ''));
const injectableClasses = JSON.parse(readFileSync(EMIT_PATH, 'utf-8'));
const injectableClasses: InjectableClassEntry[] = JSON.parse(readFileSync(EMIT_PATH, 'utf-8'));
outDirs.forEach((dir) => {
outDirs.forEach((dir: string) => {
const classes = injectableClasses.filter((entry) => entry.dirName === dir.split(/[\\/]+/).pop());
let jsFile: string = readFileSync(join(dir, 'index.js'), 'utf-8'),

View File

@@ -9,11 +9,28 @@ const exec = promisify(execCb);
import { PLUGIN_PATHS, ROOT } from '../build/helpers';
import { Logger } from '../logger';
interface PackageJson {
description: string;
type: string;
main: string;
module: string;
types: string;
exports: Record<string, { types: string; import: string; default: string } | undefined>;
sideEffects: boolean;
author: string;
license: string;
repository: { type: string; url: string };
name?: string;
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
version?: string;
}
const MAIN_PACKAGE_JSON = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
const VERSION = MAIN_PACKAGE_JSON.version;
const VERSION: string = MAIN_PACKAGE_JSON.version;
const FLAGS = '--access public --provenance';
const PACKAGE_JSON_BASE = {
const PACKAGE_JSON_BASE: PackageJson = {
description: 'Awesome Cordova Plugins - Native plugins for ionic apps',
type: 'module',
main: './index.js',
@@ -42,18 +59,22 @@ const PACKAGE_JSON_BASE = {
const DIST = resolve(ROOT, 'dist/@awesome-cordova-plugins');
const PACKAGES = [];
const PACKAGES: string[] = [];
const MIN_CORE_VERSION = '^' + VERSION;
const RXJS_VERSION = '^5.5.0 || ^6.5.0 || ^7.3.0';
const PLUGIN_PEER_DEPENDENCIES = {
const PLUGIN_PEER_DEPENDENCIES: Record<string, string> = {
'@awesome-cordova-plugins/core': MIN_CORE_VERSION,
rxjs: RXJS_VERSION,
};
function getPackageJsonContent(name: string, peerDependencies = {}, dependencies = {}) {
const pkg = {
function getPackageJsonContent(
name: string,
peerDependencies: Record<string, string> = {},
dependencies: Record<string, string> = {}
): PackageJson {
const pkg: PackageJson = {
...structuredClone(PACKAGE_JSON_BASE),
name: '@awesome-cordova-plugins/' + name,
dependencies,
@@ -61,7 +82,6 @@ function getPackageJsonContent(name: string, peerDependencies = {}, dependencies
version: VERSION,
};
// Core package has no ngx subfolder
if (name === 'core') {
delete pkg.exports['./ngx'];
}
@@ -69,23 +89,23 @@ function getPackageJsonContent(name: string, peerDependencies = {}, dependencies
return pkg;
}
function writePackageJson(data: any, dir: string) {
function writePackageJson(data: PackageJson, dir: string) {
const filePath = resolve(dir, 'package.json');
writeFileSync(filePath, JSON.stringify(data, null, 2));
PACKAGES.push(dir);
}
function writeNGXPackageJson(data: any, dir: string) {
function writeNGXPackageJson(data: PackageJson, dir: string) {
const filePath = resolve(dir, 'package.json');
writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function prepare() {
// write @awesome-cordova-plugins/core package.json
writePackageJson(
getPackageJsonContent('core', { rxjs: RXJS_VERSION }, { '@types/cordova': 'latest' }),
resolve(DIST, 'core')
);
// write plugin package.json files
PLUGIN_PATHS.forEach((pluginPath: string) => {
const pluginName = pluginPath.split(/[\/\\]+/).slice(-2)[0];
const packageJsonContents = getPackageJsonContent(pluginName, PLUGIN_PEER_DEPENDENCIES);
@@ -100,8 +120,9 @@ async function publishPackage(pkg: string, ignoreErrors: boolean): Promise<void>
try {
const { stdout } = await exec(`npm publish ${pkg} ${FLAGS}`);
if (stdout) Logger.verbose(stdout.trim());
} catch (err: any) {
if (err.message?.includes('You cannot publish over the previously published version')) {
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('You cannot publish over the previously published version')) {
Logger.verbose('Ignoring duplicate version error.');
return;
}