From 6e90335405d091259442e86ef53ad1c49dcddf5d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 31 Aug 2026 23:53:11 +0000 Subject: [PATCH 1/2] fix: harden generated identifiers and shell command execution - reject operations whose camel-cased names collide - escape reserved words in generated method and binding names - avoid binding renames in type positions - replace string-interpolated shell commands with execFileSync/fs moves --- .../__snapshots__/safe-names.test.ts.snap | 73 +++++++++ .../ast/__tests__/utils/safe-names.test.ts | 149 ++++++++++++++++++ packages/ast/src/client/client.ts | 22 +-- .../src/message-builder/message-builder.ts | 4 +- .../src/message-composer/message-composer.ts | 10 +- packages/ast/src/react-query/react-query.ts | 11 +- packages/ast/src/recoil/recoil.ts | 3 +- packages/ast/src/utils/babel.ts | 17 ++ packages/ast/src/utils/index.ts | 1 + packages/ast/src/utils/names.ts | 84 ++++++++++ packages/ast/src/utils/types.ts | 27 +++- .../src/commands/create-boilerplate.ts | 3 +- packages/ts-codegen/src/commands/install.ts | 32 ++-- .../ts-codegen/src/helpers/create-helpers.ts | 2 +- packages/ts-codegen/src/helpers/index.ts | 2 +- 15 files changed, 397 insertions(+), 43 deletions(-) create mode 100644 packages/ast/__tests__/utils/__snapshots__/safe-names.test.ts.snap create mode 100644 packages/ast/__tests__/utils/safe-names.test.ts create mode 100644 packages/ast/src/utils/names.ts diff --git a/packages/ast/__tests__/utils/__snapshots__/safe-names.test.ts.snap b/packages/ast/__tests__/utils/__snapshots__/safe-names.test.ts.snap new file mode 100644 index 00000000..6a3d8191 --- /dev/null +++ b/packages/ast/__tests__/utils/__snapshots__/safe-names.test.ts.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`reserved schema names execute class escapes reserved identifiers 1`] = ` +"export class ReservedClient implements ReservedInstance { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this._constructor = this._constructor.bind(this); + } + _constructor = async ({ + class: _class, + default: _default, + delete: _delete + }: { + class: string; + default: string; + delete: boolean; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + constructor: { + class: _class, + default: _default, + delete: _delete + } + }, fee_, memo_, funds_); + }; +}" +`; + +exports[`reserved schema names execute interface escapes reserved identifiers 1`] = ` +"export interface ReservedInstance { + contractAddress: string; + sender: string; + _constructor: (params: { + class: string; + default: string; + delete: boolean; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +}" +`; + +exports[`reserved schema names query class escapes reserved identifiers 1`] = ` +"export class ReservedQueryClient implements ReservedReadOnlyInstance { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this._constructor = this._constructor.bind(this); + } + _constructor = async ({ + class: _class, + default: _default, + delete: _delete + }: { + class: string; + default: string; + delete: boolean; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + constructor: { + class: _class, + default: _default, + delete: _delete + } + }); + }; +}" +`; diff --git a/packages/ast/__tests__/utils/safe-names.test.ts b/packages/ast/__tests__/utils/safe-names.test.ts new file mode 100644 index 00000000..aaa103b5 --- /dev/null +++ b/packages/ast/__tests__/utils/safe-names.test.ts @@ -0,0 +1,149 @@ +import { ExecuteMsg, QueryMsg } from '@cosmwasm/ts-codegen-types'; + +import { + createExecuteClass, + createExecuteInterface, + createQueryClass, +} from '../../src'; +import { getMessageProperties } from '../../src/utils'; +import { camelMethodName, camelVarName, varName } from '../../src/utils/names'; +import { expectCode, makeContext } from '../../test-utils'; + +describe('name helpers', () => { + it('escapes reserved words for bindings', () => { + expect(varName('class')).toBe('_class'); + expect(varName('default')).toBe('_default'); + expect(varName('delete')).toBe('_delete'); + expect(varName('owner')).toBe('owner'); + }); + + it('escapes unsafe member names', () => { + expect(camelMethodName('constructor')).toBe('_constructor'); + expect(camelMethodName('transfer_nft')).toBe('transferNft'); + expect(camelVarName('default')).toBe('_default'); + expect(camelVarName('token_id')).toBe('tokenId'); + }); +}); + +describe('operation name collisions', () => { + const collidingMsg: ExecuteMsg = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'ExecuteMsg', + oneOf: [ + { + type: 'object', + required: ['transfer_nft'], + properties: { + transfer_nft: { + type: 'object', + required: ['recipient', 'token_id'], + properties: { + recipient: { type: 'string' }, + token_id: { type: 'string' }, + }, + }, + }, + additionalProperties: false, + }, + { + type: 'object', + required: ['transferNft'], + properties: { + transferNft: { + type: 'object', + required: ['recipient', 'token_id'], + properties: { + recipient: { type: 'string' }, + token_id: { type: 'string' }, + }, + }, + }, + additionalProperties: false, + }, + ], + }; + + it('throws on ambiguous normalized operation names', () => { + expect(() => getMessageProperties(collidingMsg)).toThrow( + /Operation name collision/ + ); + }); +}); + +describe('reserved schema names', () => { + const queryMsg: QueryMsg = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'QueryMsg', + oneOf: [ + { + type: 'object', + required: ['constructor'], + properties: { + constructor: { + type: 'object', + required: ['class', 'default', 'delete'], + properties: { + class: { type: 'string' }, + default: { type: 'string' }, + delete: { type: 'boolean' }, + }, + }, + }, + additionalProperties: false, + }, + ], + }; + + const executeMsg: ExecuteMsg = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'ExecuteMsg', + oneOf: [ + { + type: 'object', + required: ['constructor'], + properties: { + constructor: { + type: 'object', + required: ['class', 'default', 'delete'], + properties: { + class: { type: 'string' }, + default: { type: 'string' }, + delete: { type: 'boolean' }, + }, + }, + }, + additionalProperties: false, + }, + ], + }; + + it('query class escapes reserved identifiers', () => { + const ctx = makeContext(queryMsg); + expectCode( + createQueryClass( + ctx, + 'ReservedQueryClient', + 'ReservedReadOnlyInstance', + queryMsg + ) + ); + }); + + it('execute class escapes reserved identifiers', () => { + const ctx = makeContext(executeMsg); + expectCode( + createExecuteClass( + ctx, + 'ReservedClient', + 'ReservedInstance', + null, + executeMsg + ) + ); + }); + + it('execute interface escapes reserved identifiers', () => { + const ctx = makeContext(executeMsg); + expectCode(createExecuteInterface(ctx, 'ReservedInstance', null, executeMsg)); + }); +}); diff --git a/packages/ast/src/client/client.ts b/packages/ast/src/client/client.ts index f877cf73..a8d6cf7b 100644 --- a/packages/ast/src/client/client.ts +++ b/packages/ast/src/client/client.ts @@ -6,6 +6,8 @@ import { RenderContext } from '../context'; import { arrowFunctionExpression, bindMethod, + camelMethodName, + camelVarName, classDeclaration, classProperty, FIXED_EXECUTE_PARAMS, @@ -47,7 +49,7 @@ export const createWasmQueryMethod = ( jsonschema: any ) => { const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const responseType = getResponseType(context, underscoreName); const param = createTypedObjectParams( @@ -113,7 +115,7 @@ export const createQueryClass = ( .map((method) => Object.keys(method.properties)?.[0]) .filter(Boolean); - const bindings = propertyNames.map(camel).map(bindMethod); + const bindings = propertyNames.map(camelMethodName).map(bindMethod); const methods = getMessageProperties(queryMsg).map((schema) => { return createWasmQueryMethod(context, schema); @@ -205,9 +207,9 @@ export const getWasmMethodArgs = ( const args = keys.map((prop) => { return t.objectProperty( t.identifier(prop), - t.identifier(camel(prop)), + t.identifier(camelVarName(prop)), false, - prop === camel(prop) + prop === camelVarName(prop) ); }); @@ -222,7 +224,7 @@ export const createWasmExecMethod = ( context.addUtil('Coin'); const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const param = createTypedObjectParams( context, jsonschema.properties[underscoreName] @@ -301,7 +303,7 @@ export const createExecuteClass = ( .map((method) => Object.keys(method.properties)?.[0]) .filter(Boolean); - const bindings = propertyNames.map(camel).map(bindMethod); + const bindings = propertyNames.map(camelMethodName).map(bindMethod); const methods = getMessageProperties(execMsg).map((schema) => { return createWasmExecMethod(context, schema); @@ -426,7 +428,7 @@ export const createExecuteInterface = ( ) => { const methods = getMessageProperties(execMsg).map((jsonschema) => { const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); return createPropertyFunctionWithObjectParamsForExec( context, methodName, @@ -469,7 +471,7 @@ export const createPropertyFunctionWithObjectParams = ( responseType: string, jsonschema: JSONSchema ) => { - const obj = createTypedObjectParams(context, jsonschema); + const obj = createTypedObjectParams(context, jsonschema, true, true); const func = { type: 'TSFunctionType', @@ -494,7 +496,7 @@ export const createPropertyFunctionWithObjectParamsForExec = ( ) => { context.addUtil('Coin'); - const obj = createTypedObjectParams(context, jsonschema); + const obj = createTypedObjectParams(context, jsonschema, true, true); const func = { type: 'TSFunctionType', @@ -518,7 +520,7 @@ export const createQueryInterface = ( ) => { const methods = getMessageProperties(queryMsg).map((jsonschema) => { const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const responseType = getResponseType(context, underscoreName); return createPropertyFunctionWithObjectParams( context, diff --git a/packages/ast/src/message-builder/message-builder.ts b/packages/ast/src/message-builder/message-builder.ts index 2fbd3f19..00fec07b 100644 --- a/packages/ast/src/message-builder/message-builder.ts +++ b/packages/ast/src/message-builder/message-builder.ts @@ -1,13 +1,13 @@ import * as t from '@babel/types'; import { Expression } from '@babel/types'; import { ExecuteMsg, QueryMsg } from '@cosmwasm/ts-codegen-types'; -import { camel } from 'case'; import { getWasmMethodArgs } from '../client/client'; import { RenderContext } from '../context'; import { abstractClassDeclaration, arrowFunctionExpression, + camelMethodName, getMessageProperties, } from '../utils'; import { createTypedObjectParams } from '../utils/types'; @@ -62,7 +62,7 @@ const createStaticExecMethodMessageBuilder = ( msgTitle: string ) => { const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const param = createTypedObjectParams( context, jsonschema.properties[underscoreName] diff --git a/packages/ast/src/message-composer/message-composer.ts b/packages/ast/src/message-composer/message-composer.ts index bcf7ffbf..7594d846 100644 --- a/packages/ast/src/message-composer/message-composer.ts +++ b/packages/ast/src/message-composer/message-composer.ts @@ -1,13 +1,13 @@ import * as t from '@babel/types'; import { Expression } from '@babel/types'; import { ExecuteMsg, JSONSchema } from '@cosmwasm/ts-codegen-types'; -import { camel } from 'case'; import { getWasmMethodArgs } from '../client/client'; import { RenderContext } from '../context'; import { arrowFunctionExpression, bindMethod, + camelMethodName, classDeclaration, classProperty, getMessageProperties, @@ -26,7 +26,7 @@ const createWasmExecMethodMessageComposer = ( context.addUtil('toUtf8'); const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const param = createTypedObjectParams( context, jsonschema.properties[underscoreName] @@ -133,7 +133,7 @@ export const createMessageComposerClass = ( .map((method) => Object.keys(method.properties)?.[0]) .filter(Boolean); - const bindings = propertyNames.map(camel).map(bindMethod); + const bindings = propertyNames.map(camelMethodName).map(bindMethod); const methods = getMessageProperties(execMsg).map((schema) => { return createWasmExecMethodMessageComposer(context, schema); @@ -205,7 +205,7 @@ export const createMessageComposerInterface = ( ) => { const methods = getMessageProperties(execMsg).map((jsonschema) => { const underscoreName = Object.keys(jsonschema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); return createPropertyFunctionWithObjectParamsForMessageComposer( context, methodName, @@ -246,7 +246,7 @@ const createPropertyFunctionWithObjectParamsForMessageComposer = ( responseType: string, jsonschema: JSONSchema ) => { - const obj = createTypedObjectParams(context, jsonschema); + const obj = createTypedObjectParams(context, jsonschema, true, true); const fixedParams = [OPTIONAL_FUNDS_PARAM]; const func = { type: 'TSFunctionType', diff --git a/packages/ast/src/react-query/react-query.ts b/packages/ast/src/react-query/react-query.ts index 1a9e62d5..036307f1 100644 --- a/packages/ast/src/react-query/react-query.ts +++ b/packages/ast/src/react-query/react-query.ts @@ -8,6 +8,7 @@ import { RenderContext } from '../context'; import { ReactQueryOptions } from '../types'; import { callExpression, + camelMethodName, createTypedObjectParams, getMessageProperties, identifier, @@ -85,7 +86,7 @@ export const createReactQueryHooks = ({ // list_voters const underscoreName = Object.keys(schema.properties)[0]; // listVoters - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); // Cw3FlexMultisigListVotersQuery const hookParamsTypeName = `${pascal(contractName)}${pascal( methodName @@ -475,7 +476,7 @@ export const createReactQueryMutationHooks = ({ // update_members const execMethodUnderscoreName = Object.keys(schema.properties)[0]; // updateMembers - const execMethodName = camel(execMethodUnderscoreName); + const execMethodName = camelMethodName(execMethodUnderscoreName); // Cw20UpdateMembersMutation const mutationHookParamsTypeName = `${pascal(contractName)}${pascal( execMethodName @@ -701,7 +702,7 @@ function createReactQueryKeys({ ...underscoreNames.map((underscoreMethodName) => t.objectProperty( // key id is the camel method name - t.identifier(camel(underscoreMethodName)), + t.identifier(camelMethodName(underscoreMethodName)), t.arrowFunctionExpression( [ identifier('contractAddress', contractAddressTypeAnnotation), @@ -856,7 +857,7 @@ function createReactQueryFactory({ return t.objectProperty( // key id is the camel method name - t.identifier(camel(methodName)), + t.identifier(camelMethodName(methodName)), methodQueryOptionsFn ); } @@ -1054,7 +1055,7 @@ const generateUseQueryQueryKey = ({ return t.callExpression( t.memberExpression( t.identifier(queryKeysName), - t.identifier(camel(methodName)) + t.identifier(camelMethodName(methodName)) ), callArgs ); diff --git a/packages/ast/src/recoil/recoil.ts b/packages/ast/src/recoil/recoil.ts index bcd5d5da..3923223e 100644 --- a/packages/ast/src/recoil/recoil.ts +++ b/packages/ast/src/recoil/recoil.ts @@ -5,6 +5,7 @@ import { camel, pascal } from 'case'; import { RenderContext } from '../context'; import { callExpression, + camelMethodName, getMessageProperties, getResponseType, } from '../utils'; @@ -120,7 +121,7 @@ export const createRecoilSelectors = ( ): t.ExportNamedDeclaration[] => { return getMessageProperties(queryMsg).map((schema: JSONSchema) => { const underscoreName = Object.keys(schema.properties)[0]; - const methodName = camel(underscoreName); + const methodName = camelMethodName(underscoreName); const responseType = getResponseType(context, underscoreName); return createRecoilSelector( diff --git a/packages/ast/src/utils/babel.ts b/packages/ast/src/utils/babel.ts index 053b95fb..427b9b30 100644 --- a/packages/ast/src/utils/babel.ts +++ b/packages/ast/src/utils/babel.ts @@ -4,6 +4,7 @@ import { Field } from '@cosmwasm/ts-codegen-types'; import { JSONSchema } from '@cosmwasm/ts-codegen-types'; import { snake } from 'case'; +import { camelMethodName } from './names'; import { refLookup } from './ref'; // t.TSPropertySignature - kind? @@ -63,6 +64,22 @@ export const getMessageProperties = (msg: JSONSchema): JSONSchema[] => { } } + const seen: Record = {}; + for (const result of results) { + const key = Object.keys(result.properties ?? {})[0]; + if (!key) continue; + const methodName = camelMethodName(key); + const prev = seen[methodName]; + if (prev !== undefined && prev !== key) { + throw new Error( + `Operation name collision in "${msg.title ?? 'message'}": ` + + `"${prev}" and "${key}" both normalize to "${methodName}". ` + + `Rename one of the operations so the generated members are unique.` + ); + } + seen[methodName] = key; + } + return results; }; diff --git a/packages/ast/src/utils/index.ts b/packages/ast/src/utils/index.ts index 2159272b..d06b04b3 100644 --- a/packages/ast/src/utils/index.ts +++ b/packages/ast/src/utils/index.ts @@ -2,5 +2,6 @@ export * from './babel'; export { OPTIONAL_FUNDS_PARAM } from './constants'; export { FIXED_EXECUTE_PARAMS } from './constants'; export { PROVIDER_TYPES } from './constants'; +export * from './names'; export * from './ref'; export * from './types'; diff --git a/packages/ast/src/utils/names.ts b/packages/ast/src/utils/names.ts new file mode 100644 index 00000000..9d617005 --- /dev/null +++ b/packages/ast/src/utils/names.ts @@ -0,0 +1,84 @@ +import { camel } from 'case'; + +/** + * Words that cannot be used as binding identifiers (variable names, + * destructuring bindings) in JavaScript/TypeScript, including strict-mode + * reserved words. + */ +const RESERVED_WORDS = new Set([ + 'arguments', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]); + +export const isReservedWord = (name: string): boolean => + RESERVED_WORDS.has(name); + +/** + * A name that is safe to use as a binding identifier (variable or + * destructuring binding). Reserved words are prefixed with an underscore. + */ +export const varName = (name: string): string => + isReservedWord(name) ? `_${name}` : name; + +/** + * Camel-cased name that is safe to use as a binding identifier. + */ +export const camelVarName = (prop: string): string => varName(camel(prop)); + +/** + * Camel-cased name that is safe to use as a class member or interface + * member. `constructor` is escaped in addition to reserved words, since a + * class field or method with that name collides with the class constructor. + */ +export const camelMethodName = (underscoreName: string): string => { + const name = camel(underscoreName); + return name === 'constructor' || name === 'prototype' || isReservedWord(name) + ? `_${name}` + : name; +}; diff --git a/packages/ast/src/utils/types.ts b/packages/ast/src/utils/types.ts index f6203a0b..c87878ec 100644 --- a/packages/ast/src/utils/types.ts +++ b/packages/ast/src/utils/types.ts @@ -4,6 +4,7 @@ import { camel, pascal } from 'case'; import { RenderContext } from '../context'; import { propertySignature } from './babel'; +import { varName } from './names'; export function getResponseType( context: RenderContext, @@ -414,7 +415,8 @@ export const getParamsTypeAnnotation = ( export const createTypedObjectParams = ( context: RenderContext, jsonschema: JSONSchema, - camelize: boolean = true + camelize: boolean = true, + inType: boolean = false ): t.Identifier | t.Pattern | t.RestElement => { const keys = Object.keys(jsonschema.properties ?? {}); if (!keys.length) { @@ -432,7 +434,7 @@ export const createTypedObjectParams = ( ); return id; } else if (obj) { - return createTypedObjectParams(context, obj, camelize); + return createTypedObjectParams(context, obj, camelize, inType); } } @@ -440,12 +442,25 @@ export const createTypedObjectParams = ( return; } - const params = keys.map((prop) => { + const bindings = keys.map((prop) => { + const key = camelize ? camel(prop) : prop; + return { key, value: varName(key) }; + }); + + // binding renames are not valid in type positions, so use a plain + // parameter name when any key is not usable as a binding identifier + if (inType && bindings.some(({ key, value }) => key !== value)) { + const id = t.identifier('params'); + id.typeAnnotation = getParamsTypeAnnotation(context, jsonschema, camelize); + return id; + } + + const params = bindings.map(({ key, value }) => { return t.objectProperty( - camelize ? t.identifier(camel(prop)) : t.identifier(prop), - camelize ? t.identifier(camel(prop)) : t.identifier(prop), + t.identifier(key), + t.identifier(value), false, - true + key === value ); }); diff --git a/packages/ts-codegen/src/commands/create-boilerplate.ts b/packages/ts-codegen/src/commands/create-boilerplate.ts index cec4fb2d..c4692a78 100644 --- a/packages/ts-codegen/src/commands/create-boilerplate.ts +++ b/packages/ts-codegen/src/commands/create-boilerplate.ts @@ -1,4 +1,5 @@ import { MinimistArgs } from '@cosmwasm/ts-codegen-types'; +import { execFileSync } from 'child_process'; import dargs from 'dargs'; import { lstatSync, readFileSync, writeFileSync } from 'fs'; import { globSync as glob } from 'glob'; @@ -25,7 +26,7 @@ export default async (argv: MinimistArgs) => { argv ); - shell.exec(`git clone ${repo} ${name}`); + execFileSync('git', ['clone', repo, '--', name], { stdio: 'inherit' }); shell.cd(name); const questions = JSON.parse(readFileSync(`.questions.json`, 'utf-8')); diff --git a/packages/ts-codegen/src/commands/install.ts b/packages/ts-codegen/src/commands/install.ts index c4f08d7d..aa811df4 100644 --- a/packages/ts-codegen/src/commands/install.ts +++ b/packages/ts-codegen/src/commands/install.ts @@ -1,12 +1,12 @@ import { MinimistArgs } from '@cosmwasm/ts-codegen-types'; -import { readFileSync, writeFileSync } from 'fs'; +import { execFileSync } from 'child_process'; +import { cpSync, readFileSync, renameSync, writeFileSync } from 'fs'; import { globSync as glob } from 'glob'; import { sync as mkdirp } from 'mkdirp'; import { tmpdir } from 'os'; import { parse } from 'parse-package-name'; import { basename, dirname, extname, join, resolve } from 'path'; import { sync as rimraf } from 'rimraf'; -import { exec } from 'shelljs'; import { prompt } from '../utils/prompt'; @@ -16,12 +16,20 @@ const rnd = () => Math.random().toString(36).substring(2, 15); const getPackages = (names: string[]) => { - return names - .map((pkg) => { - const { name, version } = parse(pkg); - return `${name}@${version}`; - }) - .join(' '); + return names.map((pkg) => { + const { name, version } = parse(pkg); + return `${name}@${version}`; + }); +}; + +const move = (src: string, dst: string) => { + try { + renameSync(src, dst); + } catch { + // fall back to copy + remove across devices + cpSync(src, dst, { recursive: true }); + rimraf(src); + } }; export default async (argv: MinimistArgs) => { @@ -73,8 +81,10 @@ export default async (argv: MinimistArgs) => { const tmp = join(TMPDIR, rnd()); mkdirp(tmp); process.chdir(tmp); - exec( - `npm install ${getPackages(pkg)} --production --prefix ./smart-contracts` + execFileSync( + 'npm', + ['install', ...getPackages(pkg), '--production', '--prefix', './smart-contracts'], + { stdio: 'inherit' } ); // protos @@ -108,7 +118,7 @@ export default async (argv: MinimistArgs) => { rimraf(dst); console.log(`installing ${pkg}...`); mkdirp(dirname(dst)); - exec(`mv ${src} ${dst}`); + move(src, dst); } // package diff --git a/packages/ts-codegen/src/helpers/create-helpers.ts b/packages/ts-codegen/src/helpers/create-helpers.ts index 4db9e8db..d355b53f 100644 --- a/packages/ts-codegen/src/helpers/create-helpers.ts +++ b/packages/ts-codegen/src/helpers/create-helpers.ts @@ -4,10 +4,10 @@ import { basename, dirname, extname, join } from 'path'; import { BuilderFile, TSBuilderInput } from '../builder'; import { + baseClient, contractContextBase, contractContextBaseShortHandCtor, contractsContextTSX, - baseClient, } from '../helpers'; import { writeContentToFile } from '../utils/files'; import { header } from '../utils/header'; diff --git a/packages/ts-codegen/src/helpers/index.ts b/packages/ts-codegen/src/helpers/index.ts index 87878c72..0fd9dff2 100644 --- a/packages/ts-codegen/src/helpers/index.ts +++ b/packages/ts-codegen/src/helpers/index.ts @@ -1,4 +1,4 @@ +export * from './baseClient'; export * from './contractContextBase'; export * from './contractContextBaseShortHandCtor'; -export * from './baseClient'; export * from './contractsContextTSX'; From 0b9f453ab507f0cc4e2c26f788ff842510df4395 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:27:36 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- packages/ast/__tests__/utils/safe-names.test.ts | 4 +++- packages/ts-codegen/src/commands/install.ts | 8 +++++++- yarn.lock | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/ast/__tests__/utils/safe-names.test.ts b/packages/ast/__tests__/utils/safe-names.test.ts index aaa103b5..73597a01 100644 --- a/packages/ast/__tests__/utils/safe-names.test.ts +++ b/packages/ast/__tests__/utils/safe-names.test.ts @@ -144,6 +144,8 @@ describe('reserved schema names', () => { it('execute interface escapes reserved identifiers', () => { const ctx = makeContext(executeMsg); - expectCode(createExecuteInterface(ctx, 'ReservedInstance', null, executeMsg)); + expectCode( + createExecuteInterface(ctx, 'ReservedInstance', null, executeMsg) + ); }); }); diff --git a/packages/ts-codegen/src/commands/install.ts b/packages/ts-codegen/src/commands/install.ts index aa811df4..e9cdbc31 100644 --- a/packages/ts-codegen/src/commands/install.ts +++ b/packages/ts-codegen/src/commands/install.ts @@ -83,7 +83,13 @@ export default async (argv: MinimistArgs) => { process.chdir(tmp); execFileSync( 'npm', - ['install', ...getPackages(pkg), '--production', '--prefix', './smart-contracts'], + [ + 'install', + ...getPackages(pkg), + '--production', + '--prefix', + './smart-contracts', + ], { stdio: 'inherit' } ); diff --git a/yarn.lock b/yarn.lock index c8427f2d..12ddadb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6157,10 +6157,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -nested-obj@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/nested-obj/-/nested-obj-0.0.1.tgz#efe1da127c3d00826fa10ec25673e0f9ec1224fd" - integrity sha512-kB1WKTng+IePQhZVs1UXtFaHBx4QEM5a0XKGAzYfCKvdx5DhNjCytNDWMUGpNNpHLotln+tiwcA52kWCIgGq1Q== +nested-obj@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/nested-obj/-/nested-obj-0.2.2.tgz#70a16509fc9ce7d7374ff076642ef48ac02897e2" + integrity sha512-M1etu+T6Ai9Bo06L3K3nWD0ytZWltggBGsrxJlOGvMNGlCA4fokUVlbPKoWzsiiRX+PXq6Cb1xFEn4chiyC7MQ== next-tick@^1.1.0: version "1.1.0"