diff --git a/package.json b/package.json index 52bce206a..1ea3d0c6e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "babel-jest": "^29.7.0", "brotli-size": "^4.0.0", "chalk": "~4.1.2", + "ejs": "^3.1.10", "enquirer": "^2.3.6", "esbuild": "0.25.2", "eslint": "^9.24.0", @@ -73,8 +74,7 @@ "tsc-watch": "^4.2.9", "tslib": "^2.3.1", "typescript": "~5.8.2", - "wsrun": "^5.2.1", - "zombi": "^3.3.0" + "wsrun": "^5.2.1" }, "workspaces": { "packages": [ diff --git a/packages/@magic-ext/aptos/jest.config.ts b/packages/@magic-ext/aptos/jest.config.ts index 433182c4b..2bff5a3e6 100644 --- a/packages/@magic-ext/aptos/jest.config.ts +++ b/packages/@magic-ext/aptos/jest.config.ts @@ -8,6 +8,10 @@ const config: Config.InitialOptions = { '\\.(ts|tsx)$': 'ts-jest', }, coveragePathIgnorePatterns: ['index.cdn.ts', 'index.native.ts'], + moduleNameMapper: { + '^@aptos-labs/wallet-adapter-core$': '/test/__mocks__/@aptos-labs/wallet-adapter-core.ts', + '^@aptos-labs/wallet-standard$': '/test/__mocks__/@aptos-labs/wallet-standard.ts', + }, }; export default config; diff --git a/packages/@magic-ext/aptos/package.json b/packages/@magic-ext/aptos/package.json index 82e9136d5..3cd344228 100644 --- a/packages/@magic-ext/aptos/package.json +++ b/packages/@magic-ext/aptos/package.json @@ -27,12 +27,12 @@ ] }, "devDependencies": { - "@aptos-labs/wallet-adapter-core": "^2.2.0", + "@aptos-labs/wallet-adapter-core": "^7.10.1", "@magic-sdk/provider": "^31.2.0", - "aptos": "^1.8.5" + "aptos": "^1.22.1" }, "peerDependencies": { - "@aptos-labs/wallet-adapter-core": "^2.2.0", - "aptos": "^1.8.5" + "@aptos-labs/wallet-adapter-core": "^7.10.1", + "aptos": "^1.22.1" } } diff --git a/packages/@magic-ext/aptos/src/MagicAptosWallet.ts b/packages/@magic-ext/aptos/src/MagicAptosWallet.ts index 52ab505cd..a6ca8b21b 100644 --- a/packages/@magic-ext/aptos/src/MagicAptosWallet.ts +++ b/packages/@magic-ext/aptos/src/MagicAptosWallet.ts @@ -1,19 +1,17 @@ import { AccountInfo, - AdapterPlugin, NetworkInfo, - SignMessagePayload, - SignMessageResponse, WalletInfo, WalletReadyState, } from '@aptos-labs/wallet-adapter-core'; +import { AptosSignMessageInput, AptosSignMessageOutput, AptosWallet } from '@aptos-labs/wallet-standard'; import { TxnBuilderTypes, Types } from 'aptos'; import { InstanceWithExtensions, SDKBase } from '@magic-sdk/provider'; import { AptosExtension } from '.'; import { APTOS_NETWORKS, APTOS_NODE_URLS, APTOS_WALLET_NAME, ICON_BASE64 } from './constants'; import { MagicAptosWalletConfig } from './type'; -export class MagicAptosWallet implements AdapterPlugin { +export class MagicAptosWallet implements Partial { readonly name = APTOS_WALLET_NAME; readonly url = 'https://magic.link/'; readonly icon = ICON_BASE64; @@ -23,7 +21,7 @@ export class MagicAptosWallet implements AdapterPlugin { provider: InstanceWithExtensions | undefined; config?: MagicAptosWalletConfig; - readyState?: WalletReadyState = WalletReadyState.Loadable; + readyState?: WalletReadyState = WalletReadyState.Installed; private accountInfo: AccountInfo | null; @@ -106,7 +104,7 @@ export class MagicAptosWallet implements AdapterPlugin { return this.provider.aptos.signAndSubmitBCSTransaction(accountInfo.address, transaction); } - async signMessage(message: SignMessagePayload): Promise { + async signMessage(message: AptosSignMessageInput): Promise { if (!this.provider) { throw new Error('Provider is not defined'); } @@ -115,7 +113,7 @@ export class MagicAptosWallet implements AdapterPlugin { return this.provider.aptos.signMessage(accountInfo.address, message); } - async signMessageAndVerify(message: SignMessagePayload): Promise { + async signMessageAndVerify(message: AptosSignMessageInput): Promise { if (!this.provider) { throw new Error('Provider is not defined'); } diff --git a/packages/@magic-ext/aptos/src/constants/index.ts b/packages/@magic-ext/aptos/src/constants/index.ts index 4ea465903..2d77c4efd 100644 --- a/packages/@magic-ext/aptos/src/constants/index.ts +++ b/packages/@magic-ext/aptos/src/constants/index.ts @@ -1,6 +1,6 @@ -import { NetworkName, WalletName } from '@aptos-labs/wallet-adapter-core'; +import { NetworkName } from '@aptos-labs/wallet-adapter-core'; -export const APTOS_WALLET_NAME = 'Magic' as WalletName<'Magic'>; +export const APTOS_WALLET_NAME = 'Magic' as const; export const APTOS_NODE_URLS = { MAINNET: 'https://fullnode.mainnet.aptoslabs.com', @@ -11,16 +11,17 @@ export const APTOS_NODE_URLS = { export const APTOS_NETWORKS = { [APTOS_NODE_URLS.MAINNET]: { name: NetworkName.Mainnet, - chainId: '1', + chainId: 1, url: APTOS_NODE_URLS.MAINNET, }, [APTOS_NODE_URLS.TESTNET]: { name: NetworkName.Testnet, - chainId: '2', + chainId: 2, url: APTOS_NODE_URLS.TESTNET, }, [APTOS_NODE_URLS.DEVNET]: { name: NetworkName.Devnet, + chainId: 34, url: APTOS_NODE_URLS.DEVNET, }, }; diff --git a/packages/@magic-ext/aptos/src/index.ts b/packages/@magic-ext/aptos/src/index.ts index 0f3139588..c0c25c720 100644 --- a/packages/@magic-ext/aptos/src/index.ts +++ b/packages/@magic-ext/aptos/src/index.ts @@ -1,8 +1,8 @@ import { MultichainExtension } from '@magic-sdk/provider'; -// @ts-ignore import { AptosClient, BCS, TxnBuilderTypes, Types, getAddressFromAccountOrAddress } from 'aptos'; -import { AccountInfo, SignMessagePayload, SignMessageResponse } from '@aptos-labs/wallet-adapter-core'; +import { AccountInfo } from '@aptos-labs/wallet-adapter-core'; +import { AptosSignMessageInput, AptosSignMessageOutput } from '@aptos-labs/wallet-standard'; import { AptosConfig, AptosPayloadMethod } from './type'; import { APTOS_PAYLOAD_TYPE } from './constants'; @@ -106,8 +106,8 @@ export class AptosExtension extends MultichainExtension<'aptos'> { ); }; - signMessage = async (address: string, message: SignMessagePayload): Promise => { - return this.request( + signMessage = async (address: string, message: AptosSignMessageInput): Promise => { + return this.request( this.utils.createJsonRpcRequestPayload(AptosPayloadMethod.AptosSignMessage, [ { address, @@ -117,7 +117,7 @@ export class AptosExtension extends MultichainExtension<'aptos'> { ); }; - signMessageAndVerify = async (address: string, message: SignMessagePayload): Promise => { + signMessageAndVerify = async (address: string, message: AptosSignMessageInput): Promise => { return this.request( this.utils.createJsonRpcRequestPayload(AptosPayloadMethod.AptosSignMessageAndVerify, [ { diff --git a/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-adapter-core.ts b/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-adapter-core.ts new file mode 100644 index 000000000..446943817 --- /dev/null +++ b/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-adapter-core.ts @@ -0,0 +1,32 @@ +// Mock for @aptos-labs/wallet-adapter-core +export enum NetworkName { + Mainnet = 'mainnet', + Testnet = 'testnet', + Devnet = 'devnet', +} + +export enum WalletReadyState { + Installed = 'Installed', + NotDetected = 'NotDetected', + Loadable = 'Loadable', + Unsupported = 'Unsupported', +} + +export interface AccountInfo { + address: string; + publicKey: string | string[]; + minKeysRequired?: number; + ansName?: string | null; +} + +export interface NetworkInfo { + name: NetworkName; + chainId?: number; + url?: string; +} + +export interface WalletInfo { + name: string; + icon: string; + url: string; +} diff --git a/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-standard.ts b/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-standard.ts new file mode 100644 index 000000000..080434c36 --- /dev/null +++ b/packages/@magic-ext/aptos/test/__mocks__/@aptos-labs/wallet-standard.ts @@ -0,0 +1,26 @@ +// Mock for @aptos-labs/wallet-standard +export interface AptosSignMessageInput { + address?: boolean; + application?: boolean; + chainId?: boolean; + message: string; + nonce: string; +} + +export interface AptosSignMessageOutput { + address?: string; + application?: string; + chainId?: number; + fullMessage: string; + message: string; + nonce: string; + prefix: string; + signature: string; + bitmap?: Uint8Array; +} + +export interface AptosWallet { + readonly name: string; + readonly url: string; + readonly icon: string; +} diff --git a/packages/@magic-ext/aptos/test/setup.ts b/packages/@magic-ext/aptos/test/setup.ts index 6430bda21..35ae08a6d 100644 --- a/packages/@magic-ext/aptos/test/setup.ts +++ b/packages/@magic-ext/aptos/test/setup.ts @@ -1,6 +1,40 @@ // NOTE: This module is automatically included at the top of each test file. import { mockConsole } from '../../../../scripts/utils/mock-console'; +import { TextEncoder, TextDecoder } from 'util'; + +// Polyfill TextEncoder/TextDecoder for @aptos-labs/ts-sdk +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder as typeof global.TextDecoder; + +// Mock Aptos API responses +const mockAptosApiResponse = (url: string) => { + // Mock account response + if (url.includes('/accounts/')) { + return { + sequence_number: '0', + authentication_key: '0x8293d5e05544c6e53c47fc19ae071c26a60e0ccbd8a12eb5b2c9d348c85227b6', + }; + } + // Mock modules response + if (url.includes('/modules')) { + return []; + } + // Default response + return {}; +}; + +// Polyfill fetch for jsdom environment +global.fetch = jest.fn((url: string) => + Promise.resolve({ + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(mockAptosApiResponse(url)), + text: () => Promise.resolve(JSON.stringify(mockAptosApiResponse(url))), + } as Response), +); beforeEach(() => { mockConsole(); + jest.clearAllMocks(); }); diff --git a/packages/@magic-ext/aptos/test/spec/aptos-extension.spec.ts b/packages/@magic-ext/aptos/test/spec/aptos-extension.spec.ts index c736cf41e..54687c612 100644 --- a/packages/@magic-ext/aptos/test/spec/aptos-extension.spec.ts +++ b/packages/@magic-ext/aptos/test/spec/aptos-extension.spec.ts @@ -4,6 +4,35 @@ import { AptosExtension } from '../../src'; import { AptosPayloadMethod } from '../../src/type'; import { TextEncoder, TextDecoder } from 'util'; +// Mock AptosClient to avoid network calls +const mockRawTransaction = new TxnBuilderTypes.RawTransaction( + TxnBuilderTypes.AccountAddress.fromHex('0x8293d5e05544c6e53c47fc19ae071c26a60e0ccbd8a12eb5b2c9d348c85227b6'), + BigInt(0), + new TxnBuilderTypes.TransactionPayloadEntryFunction( + TxnBuilderTypes.EntryFunction.natural( + '0x1::coin', + 'transfer', + [TxnBuilderTypes.StructTag.fromString('0x1::aptos_coin::AptosCoin')], + [], + ), + ), + BigInt(2000), + BigInt(1), + BigInt(Math.floor(Date.now() / 1000) + 600), + new TxnBuilderTypes.ChainId(2), +); + +jest.mock('aptos', () => { + const actual = jest.requireActual('aptos'); + return { + ...actual, + AptosClient: jest.fn().mockImplementation(() => ({ + generateTransaction: jest.fn().mockResolvedValue(mockRawTransaction), + generateRawTransaction: jest.fn().mockResolvedValue(mockRawTransaction), + })), + }; +}); + const APTOS_NODE_URL = 'https://fullnode.testnet.aptoslabs.com'; const SAMPLE_ADDRESS = '0x8293d5e05544c6e53c47fc19ae071c26a60e0ccbd8a12eb5b2c9d348c85227b6'; @@ -36,7 +65,7 @@ beforeAll(() => { }) beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); test('Construct GetAccount request with `aptos_getAccount`', async () => { diff --git a/packages/@magic-ext/aptos/test/spec/magic-aptos-wallet.spec.ts b/packages/@magic-ext/aptos/test/spec/magic-aptos-wallet.spec.ts index 0c321811a..70c73db94 100644 --- a/packages/@magic-ext/aptos/test/spec/magic-aptos-wallet.spec.ts +++ b/packages/@magic-ext/aptos/test/spec/magic-aptos-wallet.spec.ts @@ -26,7 +26,7 @@ const SAMPLE_BCS_TRANSACTION = new TxnBuilderTypes.TransactionPayloadEntryFuncti ), ); -const MOCK_ACCOUTN_INFO = { +const MOCK_ACCOUNT_INFO = { address: SAMPLE_ADDRESS, publicKey: 'test-public-key', }; @@ -76,7 +76,7 @@ test('Call connect() without config.connect()', async () => { }), ]); magic.user.isLoggedIn = jest.fn().mockReturnValue(Promise.resolve(false)); - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); const aptosWallet = new MagicAptosWallet(magic); @@ -91,7 +91,7 @@ test('Call connect() when a user has already logged in', async () => { }), ]); magic.user.isLoggedIn = jest.fn().mockReturnValue(Promise.resolve(true)); - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); const aptosWallet = new MagicAptosWallet(magic); @@ -99,7 +99,7 @@ test('Call connect() when a user has already logged in', async () => { const accountInfo = await aptosWallet.connect(); // Assert - expect(accountInfo).toEqual(MOCK_ACCOUTN_INFO); + expect(accountInfo).toEqual(MOCK_ACCOUNT_INFO); }); test('Call disconnect()', async () => { @@ -132,7 +132,7 @@ test('Call account()', async () => { nodeUrl: APTOS_NODE_URL, }), ]); - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); const aptosWallet = new MagicAptosWallet(magic); @@ -141,7 +141,7 @@ test('Call account()', async () => { // Assert expect(magic.aptos.getAccountInfo).toBeCalledTimes(1); - expect(accountInfo).toEqual(MOCK_ACCOUTN_INFO); + expect(accountInfo).toEqual(MOCK_ACCOUNT_INFO); }); test('Call account() when a user has already logged in ', async () => { @@ -151,7 +151,7 @@ test('Call account() when a user has already logged in ', async () => { }), ]); magic.user.isLoggedIn = jest.fn().mockReturnValue(Promise.resolve(true)); - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); const aptosWallet = new MagicAptosWallet(magic); @@ -161,7 +161,7 @@ test('Call account() when a user has already logged in ', async () => { // Assert expect(magic.aptos.getAccountInfo).toBeCalledTimes(1); - expect(accountInfo).toEqual(MOCK_ACCOUTN_INFO); + expect(accountInfo).toEqual(MOCK_ACCOUNT_INFO); }); test('Call account() without magic', async () => { @@ -180,7 +180,7 @@ test('Call signTransaction()', async () => { const mockResult = Uint8Array.from([1, 2, 3]); - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); magic.aptos.signTransaction = jest.fn().mockReturnValue(Promise.resolve(mockResult)); const aptosWallet = new MagicAptosWallet(magic); @@ -210,7 +210,7 @@ test('Call signAndSubmitTransaction()', async () => { const mockResult = { hash: 'test-hash' }; - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); magic.aptos.signAndSubmitTransaction = jest.fn().mockReturnValue(Promise.resolve(mockResult)); const aptosWallet = new MagicAptosWallet(magic); @@ -240,7 +240,7 @@ test('Call signAndSubmitBCSTransaction()', async () => { const mockResult = { hash: 'test-hash' }; - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); magic.aptos.signAndSubmitBCSTransaction = jest.fn().mockReturnValue(Promise.resolve(mockResult)); const aptosWallet = new MagicAptosWallet(magic); @@ -277,7 +277,7 @@ test('Call signMessage()', async () => { prefix: 'APTOS', }; - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); magic.aptos.signMessage = jest.fn().mockReturnValue(Promise.resolve(mockResult)); const aptosWallet = new MagicAptosWallet(magic); @@ -315,7 +315,7 @@ test('Call signMessageAndVerify()', async () => { const mockResult = true; - magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUTN_INFO)); + magic.aptos.getAccountInfo = jest.fn().mockReturnValue(Promise.resolve(MOCK_ACCOUNT_INFO)); magic.aptos.signMessageAndVerify = jest.fn().mockReturnValue(Promise.resolve(mockResult)); const aptosWallet = new MagicAptosWallet(magic); @@ -360,7 +360,7 @@ test('Set up with mainnet and call network()', async () => { // Assert expect(result).toEqual({ name: 'mainnet', - chainId: '1', + chainId: 1, url: NODE_URL, }); }); @@ -381,7 +381,7 @@ test('Set up with testnet and call network()', async () => { // Assert expect(result).toEqual({ name: 'testnet', - chainId: '2', + chainId: 2, url: NODE_URL, }); }); @@ -402,6 +402,7 @@ test('Set up with devnet and call network()', async () => { // Assert expect(result).toEqual({ name: 'devnet', + chainId: 34, url: NODE_URL, }); }); diff --git a/packages/@magic-sdk/react-native-expo/jest.config.ts b/packages/@magic-sdk/react-native-expo/jest.config.ts index b68d259b6..bd45a949a 100644 --- a/packages/@magic-sdk/react-native-expo/jest.config.ts +++ b/packages/@magic-sdk/react-native-expo/jest.config.ts @@ -3,7 +3,7 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { ...baseJestConfig, - preset: 'jest-expo', + preset: 'react-native', transform: { '^.+\\.(js|jsx)$': 'babel-jest', '\\.(ts|tsx)$': 'ts-jest', diff --git a/packages/@magic-sdk/react-native-expo/package.json b/packages/@magic-sdk/react-native-expo/package.json index b99273191..1b5833d62 100644 --- a/packages/@magic-sdk/react-native-expo/package.json +++ b/packages/@magic-sdk/react-native-expo/package.json @@ -23,7 +23,7 @@ "@react-native-async-storage/async-storage": "^1.15.5", "@types/lodash": "^4.14.158", "buffer": "~5.6.0", - "expo-application": "^5.0.1", + "expo-application": "^7.0.8", "localforage": "^1.7.4", "lodash": "^4.17.19", "process": "~0.11.10", @@ -36,10 +36,10 @@ "@react-native/assets-registry": "^0.78.2", "@testing-library/react-native": "^13.2.0", "@types/lodash": "^4.14.158", - "babel-preset-expo": "^12.0.11", - "expo": "^52.0.44", - "expo-modules-core": "^2.2.3", - "jest-expo": "~52.0.6", + "babel-preset-expo": "^13.2.4", + "expo": "^54.0.30", + "expo-modules-core": "^3.0.29", + "jest-expo": "~54.0.16", "react": "^19.1.0", "react-native": "^0.78.2", "react-native-safe-area-context": "^5.3.0", diff --git a/packages/@magic-sdk/react-native-expo/test/mocks.ts b/packages/@magic-sdk/react-native-expo/test/mocks.ts index 875b057c0..527e841f3 100644 --- a/packages/@magic-sdk/react-native-expo/test/mocks.ts +++ b/packages/@magic-sdk/react-native-expo/test/mocks.ts @@ -9,4 +9,16 @@ export function removeReactDependencies() { jest.mock('@react-native-community/netinfo', () => { return require('@react-native-community/netinfo/jest/netinfo-mock'); }); + jest.mock('expo-modules-core', () => ({ + EventEmitter: jest.fn(), + NativeModule: jest.fn(), + requireNativeModule: jest.fn(), + requireOptionalNativeModule: jest.fn(), + })); + jest.mock('expo-application', () => ({ + nativeApplicationVersion: '1.0.0', + nativeBuildVersion: '1', + applicationName: 'TestApp', + applicationId: 'com.test.app', + })); } diff --git a/scripts/bin/scaffold/run.tsx b/scripts/bin/scaffold/run.tsx index 7d18c29f7..3d750b725 100755 --- a/scripts/bin/scaffold/run.tsx +++ b/scripts/bin/scaffold/run.tsx @@ -1,73 +1,111 @@ #!/usr/bin/env ts-node-script -import React from 'react'; -import { Zombi, Template, Directory, scaffold } from 'zombi'; +import inquirer from 'inquirer'; import execa from 'execa'; import path from 'path'; +import fs from 'fs-extra'; import { camelCase } from 'lodash'; +import ejs from 'ejs'; import { environment } from '../../utils/environment'; import { runAsyncProcess } from '../../utils/run-async-process'; interface Props { - platform?: 'hybrid' | 'web' | 'react-native'; - extName?: string; - extDescription?: string; - className?: string; + platform: 'hybrid' | 'web' | 'react-native'; + extName: string; + extDescription: string; + className: string; } -const template = ( - - name="create-magic-extension" - templateRoot={path.resolve(__dirname, './template')} - destinationRoot={path.resolve(__dirname, '../../../packages')} - prompts={[ - { - name: 'platform', - message: 'What platform does the extension support?', - type: 'select', - choices: ['hybrid', 'web', 'react-native'], - }, +async function processTemplate( + sourcePath: string, + destPath: string, + data: Record, +): Promise { + const stat = await fs.stat(sourcePath); - { - name: 'extName', - message: 'What is the Extension name? (@magic-ext/[name])', - type: 'input', - }, + if (stat.isDirectory()) { + await fs.ensureDir(destPath); + const entries = await fs.readdir(sourcePath); - { - name: 'extDescription', - message: 'Please specify the description field for package.json', - type: 'input', - }, + for (const entry of entries) { + const sourceEntryPath = path.join(sourcePath, entry); + const destEntryPath = path.join(destPath, entry); + await processTemplate(sourceEntryPath, destEntryPath, data); + } + } else { + const content = await fs.readFile(sourcePath, 'utf-8'); + const processed = ejs.render(content, data, { + filename: sourcePath, + }); + await fs.writeFile(destPath, processed, 'utf-8'); + } +} - { - name: 'className', - message: 'What is the exported Extension class name?', - type: 'input', +async function main() { + const answers = await inquirer.prompt([ + { + name: 'platform', + message: 'What platform does the extension support?', + type: 'list', + choices: ['hybrid', 'web', 'react-native'], + }, + { + name: 'extName', + message: 'What is the Extension name? (@magic-ext/[name])', + type: 'input', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return 'Extension name is required'; + } + return true; + }, + }, + { + name: 'extDescription', + message: 'Please specify the description field for package.json', + type: 'input', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return 'Description is required'; + } + return true; }, - ]} - > - {props => ( - -