diff --git a/lib/commands/install.js b/lib/commands/install.js index 8875ee2ec..f5e5521f8 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -69,8 +69,8 @@ class InstallCommand extends Command { title: 'Linking latest Ghost and recording versions', task: this.link.bind(this) }, { - title: 'Linking latest default theme', - task: this.defaultTheme + title: 'Installing default themes', + task: this.defaultThemes }], false) }], { argv: {...argv, version}, @@ -129,24 +129,18 @@ class InstallCommand extends Command { ctx.installPath = path.join(process.cwd(), 'versions', resolvedVersion); // eslint-disable-line require-atomic-updates } - defaultTheme(ctx) { - const semver = require('semver'); - const version = ctx.version; - - // Starting with version 5.67.0, we are shipping an updated version of Casper with breaking changes under a new name - // If installing a version < 5.67.0, we need to symlink Casper - // We don't want the theme to outpace Ghost (e.g. try to use features that don't exist in Ghost yet) - if (semver.gte(version, '5.67.0')) { - // Create a symlink to the theme from the current version - return symlinkSync( - path.join(process.cwd(), 'current', 'content', 'themes', 'source'), - path.join(process.cwd(), 'content', 'themes', 'source') - ); - } else { - return symlinkSync( - path.join(process.cwd(), 'current', 'content', 'themes', 'casper'), - path.join(process.cwd(), 'content', 'themes', 'casper') - ); + defaultThemes() { + if (fs.existsSync(path.join(process.cwd(), 'current', 'content', 'themes'))) { + const symlinkSync = require('symlink-or-copy').sync; + const defaultThemes = fs.readdirSync(path.join(process.cwd(), 'current', 'content', 'themes')); + for (const theme of defaultThemes) { + if (!fs.existsSync(path.join(process.cwd(), 'content', 'themes', theme))) { + symlinkSync( + path.join(process.cwd(), 'current', 'content', 'themes', theme), + path.join(process.cwd(), 'content', 'themes', theme) + ); + } + } } } diff --git a/lib/commands/update.js b/lib/commands/update.js index d026b2ec2..4888d3c83 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -92,6 +92,9 @@ class UpdateCommand extends Command { }, { title: 'Linking latest Ghost and recording versions', task: this.link + }, { + title: 'Installing default themes', + task: this.linkDefaultThemes }, { title: 'Running database migrations', skip: ({rollback}) => rollback, @@ -219,23 +222,44 @@ class UpdateCommand extends Command { link({instance, installPath, version, rollback}) { const symlinkSync = require('symlink-or-copy').sync; - const semver = require('semver'); fs.removeSync(path.join(process.cwd(), 'current')); symlinkSync(installPath, path.join(process.cwd(), 'current')); - // If upgrading from a version < 5.67.0, we need to symlink source so it is installed and available to update - if (semver.lt(instance.version, '5.67.0') && semver.gte(version, '5.67.0')) { - symlinkSync( - path.join(process.cwd(), 'current', 'content', 'themes', 'source'), - path.join(process.cwd(), 'content', 'themes', 'source') - ); - } - instance.previousVersion = rollback ? null : instance.version; instance.version = version; instance.nodeVersion = process.versions.node; } + + linkDefaultThemes({rollback}) { + const currentThemesDir = path.join(process.cwd(), 'current', 'content', 'themes'); + const contentThemesDir = path.join(process.cwd(), 'content', 'themes'); + // remove any broken symlinks caused by default themes no longer existing in previous version + if (rollback) { + if (fs.existsSync(currentThemesDir)) { + const installedThemes = fs.readdirSync(contentThemesDir); + for (const theme of installedThemes) { + if (!fs.existsSync(path.join(contentThemesDir, theme))) { + fs.rmSync(path.join(contentThemesDir, theme)); + } + } + } + } + + // ensure all default themes (e.g. themes shipped with Ghost) are symlinked to /content/themes directory + if (fs.existsSync(path.join(process.cwd(), 'current', 'content', 'themes'))) { + const symlinkSync = require('symlink-or-copy').sync; + const defaultThemes = fs.readdirSync(path.join(process.cwd(), 'current', 'content', 'themes')); + for (const theme of defaultThemes) { + if (!fs.existsSync(path.join(process.cwd(), 'content', 'themes', theme))) { + symlinkSync( + path.join(process.cwd(), 'current', 'content', 'themes', theme), + path.join(process.cwd(), 'content', 'themes', theme) + ); + } + } + } + } } UpdateCommand.description = 'Update a Ghost instance'; diff --git a/test/unit/commands/install-spec.js b/test/unit/commands/install-spec.js index e9a7044c9..7c157cf7e 100644 --- a/test/unit/commands/install-spec.js +++ b/test/unit/commands/install-spec.js @@ -207,7 +207,7 @@ describe('Unit: Commands > Install', function () { const runCommandStub = sinon.stub(testInstance, 'runCommand').resolves(); const versionStub = sinon.stub(testInstance, 'version').resolves(); const linkStub = sinon.stub(testInstance, 'link').resolves(); - const defaultThemeStub = sinon.stub(testInstance, 'defaultTheme').resolves(); + const defaultThemesStub = sinon.stub(testInstance, 'defaultThemes').resolves(); return testInstance.run({version: '1.0.0', setup: false, 'check-empty': true}).then(() => { expect(dirEmptyStub.calledOnce).to.be.true; @@ -216,7 +216,7 @@ describe('Unit: Commands > Install', function () { expect(ensureStructureStub.calledOnce).to.be.true; expect(versionStub.calledOnce).to.be.true; expect(linkStub.calledOnce).to.be.true; - expect(defaultThemeStub.calledOnce).to.be.true; + expect(defaultThemesStub.calledOnce).to.be.true; expect(runCommandStub.calledOnce).to.be.true; }); }); @@ -412,38 +412,31 @@ describe('Unit: Commands > Install', function () { }); }); - describe('tasks > defaultTheme', function () { - it('links source version correctly if version >= 5.67.0', function () { + describe('tasks > defaultThemes', function () { + it('creates a symlink to all themes shipped with Ghost', function () { const symlinkSyncStub = sinon.stub(); + const readdirSyncStub = sinon.stub().returns(['casper', 'source']); + const existsSyncStub = sinon.stub(); + existsSyncStub.onCall(0).returns(true); + existsSyncStub.onCall(1).returns(false); + existsSyncStub.onCall(2).returns(false); const InstallCommand = proxyquire(modulePath, { - 'symlink-or-copy': {sync: symlinkSyncStub} + 'symlink-or-copy': {sync: symlinkSyncStub}, + 'fs-extra': {readdirSync: readdirSyncStub, existsSync: existsSyncStub} }); const testInstance = new InstallCommand({}, {}); const context = {version: '5.67.0'}; - testInstance.defaultTheme(context); - expect(symlinkSyncStub.calledOnce).to.be.true; + testInstance.defaultThemes(context); + expect(symlinkSyncStub.callCount).to.equal(2); expect(symlinkSyncStub.calledWithExactly( - path.join(process.cwd(), 'current/content/themes/source'), - path.join(process.cwd(), 'content/themes/source') + path.join(process.cwd(), 'current', 'content', 'themes', 'casper'), + path.join(process.cwd(), 'content', 'themes', 'casper') )); - }); - - it('links casper version correctly if version < 5.67.0', function () { - const symlinkSyncStub = sinon.stub(); - const InstallCommand = proxyquire(modulePath, { - 'symlink-or-copy': {sync: symlinkSyncStub} - }); - - const testInstance = new InstallCommand({}, {}); - - const context = {version: '5.62.0'}; - testInstance.defaultTheme(context); - expect(symlinkSyncStub.calledOnce).to.be.true; expect(symlinkSyncStub.calledWithExactly( - path.join(process.cwd(), 'current/content/themes/casper'), - path.join(process.cwd(), 'content/themes/casper') + path.join(process.cwd(), 'current', 'content', 'themes', 'source'), + path.join(process.cwd(), 'content', 'themes', 'source') )); }); }); diff --git a/test/unit/commands/update-spec.js b/test/unit/commands/update-spec.js index c0064a1a1..78a41899a 100644 --- a/test/unit/commands/update-spec.js +++ b/test/unit/commands/update-spec.js @@ -1068,11 +1068,15 @@ describe('Unit: Commands > Update', function () { expect(instance.previousVersion).to.be.null; expect(instance.nodeVersion).to.equal(process.versions.node); }); + }); + + describe('linkDefaultThemes', function () { + const UpdateCommand = require(modulePath); - it('installs source theme when upgrading from < 5.67.0 to >= 5.67.0', function () { + it('links all default themes bundled with Ghost', function () { const command = new UpdateCommand({}, {}); const envCfg = { - dirs: ['versions/5.62.0', 'versions/5.67.0', 'versions/5.67.0/content/themes/source', 'content/themes'], + dirs: ['versions/5.62.0', 'versions/5.67.0', 'versions/5.67.0/content/themes/source', 'versions/5.67.0/content/themes/casper', 'content/themes'], links: [['versions/5.62.0', 'current']] }; const env = setupTestFolder(envCfg); @@ -1088,10 +1092,33 @@ describe('Unit: Commands > Update', function () { }; command.link(context); - expect(fs.readlinkSync(path.join(env.dir, 'current'))).to.equal(path.join(env.dir, 'versions/5.67.0')); - expect(instance.version).to.equal('5.67.0'); - expect(instance.previousVersion).to.equal('5.62.0'); - expect(instance.nodeVersion).to.equal(process.versions.node); + command.linkDefaultThemes(context); + expect(fs.readlinkSync(path.join(env.dir, 'content', 'themes', 'source'))).to.equal(path.join(env.dir, 'current', 'content', 'themes', 'source')); + expect(fs.readlinkSync(path.join(env.dir, 'content', 'themes', 'casper'))).to.equal(path.join(env.dir, 'current', 'content', 'themes', 'casper')); + }); + + it('removes invalid symlinks when rolling back', function () { + const command = new UpdateCommand({}, {}); + const envCfg = { + dirs: ['versions/5.62.0', 'versions/5.67.0', 'versions/5.62.0/content/themes/casper', 'versions/5.67.0/content/themes/source', 'versions/5.67.0/content/themes/casper', 'content/themes'], + links: [['versions/5.67.0', 'current']] + }; + const env = setupTestFolder(envCfg); + sinon.stub(process, 'cwd').returns(env.dir); + const instance = { + version: '5.67.0' + }; + const context = { + installPath: path.join(env.dir, 'versions/5.62.0'), + version: '5.62.0', + rollback: true, + instance + }; + + command.link(context); + command.linkDefaultThemes(context); + expect(fs.existsSync(path.join(env.dir, 'content', 'themes', 'source'))).to.equal(false); + expect(fs.readlinkSync(path.join(env.dir, 'content', 'themes', 'casper'))).to.equal(path.join(env.dir, 'current', 'content', 'themes', 'casper')); }); }); });