diff --git a/scripts/build/helpers.ts b/scripts/build/helpers.ts index 4d12dabb7..714529047 100644 --- a/scripts/build/helpers.ts +++ b/scripts/build/helpers.ts @@ -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 }>; + }; + 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 { + const properties = getRawDecoratorArgs(decorator); + const args: Record = {}; 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 | 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); } 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): 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']; diff --git a/scripts/build/transformers/extract-injectables.ts b/scripts/build/transformers/extract-injectables.ts index a665d2946..08fba715e 100644 --- a/scripts/build/transformers/extract-injectables.ts +++ b/scripts/build/transformers/extract-injectables.ts @@ -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], }); } diff --git a/scripts/build/transformers/imports.ts b/scripts/build/transformers/imports.ts index 2e5348567..23787af29 100644 --- a/scripts/build/transformers/imports.ts +++ b/scripts/build/transformers/imports.ts @@ -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>).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 | 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); }; }; diff --git a/scripts/build/transformers/members.ts b/scripts/build/transformers/members.ts index 58b544154..8d454baa5 100644 --- a/scripts/build/transformers/members.ts +++ b/scripts/build/transformers/members.ts @@ -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)); diff --git a/scripts/build/transformers/methods.ts b/scripts/build/transformers/methods.ts index 4208502cb..2e02941a7 100644 --- a/scripts/build/transformers/methods.ts +++ b/scripts/build/transformers/methods.ts @@ -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 +): 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'), ]); diff --git a/scripts/build/transformers/plugin-class.ts b/scripts/build/transformers/plugin-class.ts index 5229414f7..b7d97ac67 100644 --- a/scripts/build/transformers/plugin-class.ts +++ b/scripts/build/transformers/plugin-class.ts @@ -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 { return (ctx: TransformationContext) => { - return (tsSourceFile) => { + return (tsSourceFile: SourceFile) => { if (tsSourceFile.fileName.indexOf('src/@awesome-cordova-plugins/plugins') > -1) { return transformClasses(tsSourceFile, ctx, ngcBuild); } diff --git a/scripts/build/transformers/properties.ts b/scripts/build/transformers/properties.ts index d8097c3b7..96fdc6b27 100644 --- a/scripts/build/transformers/properties.ts +++ b/scripts/build/transformers/properties.ts @@ -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'), ]) ), diff --git a/scripts/tasks/build-esm.ts b/scripts/tasks/build-esm.ts index 057048584..22e69237c 100644 --- a/scripts/tasks/build-esm.ts +++ b/scripts/tasks/build-esm.ts @@ -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'), diff --git a/scripts/tasks/publish.ts b/scripts/tasks/publish.ts index dde6a2746..caa27c37e 100644 --- a/scripts/tasks/publish.ts +++ b/scripts/tasks/publish.ts @@ -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; + sideEffects: boolean; + author: string; + license: string; + repository: { type: string; url: string }; + name?: string; + dependencies?: Record; + peerDependencies?: Record; + 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 = { '@awesome-cordova-plugins/core': MIN_CORE_VERSION, rxjs: RXJS_VERSION, }; -function getPackageJsonContent(name: string, peerDependencies = {}, dependencies = {}) { - const pkg = { +function getPackageJsonContent( + name: string, + peerDependencies: Record = {}, + dependencies: Record = {} +): 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 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; }