-
Notifications
You must be signed in to change notification settings - Fork 72
Revamp local dev to support websocket server for UI #1479
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
Changes from all commits
9096dfe
50d6ee5
a9d2bbe
415e1f3
998b57a
6a6d2ff
6638562
1336d0a
3c3c6b3
3aa1e68
e813163
7eeb2b4
01fe00f
392ffd8
2963cde
610f4f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,11 @@ import { | |
| uiCommandReference, | ||
| uiLink, | ||
| } from '../lib/ui'; | ||
| import { getProjectSettingsUrl } from '../lib/projects/urls'; | ||
| import { | ||
| getProjectDetailUrl, | ||
| getProjectSettingsUrl, | ||
| } from '../lib/projects/urls'; | ||
| import { UI_COLORS } from '../lib/ui'; | ||
|
|
||
| type LangFunction = (...args: never[]) => string; | ||
|
|
||
|
|
@@ -2689,11 +2693,20 @@ export const lib = { | |
| `Your project ${chalk.bold(projectName)} exists in ${accountIdentifier}, but has no deployed build. Projects must be successfully deployed to be developed locally. Address any build and deploy errors your project may have, then run ${uploadCommand} to upload and deploy your project.`, | ||
| noComponents: 'There are no components in this project.', | ||
| betaMessage: 'HubSpot projects local development', | ||
| learnMoreLocalDevServer: 'Learn more about the projects local dev server', | ||
| learnMoreLocalDevServer: uiLink( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these translations in the en.lyaml file too? Would it make sense to remove them as a part of this, or should we wait and do that later once everything is ported?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been operating under the assumption that we'd just delete the entire file at the end, so haven't been removing things from it. I don't think there's really any need to, especially since its easy to look up what we haven't ported to the new system by searching for |
||
| 'Learn more about the projects local dev server', | ||
| 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server' | ||
| ), | ||
| running: (projectName: string, accountIdentifier: string) => | ||
| `Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`, | ||
| chalk.hex(UI_COLORS.SORBET)( | ||
| `Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...` | ||
| ), | ||
| quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`, | ||
| viewProjectLink: 'View project in HubSpot', | ||
| viewProjectLink: (name: string, accountId: number) => | ||
| uiLink( | ||
| 'View project in HubSpot', | ||
| getProjectDetailUrl(name, accountId) || '' | ||
| ), | ||
| viewTestAccountLink: 'View developer test account in HubSpot', | ||
| exitingStart: 'Stopping local dev server ...', | ||
| exitingSucceed: 'Successfully exited', | ||
|
|
@@ -2711,6 +2724,7 @@ export const lib = { | |
| `${chalk.bold('Changing project configuration requires a new project build.')}\n\nThis will affect your public app's ${chalk.bold(`${installCount} existing ${installText}`)}. If your app has users in production, we strongly recommend creating a copy of this app to test your changes before proceding.`, | ||
| header: (warning: string) => | ||
| `${warning} To reflect these changes and continue testing:`, | ||
| instructionsHeader: 'To reflect these changes and continue testing:', | ||
| stopDev: ` * Stop ${uiCommandReference('hs project dev')}`, | ||
| runUpload: (command: string) => ` * Run ${command}`, | ||
| restartDev: ` * Re-run ${uiCommandReference('hs project dev')}`, | ||
|
|
@@ -2722,12 +2736,8 @@ export const lib = { | |
| `${chalk.bold('Changing project configuration requires creating a new project build.')}\n\nYour marketplace app is currently installed in ${chalk.bold(`${installCount} ${accountText}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`, | ||
| }, | ||
| activeInstallWarning: { | ||
| installCount: ( | ||
| appName: string, | ||
| installCount: number, | ||
| installText: string | ||
| ) => | ||
| `${chalk.bold(`The app ${appName} has ${installCount} production ${installText}`)}`, | ||
| installCount: (appName: string, installCount: number) => | ||
| `${chalk.bold(`The app ${appName} is installed in ${installCount} production ${installCount === 1 ? 'account' : 'accounts'}`)}`, | ||
| explanation: | ||
| 'Some changes made during local development may need to be synced to HubSpot, which will impact those existing installs. We strongly recommend creating a copy of this app to use instead.', | ||
| confirmation: `You will always be asked to confirm any permanent changes to your app's configuration before uploading them.`, | ||
|
|
@@ -2744,6 +2754,10 @@ export const lib = { | |
| `Failed to notify local dev server of file change: ${message}`, | ||
| }, | ||
| }, | ||
| AppDevModeInterface: { | ||
| defaultMarketplaceAppWarning: (installCount: number) => | ||
| `\n\nYour marketplace app is currently installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`, | ||
| }, | ||
| localDevHelpers: { | ||
| confirmDefaultAccountIsTarget: { | ||
| configError: `An error occurred while reading the default account from your config. Run ${uiCommandReference('hs auth')} to re-auth this account`, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a wrapper around the |
||
| import { | ||
| fetchPublicAppsForPortal, | ||
| fetchPublicAppProductionInstallCounts, | ||
| } from '@hubspot/local-dev-lib/api/appsDev'; | ||
| import { PublicApp } from '@hubspot/local-dev-lib/types/Apps'; | ||
| import { DevModeUnifiedInterface as UIEDevModeInterface } from '@hubspot/ui-extensions-dev-server'; | ||
| import { requestPorts } from '@hubspot/local-dev-lib/portManager'; | ||
|
|
||
| import { APP_DISTRIBUTION_TYPES } from '../../constants'; | ||
| import { EXIT_CODES } from '../../enums/exitCodes'; | ||
| import { isAppIRNode } from '../../projects/structure'; | ||
| import { uiLine } from '../../ui'; | ||
| import { logError } from '../../errorHandlers/index'; | ||
| import { installPublicAppPrompt } from '../../prompts/installPublicAppPrompt'; | ||
| import { confirmPrompt } from '../../prompts/promptUtils'; | ||
| import { AppIRNode } from '../../../types/ProjectComponents'; | ||
| import { lib } from '../../../lang/en'; | ||
| import { uiLogger } from '../../ui/logger'; | ||
| import { LocalDevState } from '../../../types/LocalDev'; | ||
| import LocalDevLogger from './LocalDevLogger'; | ||
|
|
||
| type AppDevModeInterfaceConstructorOptions = { | ||
| localDevState: LocalDevState; | ||
| localDevLogger: LocalDevLogger; | ||
| }; | ||
|
|
||
| class AppDevModeInterface { | ||
| localDevState: LocalDevState; | ||
| localDevLogger: LocalDevLogger; | ||
| _app?: AppIRNode | null; | ||
| marketplaceAppData?: PublicApp; | ||
| marketplaceAppInstalls?: number; | ||
|
|
||
| constructor(options: AppDevModeInterfaceConstructorOptions) { | ||
| this.localDevState = options.localDevState; | ||
| this.localDevLogger = options.localDevLogger; | ||
|
|
||
| if ( | ||
| !this.localDevState.targetProjectAccountId || | ||
| !this.localDevState.projectConfig || | ||
| !this.localDevState.projectDir | ||
| ) { | ||
| uiLogger.log(lib.LocalDevManager.failedToInitialize); | ||
| process.exit(EXIT_CODES.ERROR); | ||
| } | ||
| } | ||
|
|
||
| // Assumes only one app per project | ||
| private get app(): AppIRNode | null { | ||
| if (this._app === undefined) { | ||
| this._app = | ||
| Object.values(this.localDevState.projectNodes).find(isAppIRNode) || | ||
| null; | ||
| } | ||
| return this._app; | ||
| } | ||
|
|
||
| private async fetchMarketplaceAppData(): Promise<void> { | ||
| const { | ||
| data: { results: portalMarketplaceApps }, | ||
| } = await fetchPublicAppsForPortal( | ||
| this.localDevState.targetProjectAccountId | ||
| ); | ||
|
|
||
| const marketplaceAppData = portalMarketplaceApps.find( | ||
| ({ sourceId }) => sourceId === this.app?.uid | ||
| ); | ||
|
|
||
| if (!marketplaceAppData) { | ||
| return; | ||
| } | ||
|
|
||
| const { | ||
| data: { uniquePortalInstallCount }, | ||
| } = await fetchPublicAppProductionInstallCounts( | ||
| marketplaceAppData.id, | ||
| this.localDevState.targetProjectAccountId | ||
| ); | ||
|
|
||
| this.marketplaceAppData = marketplaceAppData; | ||
| this.marketplaceAppInstalls = uniquePortalInstallCount; | ||
| } | ||
|
|
||
| private async checkMarketplaceAppInstalls(): Promise<void> { | ||
| if (!this.marketplaceAppData || !this.marketplaceAppInstalls) { | ||
| return; | ||
| } | ||
| uiLine(); | ||
|
|
||
| uiLogger.warn( | ||
| lib.LocalDevManager.activeInstallWarning.installCount( | ||
| this.marketplaceAppData.name, | ||
| this.marketplaceAppInstalls | ||
| ) | ||
| ); | ||
| uiLogger.log(lib.LocalDevManager.activeInstallWarning.explanation); | ||
| uiLine(); | ||
|
|
||
| const proceed = await confirmPrompt( | ||
| lib.LocalDevManager.activeInstallWarning.confirmationPrompt, | ||
| { defaultAnswer: false } | ||
| ); | ||
|
|
||
| if (!proceed) { | ||
| process.exit(EXIT_CODES.SUCCESS); | ||
| } | ||
|
|
||
| this.localDevLogger.addUploadWarning( | ||
| lib.AppDevModeInterface.defaultMarketplaceAppWarning( | ||
| this.marketplaceAppInstalls | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| private async checkMarketplaceAppInstallation(): Promise<void> { | ||
| if (!this.app || !this.marketplaceAppData) { | ||
| return; | ||
| } | ||
|
|
||
| const { | ||
| data: { isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups }, | ||
| } = await fetchAppInstallationData( | ||
| this.localDevState.targetTestingAccountId, | ||
| this.localDevState.projectId, | ||
| this.app.uid, | ||
| this.app.config.auth.requiredScopes, | ||
| this.app.config.auth.optionalScopes | ||
| ); | ||
| const isReinstall = previouslyAuthorizedScopeGroups.length > 0; | ||
|
|
||
| if (!isInstalledWithScopeGroups) { | ||
| await installPublicAppPrompt( | ||
| this.localDevState.env, | ||
| this.localDevState.targetTestingAccountId, | ||
| this.marketplaceAppData.clientId, | ||
| this.app.config.auth.requiredScopes, | ||
| this.app.config.auth.redirectUrls, | ||
| isReinstall | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // @ts-expect-error TODO: reconcile types between CLI and UIE Dev Server | ||
| // In the future, update UIE Dev Server to use LocalDevState | ||
| async setup(args): Promise<void> { | ||
| if (!this.app) { | ||
| return; | ||
| } | ||
|
|
||
| if (this.app?.config.distribution === APP_DISTRIBUTION_TYPES.MARKETPLACE) { | ||
| try { | ||
| await this.fetchMarketplaceAppData(); | ||
| await this.checkMarketplaceAppInstalls(); | ||
| await this.checkMarketplaceAppInstallation(); | ||
| } catch (e) { | ||
| logError(e); | ||
| } | ||
| } | ||
| return UIEDevModeInterface.setup(args); | ||
| } | ||
|
|
||
| async start() { | ||
| if (!this.app) { | ||
| return; | ||
| } | ||
|
|
||
| return UIEDevModeInterface.start({ | ||
| accountId: this.localDevState.targetTestingAccountId, | ||
| // @ts-expect-error TODO: reconcile types between CLI and UIE Dev Server | ||
| projectConfig: this.localDevState.projectConfig, | ||
| requestPorts, | ||
| }); | ||
| } | ||
| async fileChange(filePath: string, event: string) { | ||
| if (!this.app) { | ||
| return; | ||
| } | ||
|
|
||
| return UIEDevModeInterface.fileChange(filePath, event); | ||
| } | ||
| async cleanup() { | ||
| if (!this.app) { | ||
| return; | ||
| } | ||
|
|
||
| return UIEDevModeInterface.cleanup(); | ||
| } | ||
| } | ||
|
|
||
| export default AppDevModeInterface; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LocalDevProcessrepresents the current local dev session (similar to the oldLocalDevManager). Everything that interacts with theLocalDevProcessnow lives outside of it and uses public methods to make necessary changes to the process.This includes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add a note with that explanation directly into the code.