Skip to content
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

fix(studio): Add version checks when first running Studio #9876

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
149 changes: 149 additions & 0 deletions packages/cli/src/commands/__tests__/studioHandler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Have to use `var` here to avoid "Temporal Dead Zone" issues
// eslint-disable-next-line
var mockedRedwoodVersion = '0.0.0'

jest.mock('@redwoodjs/project-config', () => ({
getPaths: () => ({ base: '' }),
}))

jest.mock('fs-extra', () => ({
readJSONSync: () => ({
devDependencies: {
'@redwoodjs/core': mockedRedwoodVersion,
},
}),
}))

import { assertRedwoodVersion } from '../studioHandler'

describe('studioHandler', () => {
describe('assertRedwoodVersion', () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit(${code})`)
})

jest.spyOn(console, 'error').mockImplementation()

afterEach(() => {
jest.clearAllMocks()
})

afterAll(() => {
jest.restoreAllMocks()
})

const minVersions = ['7.0.0-canary.874', '7.x', '8.0.0-0']

it('exits on RW v6', () => {
mockedRedwoodVersion = '6.6.2'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('exits on RW v7.0.0-canary.785', () => {
mockedRedwoodVersion = '7.0.0-canary.785'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('exits on RW v7.0.0-canary.785+fcb9d66b5', () => {
mockedRedwoodVersion = '7.0.0-canary.785+fcb9d66b5'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('exits on RW v0.0.0-experimental.999', () => {
mockedRedwoodVersion = '0.0.0-experimental.999'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('exits on RW v7.0.0-alpha.999', () => {
mockedRedwoodVersion = '7.0.0-alpha.999'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('exits on RW v7.0.0-rc.999', () => {
mockedRedwoodVersion = '7.0.0-rc.999'

expect(() => assertRedwoodVersion(minVersions)).toThrow()
expect(exitSpy).toHaveBeenCalledWith(1)
})

it('allows RW v7.0.0-canary.874', () => {
mockedRedwoodVersion = '7.0.0-canary.874'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v7.0.0-canary.874+fcb9d66b5', () => {
mockedRedwoodVersion = '7.0.0-canary.874+fcb9d66b5'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v7.0.0', () => {
mockedRedwoodVersion = '7.0.0'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v8.0.0', () => {
mockedRedwoodVersion = '8.0.0'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v7.0.1', () => {
mockedRedwoodVersion = '7.0.1'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v8.0.0-canary.1', () => {
mockedRedwoodVersion = '8.0.0-canary.1'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v8.0.0-rc.1', () => {
mockedRedwoodVersion = '8.0.0-rc.1'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v8.0.0', () => {
mockedRedwoodVersion = '8.0.0'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v8.0.1', () => {
mockedRedwoodVersion = '8.0.1'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})

it('allows RW v9.1.0', () => {
mockedRedwoodVersion = '9.1.0'

expect(() => assertRedwoodVersion(minVersions)).not.toThrow()
expect(exitSpy).not.toHaveBeenCalled()
})
})
})
75 changes: 71 additions & 4 deletions packages/cli/src/commands/studioHandler.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import { setTomlSetting } from '@redwoodjs/cli-helpers'
import path from 'node:path'

import fs from 'fs-extra'
import semver from 'semver'

import { getPaths } from '@redwoodjs/project-config'

import { isModuleInstalled, installModule } from '../lib/packages'

export const handler = async (options) => {
try {
// Check the module is installed
if (!isModuleInstalled('@redwoodjs/studio')) {
const minVersions = ['7.0.0-canary.874', '7.x', '8.0.0-0']
assertRedwoodVersion(minVersions)

console.log(
'The studio package is not installed, installing it for you, this may take a moment...'
)
await installModule('@redwoodjs/studio', '11.0.1')
await installModule('@redwoodjs/studio', '11')
console.log('Studio package installed successfully.')

console.log('Adding config to redwood.toml...')
setTomlSetting('studio', 'enabled', true)
const installedRealtime = await installModule('@redwoodjs/realtime')
if (installedRealtime) {
console.log(
"Added @redwoodjs/realtime to your project, as it's used by Studio"
)
}

const installedApiServer = await installModule('@redwoodjs/api-server')
if (installedApiServer) {
console.log(
"Added @redwoodjs/api-server to your project, as it's used by Studio"
)
}
}

// Import studio and start it
Expand All @@ -25,3 +44,51 @@ export const handler = async (options) => {
process.exit(1)
}
}

// Exported for unit testing
export function assertRedwoodVersion(minVersions) {
const rwVersion = getProjectRedwoodVersion()
const coercedRwVersion = semver.coerce(rwVersion)

if (
minVersions.some((minVersion) => {
// Have to do this to handle pre-release versions until
// https://github.com/npm/node-semver/pull/671 is merged
const v = semver.valid(minVersion) || semver.coerce(minVersion)

const coercedMin = semver.coerce(minVersion)

// According to semver 1.0.0-rc.X > 1.0.0-canary.Y (for all values of X
// and Y)
// But for Redwood an RC release can be much older than a Canary release
// (and not contain features from Canary that whoever calls this need)
// Because RW doesn't 100% follow SemVer for pre-releases we have to
// have some custom logic here
return (
semver.gte(rwVersion, v) &&
(coercedRwVersion.major === coercedMin.major
? semver.prerelease(rwVersion)?.[0] === semver.prerelease(v)?.[0]
: true)
)
})
) {
// All good, the user's RW version meets at least one of the minimum
// version requirements
return
}

console.error(
`The studio command requires Redwood version ${minVersions[0]} or ` +
`greater, you are using ${rwVersion}.`
)

process.exit(1)
}

function getProjectRedwoodVersion() {
const { devDependencies } = fs.readJSONSync(
path.join(getPaths().base, 'package.json')
)

return devDependencies['@redwoodjs/core']
}
18 changes: 12 additions & 6 deletions packages/cli/src/lib/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import fs from 'fs-extra'

import { getPaths } from './index'

// Note: Have to add backslash (\) before @ below for intellisense to display
// the doc comments properly
/**
* Installs a module into a user's project. If the module is already installed,
* this function does nothing. If no version is specified, the version will be assumed
* to be the same as that of \@redwoodjs/cli.
* Installs a module into a user's project. If the module is already installed,
* this function does nothing. If no version is specified, the version will be
* assumed to be the same as that of \@redwoodjs/cli.
*
* @param {string} name The name of the module to install
* @param {string} version The version of the module to install, otherwise the same as that of \@redwoodjs/cli
Expand All @@ -19,21 +21,25 @@ export async function installModule(name, version = undefined) {
if (isModuleInstalled(name)) {
return false
}

if (version === undefined) {
return await installRedwoodModule(name)
return installRedwoodModule(name)
} else {
await execa.command(`yarn add -D ${name}@${version}`, {
stdio: 'inherit',
cwd: getPaths().base,
})
}

return true
}

/**
* Installs a Redwood module into a user's project keeping the version consistent with that of \@redwoodjs/cli.
* Installs a Redwood module into a user's project keeping the version
* consistent with that of \@redwoodjs/cli.
* If the module is already installed, this function does nothing.
* If no remote version can not be found which matches the local cli version then the latest canary version will be used.
* If no remote version can not be found which matches the local cli version
* then the latest canary version will be used.
*
* @param {string} module A redwoodjs module, e.g. \@redwoodjs/web
* @returns {boolean} Whether the module was installed or not
Expand Down
Loading