Skip to content

Commit

Permalink
feat(cli): add deploy command for non-studio applications (#8592)
Browse files Browse the repository at this point in the history
  • Loading branch information
cngonzalez authored and bjoerge committed Feb 18, 2025
1 parent 9675bdd commit d315921
Show file tree
Hide file tree
Showing 11 changed files with 655 additions and 101 deletions.
12 changes: 8 additions & 4 deletions packages/@sanity/cli/src/actions/init-project/initProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,20 @@ export default async function initSanity(
await getOrCreateUser()
}

// skip project / dataset prompting
const isCoreAppTemplate = cliFlags.template ? determineCoreAppTemplate(cliFlags.template) : false // Default to false

let introMessage = 'Fetching existing projects'
if (cliFlags.quickstart) {
introMessage = "Eject your existing project's Sanity configuration"
}
success(introMessage)
print('')

if (!isCoreAppTemplate) {
success(introMessage)
print('')
}

const flags = await prepareFlags()
// skip project / dataset prompting
const isCoreAppTemplate = cliFlags.template ? determineCoreAppTemplate(cliFlags.template) : false // Default to false

// We're authenticated, now lets select or create a project (for studios) or org (for core apps)
const {projectId, displayName, isFirstProject, datasetName, schemaUrl, organizationId} =
Expand Down
18 changes: 14 additions & 4 deletions packages/@sanity/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,17 @@ export async function runCli(cliRoot: string, {cliVersion}: {cliVersion: string}

const args = parseArguments()
const isInit = args.groupOrCommand === 'init' && args.argsWithoutOptions[0] !== 'plugin'
const isCoreApp = args.groupOrCommand === 'app'
const cwd = getCurrentWorkingDirectory()
let workDir: string | undefined
try {
workDir = isInit ? process.cwd() : resolveRootDir(cwd)
workDir = isInit ? process.cwd() : resolveRootDir(cwd, isCoreApp)
} catch (err) {
console.error(chalk.red(err.message))
process.exit(1)
}

loadAndSetEnvFromDotEnvFiles({workDir, cmd: args.groupOrCommand})
loadAndSetEnvFromDotEnvFiles({workDir, cmd: args.groupOrCommand, isCoreApp})
maybeFixMissingWindowsEnvVar()

// Check if there are updates available for the CLI, and notify if there is
Expand Down Expand Up @@ -99,6 +100,7 @@ export async function runCli(cliRoot: string, {cliVersion}: {cliVersion: string}
corePath: await getCoreModulePath(workDir, cliConfig),
cliConfig,
telemetry,
isCoreApp,
}

warnOnNonProductionEnvironment()
Expand Down Expand Up @@ -274,7 +276,15 @@ function warnOnNonProductionEnvironment(): void {
)
}

function loadAndSetEnvFromDotEnvFiles({workDir, cmd}: {workDir: string; cmd: string}) {
function loadAndSetEnvFromDotEnvFiles({
workDir,
cmd,
isCoreApp,
}: {
workDir: string
cmd: string
isCoreApp: boolean
}) {
/* eslint-disable no-process-env */

// Do a cheap lookup for a sanity.json file. If there is one, assume it is a v2 project,
Expand Down Expand Up @@ -309,7 +319,7 @@ function loadAndSetEnvFromDotEnvFiles({workDir, cmd}: {workDir: string; cmd: str

debug('Loading environment files using %s mode', mode)

const studioEnv = loadEnv(mode, workDir, ['SANITY_STUDIO_'])
const studioEnv = loadEnv(mode, workDir, isCoreApp ? ['VITE_'] : ['SANITY_STUDIO_'])
process.env = {...process.env, ...studioEnv}
/* eslint-disable no-process-env */
}
Expand Down
2 changes: 2 additions & 0 deletions packages/@sanity/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export interface CommandRunnerOptions {
workDir: string
corePath: string | undefined
telemetry: TelemetryLogger<TelemetryUserProperties>
isCoreApp: boolean
}

export interface CliOutputter {
Expand Down Expand Up @@ -354,6 +355,7 @@ export interface CliConfig {
__experimental_coreAppConfiguration?: {
organizationId?: string
appLocation?: string
appId?: string
}
}

Expand Down
17 changes: 9 additions & 8 deletions packages/@sanity/cli/src/util/resolveRootDir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@ import {debug} from '../debug'
/**
* Resolve project root directory, falling back to cwd if it cannot be found
*/
export function resolveRootDir(cwd: string): string {
export function resolveRootDir(cwd: string, isCoreApp = false): string {
try {
return resolveProjectRoot(cwd) || cwd
return resolveProjectRoot(cwd, 0, isCoreApp) || cwd
} catch (err) {
throw new Error(`Error occurred trying to resolve project root:\n${err.message}`)
}
}

function hasStudioConfig(basePath: string): boolean {
function hasSanityConfig(basePath: string, configName: string): boolean {
const buildConfigs = [
fileExists(path.join(basePath, 'sanity.config.js')),
fileExists(path.join(basePath, 'sanity.config.ts')),
fileExists(path.join(basePath, `${configName}.js`)),
fileExists(path.join(basePath, `${configName}.ts`)),
isSanityV2StudioRoot(basePath),
]

return buildConfigs.some(Boolean)
}

function resolveProjectRoot(basePath: string, iterations = 0): string | false {
if (hasStudioConfig(basePath)) {
function resolveProjectRoot(basePath: string, iterations = 0, isCoreApp = false): string | false {
const configName = isCoreApp ? 'sanity.cli' : 'sanity.config'
if (hasSanityConfig(basePath, configName)) {
return basePath
}

Expand All @@ -36,7 +37,7 @@ function resolveProjectRoot(basePath: string, iterations = 0): string | false {
return false
}

return resolveProjectRoot(parentDir, iterations + 1)
return resolveProjectRoot(parentDir, iterations + 1, isCoreApp)
}

function isSanityV2StudioRoot(basePath: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import zlib from 'node:zlib'

import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli'
import {type CliCommandArguments, type CliCommandContext, type CliConfig} from '@sanity/cli'
import tar from 'tar-fs'
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'

Expand Down Expand Up @@ -36,6 +36,7 @@ describe('deployStudioAction', () => {
updatedAt: new Date().toISOString(),
urlType: 'internal',
projectId: 'example',
organizationId: null,
title: null,
type: 'studio',
}
Expand Down Expand Up @@ -71,7 +72,7 @@ describe('deployStudioAction', () => {
// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockResolvedValueOnce(mockApplication)
helpers.getOrCreateStudio.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValue({pipe: vi.fn(() => 'tarball')} as unknown as ReturnType<
Expand Down Expand Up @@ -99,7 +100,7 @@ describe('deployStudioAction', () => {
expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplication).toHaveBeenCalledWith(
expect(helpers.getOrCreateStudio).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
Expand All @@ -111,6 +112,7 @@ describe('deployStudioAction', () => {
version: 'vX',
isAutoUpdating: false,
tarball: 'tarball',
isCoreApp: false,
})

expect(mockContext.output.print).toHaveBeenCalledWith(
Expand Down Expand Up @@ -167,6 +169,7 @@ describe('deployStudioAction', () => {
version: 'vX',
isAutoUpdating: false,
tarball: 'tarball',
isCoreApp: false,
})

expect(mockContext.output.print).toHaveBeenCalledWith(
Expand All @@ -183,7 +186,7 @@ describe('deployStudioAction', () => {
true,
) // User confirms to proceed
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockResolvedValueOnce(mockApplication)
helpers.getOrCreateStudio.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValue({pipe: vi.fn(() => 'tarball')} as unknown as ReturnType<
Expand Down Expand Up @@ -267,7 +270,7 @@ describe('deployStudioAction', () => {
// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockRejectedValueOnce({
helpers.getOrCreateStudio.mockRejectedValueOnce({
statusCode: 402,
message: 'Application limit reached',
error: 'Payment Required',
Expand All @@ -289,7 +292,7 @@ describe('deployStudioAction', () => {
expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplication).toHaveBeenCalledWith(
expect(helpers.getOrCreateStudio).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
Expand All @@ -299,4 +302,56 @@ describe('deployStudioAction', () => {

expect(mockContext.output.error).toHaveBeenCalledWith('Application limit reached')
})

it('handles core app deployment correctly', async () => {
// Create a mock application with all required properties
const mockCoreApp: UserApplication = {
id: 'core-app-id',
appHost: 'core-app-host',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
urlType: 'internal',
projectId: null,
title: null,
type: 'coreApp',
organizationId: 'org-id',
}

mockContext = {
...mockContext,
cliConfig: {
// eslint-disable-next-line camelcase
__experimental_coreAppConfiguration: {
appId: 'core-app-id',
organizationId: 'org-id',
},
} as CliConfig,
}

helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplicationFromConfig.mockResolvedValueOnce(mockCoreApp)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://core-app-host'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValue({pipe: vi.fn(() => 'tarball')} as unknown as ReturnType<
typeof tar.pack
>)
zlibCreateGzipMock.mockReturnValue('gzipped' as unknown as ReturnType<typeof zlib.createGzip>)

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

expect(helpers.getOrCreateUserApplicationFromConfig).toHaveBeenCalled()
expect(helpers.createDeployment).toHaveBeenCalledWith(
expect.objectContaining({
isCoreApp: true,
}),
)
expect(mockContext.output.print).toHaveBeenCalledWith('\nSuccess! Application deployed')
})
})
Loading

0 comments on commit d315921

Please sign in to comment.