Skip to content

Convert project dev command to ts #1387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion commands/__tests__/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import logs from '../project/logs';
import watch from '../project/watch';
import download from '../project/download';
import open from '../project/open';
import dev from '../project/dev';
import * as dev from '../project/dev';
import add from '../project/add';
import migrateApp from '../project/migrateApp';
import cloneApp from '../project/cloneApp';
Expand Down
121 changes: 74 additions & 47 deletions commands/project/dev.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
// @ts-nocheck
const {
import {
addAccountOptions,
addConfigOptions,
addUseEnvironmentOptions,
addTestingOptions,
} = require('../../lib/commonOpts');
const { trackCommandUsage } = require('../../lib/usageTracking');
const { handleExit } = require('../../lib/process');
const { i18n } = require('../../lib/lang');
const { logger } = require('@hubspot/local-dev-lib/logger');
const {
} from '../../lib/commonOpts';
import { trackCommandUsage } from '../../lib/usageTracking';
import { handleExit } from '../../lib/process';
import { i18n } from '../../lib/lang';
import { logger } from '@hubspot/local-dev-lib/logger';
import {
getConfigAccounts,
getAccountConfig,
getEnv,
} = require('@hubspot/local-dev-lib/config');
const {
} from '@hubspot/local-dev-lib/config';
import {
getProjectConfig,
ensureProjectExists,
validateProjectConfig,
} = require('../../lib/projects');
const { EXIT_CODES } = require('../../lib/enums/exitCodes');
const { uiBetaTag, uiCommandReference, uiLink } = require('../../lib/ui');
const SpinniesManager = require('../../lib/ui/SpinniesManager');
const LocalDevManager = require('../../lib/LocalDevManager');
const {
} from '../../lib/projects';
import { EXIT_CODES } from '../../lib/enums/exitCodes';
import { uiBetaTag, uiCommandReference, uiLink } from '../../lib/ui';
import SpinniesManager from '../../lib/ui/SpinniesManager';
import LocalDevManager from '../../lib/LocalDevManager';
import {
isSandbox,
isDeveloperTestAccount,
isStandardAccount,
isAppDeveloperAccount,
} = require('../../lib/accountTypes');
const { getValidEnv } = require('@hubspot/local-dev-lib/environment');
const { ComponentTypes } = require('../../types/Projects');
const {
} from '../../lib/accountTypes';
import { getValidEnv } from '@hubspot/local-dev-lib/environment';
import { ComponentTypes } from '../../types/Projects';
import {
findProjectComponents,
getProjectComponentTypes,
} = require('../../lib/projects/structure');
const {
} from '../../lib/projects/structure';
import {
confirmDefaultAccountIsTarget,
suggestRecommendedNestedAccount,
checkIfDefaultAccountIsSupported,
Expand All @@ -46,19 +44,23 @@ const {
useExistingDevTestAccount,
checkIfAccountFlagIsSupported,
checkIfParentAccountIsAuthed,
} = require('../../lib/localDev');
} from '../../lib/localDev';
import { ArgumentsCamelCase, Argv } from 'yargs';
import { CommonArgs, ConfigArgs, EnvironmentArgs } from '../../types/Yargs';

const i18nKey = 'commands.project.subcommands.dev';

exports.command = 'dev';
exports.describe = uiBetaTag(i18n(`${i18nKey}.describe`), false);
export const command = 'dev';
export const describe = uiBetaTag(i18n(`${i18nKey}.describe`), false);

exports.handler = async options => {
const { derivedAccountId, providedAccountId } = options;
type ProjectDevArgs = CommonArgs & ConfigArgs & EnvironmentArgs;

export async function handler(args: ArgumentsCamelCase<ProjectDevArgs>) {
const { derivedAccountId, providedAccountId } = args;
const accountConfig = getAccountConfig(derivedAccountId);
const env = getValidEnv(getEnv(derivedAccountId));

trackCommandUsage('project-dev', null, derivedAccountId);
trackCommandUsage('project-dev', {}, derivedAccountId);

const { projectConfig, projectDir } = await getProjectConfig();

Expand All @@ -71,8 +73,18 @@ exports.handler = async options => {
)
);

if (!projectConfig) {
logger.error(i18n(`${i18nKey}.errors.noProjectConfig`));
if (!projectConfig || !projectDir) {
logger.error(
i18n(`${i18nKey}.errors.noProjectConfig`, {
accountId: derivedAccountId,
authCommand: uiCommandReference('hs auth'),
})
);
process.exit(EXIT_CODES.ERROR);
}

if (!accountConfig) {
logger.error(i18n(`${i18nKey}.errors.noAccount`));
process.exit(EXIT_CODES.ERROR);
}

Expand All @@ -99,6 +111,15 @@ exports.handler = async options => {

const accounts = getConfigAccounts();

if (!accounts) {
logger.error(
i18n(`${i18nKey}.errors.noAccountsInConfig`, {
authCommand: uiCommandReference('hs auth'),
})
);
process.exit(EXIT_CODES.SUCCESS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an error?

}

const defaultAccountIsRecommendedType =
isDeveloperTestAccount(accountConfig) ||
(!hasPublicApps && isSandbox(accountConfig));
Expand All @@ -114,7 +135,7 @@ exports.handler = async options => {
checkIfAccountFlagIsSupported(accountConfig, hasPublicApps);

if (hasPublicApps) {
targetProjectAccountId = accountConfig.parentAccountId;
targetProjectAccountId = accountConfig.parentAccountId || null;
}
} else {
checkIfDefaultAccountIsSupported(accountConfig, hasPublicApps);
Expand All @@ -124,11 +145,11 @@ exports.handler = async options => {
if (!targetProjectAccountId && defaultAccountIsRecommendedType) {
targetTestingAccountId = derivedAccountId;

await confirmDefaultAccountIsTarget(accountConfig, hasPublicApps);
await confirmDefaultAccountIsTarget(accountConfig);

if (hasPublicApps) {
checkIfParentAccountIsAuthed(accountConfig);
targetProjectAccountId = accountConfig.parentAccountId;
targetProjectAccountId = accountConfig.parentAccountId || null;
} else {
targetProjectAccountId = derivedAccountId;
}
Expand All @@ -149,7 +170,9 @@ exports.handler = async options => {
hasPublicApps
);

targetProjectAccountId = hasPublicApps ? parentAccountId : targetAccountId;
targetProjectAccountId = hasPublicApps
? parentAccountId || null
: targetAccountId;
targetTestingAccountId = targetAccountId;

// Only used for developer test accounts that are not yet in the config
Expand Down Expand Up @@ -180,6 +203,11 @@ exports.handler = async options => {
targetProjectAccountId = derivedAccountId;
}

if (!targetProjectAccountId || !targetTestingAccountId) {
logger.error(i18n(`${i18nKey}.errors.noAccount`));
process.exit(EXIT_CODES.ERROR);
}

// eslint-disable-next-line prefer-const
let { projectExists, project } = await ensureProjectExists(
targetProjectAccountId,
Expand All @@ -192,15 +220,15 @@ exports.handler = async options => {
);

let deployedBuild;
let isGithubLinked;
let isGithubLinked = false;

SpinniesManager.init();

if (projectExists) {
if (projectExists && project) {
deployedBuild = project.deployedBuild;
isGithubLinked =
project.sourceIntegration &&
project.sourceIntegration.source === 'GITHUB';
isGithubLinked = Boolean(
project.sourceIntegration && project.sourceIntegration.source === 'GITHUB'
);
} else {
project = await createNewProjectForLocalDev(
projectConfig,
Expand All @@ -218,29 +246,28 @@ exports.handler = async options => {

const LocalDev = new LocalDevManager({
runnableComponents,
debug: options.debug,
debug: args.debug,
deployedBuild,
isGithubLinked,
parentAccountId: targetProjectAccountId,
projectConfig,
projectDir,
projectId: project.id,
projectId: project!.id,
targetAccountId: targetTestingAccountId,
env,
});

await LocalDev.start();

handleExit(({ isSIGHUP }) => LocalDev.stop(!isSIGHUP));
};
}

exports.builder = yargs => {
export function builder(yargs: Argv): Argv<ProjectDevArgs> {
addConfigOptions(yargs);
addAccountOptions(yargs);
addUseEnvironmentOptions(yargs);
addTestingOptions(yargs);

yargs.example([['$0 project dev', i18n(`${i18nKey}.examples.default`)]]);

return yargs;
};
return yargs as Argv<ProjectDevArgs>;
}
2 changes: 2 additions & 0 deletions lang/en.lyaml
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@ en:
learnMoreLocalDevServer: "Learn more about the projects local dev server"
errors:
noProjectConfig: "No project detected. Please run this command again from a project directory."
noAccount: "An error occurred while reading account {{ accountId }} from your config. Run {{ authCommand }} to re-auth this account."
noAccountsInConfig: "No accounts found in your config. Run {{ authCommand }} to configure a HubSpot account with the CLI."
invalidProjectComponents: "Projects cannot contain both private and public apps. Move your apps to separate projects before attempting local development."
noRunnableComponents: "No supported components were found in this project. Run {{ command }} to see a list of available components and add one to your project."
examples:
Expand Down
6 changes: 3 additions & 3 deletions lib/LocalDevManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type LocalDevManagerConstructorOptions = {
projectDir: string;
projectId: number;
debug?: boolean;
deployedBuild: Build;
deployedBuild?: Build;
isGithubLinked: boolean;
runnableComponents: Component[];
env: Environment;
Expand All @@ -72,7 +72,7 @@ class LocalDevManager {
projectDir: string;
projectId: number;
debug: boolean;
deployedBuild: Build;
deployedBuild?: Build;
isGithubLinked: boolean;
watcher: FSWatcher | null;
uploadWarnings: { [key: string]: boolean };
Expand Down Expand Up @@ -444,7 +444,7 @@ class LocalDevManager {
}

compareLocalProjectToDeployed(): void {
const deployedComponentNames = this.deployedBuild.subbuildStatuses.map(
const deployedComponentNames = this.deployedBuild!.subbuildStatuses.map(
subbuildStatus => subbuildStatus.buildName
);

Expand Down
3 changes: 2 additions & 1 deletion lib/localDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { hubspotAccountNamePrompt } from './prompts/accountNamePrompt';
import {
ProjectConfig,
ProjectDevTargetAccountPromptResponse,
ProjectPollResult,
ProjectSubtask,
} from '../types/Projects';
Expand Down Expand Up @@ -173,7 +174,7 @@ export async function suggestRecommendedNestedAccount(
accounts: CLIAccount[],
accountConfig: CLIAccount,
hasPublicApps: boolean
) {
): Promise<ProjectDevTargetAccountPromptResponse> {
logger.log();
uiLine();
if (hasPublicApps) {
Expand Down
9 changes: 5 additions & 4 deletions lib/prompts/projectDevTargetAccountPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@hubspot/local-dev-lib/types/developerTestAccounts';
import { PromptChoices } from '../../types/Prompts';
import { EXIT_CODES } from '../enums/exitCodes';
import { ProjectDevTargetAccountPromptResponse } from '../../types/Projects';

const i18nKey = 'lib.prompts.projectDevTargetAccountPrompt';

Expand Down Expand Up @@ -52,7 +53,7 @@ function getNonConfigDeveloperTestAccountName(
export async function selectSandboxTargetAccountPrompt(
accounts: CLIAccount[],
defaultAccountConfig: CLIAccount
): Promise<DeveloperTestAccount | CLIAccount> {
): Promise<ProjectDevTargetAccountPromptResponse> {
const defaultAccountId = getAccountId(defaultAccountConfig.name);
let choices = [];
let sandboxUsage: Usage = {
Expand Down Expand Up @@ -125,7 +126,7 @@ export async function selectSandboxTargetAccountPrompt(
export async function selectDeveloperTestTargetAccountPrompt(
accounts: CLIAccount[],
defaultAccountConfig: CLIAccount
): Promise<DeveloperTestAccount | CLIAccount> {
): Promise<ProjectDevTargetAccountPromptResponse> {
const defaultAccountId = getAccountId(defaultAccountConfig.name);
let devTestAccountsResponse: FetchDeveloperTestAccountsResponse | undefined;
try {
Expand Down Expand Up @@ -193,10 +194,10 @@ async function selectTargetAccountPrompt(
defaultAccountId: number | null,
accountType: string,
choices: PromptChoices
): Promise<CLIAccount | DeveloperTestAccount> {
): Promise<ProjectDevTargetAccountPromptResponse> {
const accountId = defaultAccountId;
const { targetAccountInfo } = await promptUser<{
targetAccountInfo: CLIAccount | DeveloperTestAccount;
targetAccountInfo: ProjectDevTargetAccountPromptResponse;
}>([
{
name: 'targetAccountInfo',
Expand Down
8 changes: 8 additions & 0 deletions types/Projects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Build, SubbuildStatus } from '@hubspot/local-dev-lib/types/Build';
import { Deploy, SubdeployStatus } from '@hubspot/local-dev-lib/types/Deploy';
import { DeveloperTestAccount } from '@hubspot/local-dev-lib/types/developerTestAccounts';

export type ProjectTemplate = {
name: string;
Expand Down Expand Up @@ -127,3 +128,10 @@ export type Component<T = GenericComponentConfig> = {
runnable: boolean;
path: string;
};

export type ProjectDevTargetAccountPromptResponse = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this live in the prompt file instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't decide between the two, but if you vote for prompts lets go with that

targetAccountId: number | null;
createNestedAccount: boolean;
parentAccountId?: number | null;
notInConfigAccount?: DeveloperTestAccount | null;
};