diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 79bb32c9d2..1b1515c0b8 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Multi-chain project initialization error + ## [5.3.0] - 2024-10-21 ### Changed - Improve codegen error messages (#2567) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 3199ae028e..d88b6af1cc 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -7,6 +7,7 @@ import path from 'path'; import {search, confirm, input} from '@inquirer/prompts'; import {Args, Command, Flags} from '@oclif/core'; import {NETWORK_FAMILY} from '@subql/common'; +import {ProjectNetworkConfig} from '@subql/types-core'; import chalk from 'chalk'; import fuzzy from 'fuzzy'; import ora from 'ora'; @@ -128,8 +129,8 @@ export default class Init extends Command { assert(selectedProject, 'No project selected'); const projectPath: string = await cloneProjectTemplate(location, project.name, selectedProject); - - await this.setupProject(project, projectPath, flags); + const {isMultiChainProject} = await this.setupProject(project, projectPath, flags); + if (isMultiChainProject) return; if (await validateEthereumProjectManifest(projectPath)) { const loadAbi = await confirm({ @@ -147,17 +148,26 @@ export default class Init extends Command { project: ProjectSpecBase, projectPath: string, flags: {npm: boolean; 'install-dependencies': boolean} - ): Promise { - const [defaultEndpoint, defaultAuthor, defaultDescription] = await readDefaults(projectPath); + ): Promise<{isMultiChainProject: boolean}> { + const { + author: defaultAuthor, + description: defaultDescription, + endpoint: defaultEndpoint, + isMultiChainProject, + } = await readDefaults(projectPath); + + if (!isMultiChainProject) { + const projectEndpoints: string[] = this.extractEndpoints(defaultEndpoint); + const userInput = await input({ + message: 'RPC endpoint:', + default: projectEndpoints[0], + required: false, + }); + if (!projectEndpoints.includes(userInput)) { + projectEndpoints.push(userInput); + } - project.endpoint = !Array.isArray(defaultEndpoint) ? [defaultEndpoint] : defaultEndpoint; - const userInput = await input({ - message: 'RPC endpoint:', - default: defaultEndpoint[0] ?? 'wss://polkadot.api.onfinality.io/public-ws', - required: false, - }); - if (!project.endpoint.includes(userInput)) { - (project.endpoint as string[]).push(userInput); + project.endpoint = projectEndpoints; } const descriptionHint = defaultDescription.substring(0, 40).concat('...'); project.author = await input({message: 'Author', required: true, default: defaultAuthor}); @@ -170,14 +180,16 @@ export default class Init extends Command { }); const spinner = ora('Preparing project').start(); - await prepare(projectPath, project); + await prepare(projectPath, project, isMultiChainProject); spinner.stop(); if (flags['install-dependencies']) { const spinner = ora('Installing dependencies').start(); installDependencies(projectPath, flags.npm); spinner.stop(); } - this.log(`${project.name} is ready`); + this.log(`${project.name} is ready${isMultiChainProject ? ' as a multi-chain project' : ''}`); + + return {isMultiChainProject}; } async createProjectScaffold(projectPath: string): Promise { @@ -210,4 +222,14 @@ export default class Init extends Command { `${startBlock}`, ]); } + + extractEndpoints(endpointConfig: ProjectNetworkConfig['endpoint']): string[] { + if (typeof endpointConfig === 'string') { + return [endpointConfig]; + } + if (endpointConfig instanceof Array) { + return endpointConfig; + } + return Object.keys(endpointConfig); + } } diff --git a/packages/cli/src/controller/init-controller.test.ts b/packages/cli/src/controller/init-controller.test.ts index b0e0749c72..db796ec5ef 100644 --- a/packages/cli/src/controller/init-controller.test.ts +++ b/packages/cli/src/controller/init-controller.test.ts @@ -119,7 +119,7 @@ describe('Cli can create project', () => { const project = projects.find((p) => p.name === 'Polkadot-starter')!; const projectPath = await cloneProjectTemplate(tempPath, projectSpec.name, project); await prepare(projectPath, projectSpec); - const [, author, description] = await readDefaults(projectPath); + const {author, description} = await readDefaults(projectPath); //spec version is not returned from readDefaults //expect(projectSpec.specVersion).toEqual(specVersion); diff --git a/packages/cli/src/controller/init-controller.ts b/packages/cli/src/controller/init-controller.ts index 8c6178b542..7a8294e90f 100644 --- a/packages/cli/src/controller/init-controller.ts +++ b/packages/cli/src/controller/init-controller.ts @@ -20,6 +20,7 @@ import { defaultEnvLocalPath, defaultEnvPath, defaultGitIgnorePath, + defaultMultiChainYamlManifestPath, defaultTSManifestPath, defaultYamlManifestPath, errorHandle, @@ -138,12 +139,21 @@ export async function cloneProjectTemplate( return projectPath; } -export async function readDefaults(projectPath: string): Promise { +export async function readDefaults(projectPath: string): Promise<{ + endpoint: ProjectNetworkConfig['endpoint']; + author: string; + description: string; + isMultiChainProject: boolean; +}> { const packageData = await fs.promises.readFile(`${projectPath}/package.json`); const currentPackage = JSON.parse(packageData.toString()); + const author: string = currentPackage.author; + const description: string = currentPackage.description; let endpoint: ProjectNetworkConfig['endpoint']; + let isMultiChainProject = false; const defaultTsPath = defaultTSManifestPath(projectPath); const defaultYamlPath = defaultYamlManifestPath(projectPath); + const defaultMultiChainPath = defaultMultiChainYamlManifestPath(projectPath); if (fs.existsSync(defaultTsPath)) { const tsManifest = await fs.promises.readFile(defaultTsPath, 'utf8'); @@ -152,23 +162,32 @@ export async function readDefaults(projectPath: string): Promise { }); endpoint = extractedTsValues.endpoint ?? []; - } else { + } else if (fs.existsSync(defaultYamlPath)) { const yamlManifest = await fs.promises.readFile(defaultYamlPath, 'utf8'); const extractedYamlValues = parseDocument(yamlManifest).toJS() as ProjectManifestV1_0_0; endpoint = extractedYamlValues.network.endpoint; + } else if (fs.existsSync(defaultMultiChainPath)) { + endpoint = []; + isMultiChainProject = true; + } else { + throw new Error('Failed to read manifest file while preparing the project'); } - return [endpoint, currentPackage.author, currentPackage.description]; + return {endpoint, author, description, isMultiChainProject}; } -export async function prepare(projectPath: string, project: ProjectSpecBase): Promise { +export async function prepare( + projectPath: string, + project: ProjectSpecBase, + isMultiChainProject = false +): Promise { try { - await prepareEnv(projectPath, project); + if (!isMultiChainProject) await prepareEnv(projectPath, project); } catch (e) { throw new Error('Failed to prepare read or write .env file while preparing the project'); } try { - await prepareManifest(projectPath, project); + if (!isMultiChainProject) await prepareManifest(projectPath, project); } catch (e) { throw new Error('Failed to prepare read or write manifest while preparing the project'); } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 23b96a2761..949b64dae6 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,13 +1,13 @@ // Copyright 2020-2024 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {RunnerSpecs} from '@subql/types-core'; +import {ProjectNetworkConfig, RunnerSpecs} from '@subql/types-core'; export interface ProjectSpecBase { name: string; author: string; description?: string; - endpoint: string[] | string; + endpoint: ProjectNetworkConfig['endpoint']; } export interface QueryAdvancedOpts { diff --git a/packages/cli/src/utils/build.ts b/packages/cli/src/utils/build.ts index d49cb9b506..dfdcb93c99 100644 --- a/packages/cli/src/utils/build.ts +++ b/packages/cli/src/utils/build.ts @@ -13,6 +13,7 @@ import { import {MultichainProjectManifest} from '@subql/types-core'; import * as yaml from 'js-yaml'; import * as tsNode from 'ts-node'; +import {isMultichain} from './utils'; const requireScriptWrapper = (scriptPath: string, outputPath: string): string => `import {toJsonObject} from '@subql/common';` + @@ -131,12 +132,6 @@ function getTsManifestsFromMultichain(location: string): string[] { .map((project) => path.resolve(path.dirname(location), project)); } -function isMultichain(location: string): boolean { - const multichainContent = yaml.load(readFileSync(location, 'utf8')) as MultichainProjectManifest; - - return !!multichainContent && !!multichainContent.projects; -} - function replaceTsReferencesInMultichain(location: string): void { const multichainContent = yaml.load(readFileSync(location, 'utf8')) as MultichainProjectManifest; multichainContent.projects = multichainContent.projects.map((project) => tsProjectYamlPath(project)); diff --git a/packages/cli/src/utils/utils.ts b/packages/cli/src/utils/utils.ts index dd1fbc2792..2d7fcb81a6 100644 --- a/packages/cli/src/utils/utils.ts +++ b/packages/cli/src/utils/utils.ts @@ -13,10 +13,13 @@ import { DEFAULT_ENV_LOCAL, DEFAULT_GIT_IGNORE, DEFAULT_MANIFEST, + DEFAULT_MULTICHAIN_MANIFEST, DEFAULT_TS_MANIFEST, } from '@subql/common'; +import {MultichainProjectManifest} from '@subql/types-core'; import axios from 'axios'; import ejs from 'ejs'; +import * as yaml from 'js-yaml'; import JSON5 from 'json5'; import {rimraf} from 'rimraf'; import {ACCESS_TOKEN_PATH} from '../constants'; @@ -263,3 +266,12 @@ export function copyFolderSync(source: string, target: string): void { } }); } + +export function defaultMultiChainYamlManifestPath(projectPath: string): string { + return path.join(projectPath, DEFAULT_MULTICHAIN_MANIFEST); +} + +export function isMultichain(location: string): boolean { + const multichainContent = yaml.load(readFileSync(location, 'utf8')) as MultichainProjectManifest; + return !!multichainContent && !!multichainContent.projects; +}