diff --git a/packages/cli/src/commands/__tests__/studioHandler.test.js b/packages/cli/src/commands/__tests__/studioHandler.test.js new file mode 100644 index 000000000000..042bf310846c --- /dev/null +++ b/packages/cli/src/commands/__tests__/studioHandler.test.js @@ -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() + }) + }) +}) diff --git a/packages/cli/src/commands/studioHandler.js b/packages/cli/src/commands/studioHandler.js index 50ca7df5c3ef..787b575f62ca 100644 --- a/packages/cli/src/commands/studioHandler.js +++ b/packages/cli/src/commands/studioHandler.js @@ -1,4 +1,9 @@ -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' @@ -6,14 +11,28 @@ 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 @@ -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'] +} diff --git a/packages/cli/src/lib/packages.js b/packages/cli/src/lib/packages.js index aaaf04240333..c65c7dc9d49c 100644 --- a/packages/cli/src/lib/packages.js +++ b/packages/cli/src/lib/packages.js @@ -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 @@ -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