diff --git a/lib/_update_javascripts.js b/lib/_update_javascripts.js new file mode 100644 index 0000000000..cc29ca5ad4 --- /dev/null +++ b/lib/_update_javascripts.js @@ -0,0 +1,68 @@ +const fs = require('fs').promises +const path = require('path') + +const { projectDir } = require('./path-utils') +const { getProjectVersion, fetchOriginal } = require('./update-utils') + +const updateDir = path.join(projectDir, 'update') + +async function removeKitJsFromApplicationJs () { + const userVersion = await getProjectVersion() + + // If the user already has version 13 or greater of the kit installed then + // their application.js file is all their code and we don't don't want to + // change it + if (userVersion >= '13.0.0') { + return + } + + const assetPath = 'assets/javascripts/application.js' + const original = await fetchOriginal(userVersion, path.posix.join('app', assetPath)) + const theirs = await fs.readFile(path.resolve(projectDir, 'app', assetPath), 'utf8') + + // If the user hasn't changed their application.js file we can just replace it completely + if (original === theirs) { + return fs.copyFile(path.join(updateDir, 'app', assetPath), path.join(projectDir, 'app', assetPath)) + } + + // Otherwise, if the original code is contained as-is in their file, we can + // remove the shared lines, and add our hints + if (theirs.includes(original)) { + const ours = await fs.readFile(path.resolve(updateDir, 'app', assetPath), 'utf8') + + let merged + merged = theirs.replace(original, '') + merged = ours + merged + return fs.writeFile(path.resolve(projectDir, 'app', assetPath), merged, 'utf8') + } + + // If the original code is not recognisable, we should give up, but not + // without giving a warning to the user + console.warn( + `WARNING: update.sh was not able to automatically update your ${assetPath} file.\n` + + 'If you have issues when running your prototype please contact the GOV.UK Prototype Kit team for support,\n' + + 'using one of the methods listed at https://design-system.service.gov.uk/get-in-touch/' + ) +} + +async function removeKitJsFromAppJsPath () { + const appJsPath = path.join(projectDir, 'app', 'assets', 'javascripts') + await fs.unlink(path.join(appJsPath, 'auto-store-data.js')).catch(() => {}) + await fs.unlink(path.join(appJsPath, 'jquery-1.11.3.js')).catch(() => {}) + await fs.unlink(path.join(appJsPath, 'step-by-step-nav.js')).catch(() => {}) + await fs.unlink(path.join(appJsPath, 'step-by-step-navigation.js')).catch(() => {}) +} + +async function main () { + await removeKitJsFromAppJsPath() + await removeKitJsFromApplicationJs() +} + +module.exports = { + /* exported for tests only */ + removeKitJsFromApplicationJs +} + +if (require.main === module) { + main() +} diff --git a/lib/_update_javascripts.test.js b/lib/_update_javascripts.test.js new file mode 100644 index 0000000000..f0ec42ef61 --- /dev/null +++ b/lib/_update_javascripts.test.js @@ -0,0 +1,113 @@ +/* eslint-env jest */ + +const fs = require('fs').promises +const path = require('path') + +jest.mock('./update-utils') +const updateUtils = require('./update-utils') +const updatejs = require('./_update_javascripts') + +const oldApplicationJs = `/* global $ */ + +// Warn about using the kit in production +if (window.console && window.console.info) { + window.console.info('GOV.UK Prototype Kit - do not use for production') +} + +$(document).ready(function () { + window.GOVUKFrontend.initAll() +}) +` + +const newApplicationJs = `// Add extra JavaScript here + +ready(() => { + // Add JavaScript that needs to be run when the page is loaded +}) +` + +describe('removeKitJsFromApplicationJs', () => { + let mockCopyFile, mockWriteFile + + afterEach(() => { + jest.restoreAllMocks() + }) + + beforeEach(() => { + jest.spyOn(updateUtils, 'getProjectVersion').mockImplementation( + async () => '12.1.1' + ) + + jest.spyOn(updateUtils, 'fetchOriginal').mockImplementation( + async () => oldApplicationJs + ) + + mockCopyFile = jest.spyOn(fs, 'copyFile').mockImplementation( + async () => {} + ) + + mockWriteFile = jest.spyOn(fs, 'writeFile').mockImplementation( + async () => {} + ) + }) + + it('replaces application.js if the user has not updated it', async () => { + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => oldApplicationJs + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockCopyFile).toHaveBeenCalledWith( + expect.stringContaining(path.join('update', 'app', 'assets', 'javascripts', 'application.js')), + expect.stringContaining(path.join('app', 'assets', 'javascripts', 'application.js')) + ) + }) + + it('rewrites application.js if the user has added lines to the bottom of the file', async () => { + // theirs + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => oldApplicationJs + '\ncallMyCode()\n' + ) + // ours + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => newApplicationJs + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockWriteFile).toHaveBeenCalledWith( + expect.stringContaining(path.join('app', 'assets', 'javascripts', 'application.js')), + newApplicationJs + '\ncallMyCode()\n', + 'utf8' + ) + }) + + it('does not touch application.js if the user has rewritten it a lot', async () => { + // theirs + jest.spyOn(fs, 'readFile').mockImplementationOnce( + async () => 'justMyCode()\n' + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockWriteFile).not.toHaveBeenCalled() + expect(mockCopyFile).not.toHaveBeenCalled() + }) + + it('does not touch application.js if the user prototype is already on v13', async () => { + jest.spyOn(updateUtils, 'getProjectVersion').mockImplementation( + async () => '13.0.0' + ) + + const mockReadFile = jest.spyOn(fs, 'readFile').mockImplementation( + async () => {} + ) + + await updatejs.removeKitJsFromApplicationJs() + + expect(mockReadFile).not.toHaveBeenCalled() + expect(mockWriteFile).not.toHaveBeenCalled() + expect(mockCopyFile).not.toHaveBeenCalled() + }) +}) diff --git a/lib/update-utils.js b/lib/update-utils.js new file mode 100644 index 0000000000..bac3037100 --- /dev/null +++ b/lib/update-utils.js @@ -0,0 +1,42 @@ +const fs = require('fs').promises +const https = require('https') +const path = require('path') + +const { projectDir } = require('./path-utils') + +async function getProjectVersion () { + return (await fs.readFile(path.join(projectDir, 'VERSION.txt'), 'utf8')).trim() +} + +async function fetchOriginal (version, filePath) { + const remoteUrl = `https://raw.githubusercontent.com/alphagov/govuk-prototype-kit/v${version}/${filePath}` + + let data = '' + return new Promise((resolve, reject) => { + https.get(remoteUrl, (response) => { + let error + + if (response.statusCode !== 200) { + error = new Error(`could not fetch ${remoteUrl}: status code ${response.statusCode}`) + Object.assign(error, response) + response.resume() + reject(error) + } + + response.setEncoding('utf8') + + response.on('data', (chunk) => { + data += chunk + }) + + response.on('end', () => { + resolve(data) + }) + }) + }) +} + +module.exports = { + fetchOriginal, + getProjectVersion +} diff --git a/lib/update-utils.test.js b/lib/update-utils.test.js new file mode 100644 index 0000000000..c9a323cce7 --- /dev/null +++ b/lib/update-utils.test.js @@ -0,0 +1,49 @@ +/* eslint-env jest */ + +const fs = require('fs').promises +const https = require('https') +const path = require('path') +const stream = require('stream') + +const { fetchOriginal, getProjectVersion } = require('./update-utils.js') + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe('fetchOriginal', () => { + it('gets the contents of a file as of version from GitHub', async () => { + const mockHttpsGet = jest.spyOn(https, 'get').mockImplementation((url, callback) => { + const response = new stream.PassThrough() + response.statusCode = 200 + + callback(response) + + response.write('foo\n') + response.write('bar\n') + response.end() + }) + + await expect(fetchOriginal('99.99.99', 'app/views/foo.html')).resolves.toEqual( + 'foo\nbar\n' + ) + expect(mockHttpsGet).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/alphagov/govuk-prototype-kit/v99.99.99/app/views/foo.html', + expect.anything() + ) + }) +}) + +describe('getProjectVersion', () => { + it('reads the VERSION.txt file to get the version number', async () => { + const mockReadFile = jest.spyOn(fs, 'readFile').mockImplementation( + async () => '99.99.99\n' + ) + + await expect(getProjectVersion()).resolves.toEqual('99.99.99') + expect(mockReadFile).toHaveBeenCalledWith( + expect.stringContaining(`${path.sep}VERSION.txt`), + 'utf8' + ) + }) +}) diff --git a/update.sh b/update.sh index 2af30c4725..5241876229 100755 --- a/update.sh +++ b/update.sh @@ -181,10 +181,12 @@ copy () { } post () { - # execute _update_scss if it exists in the update folder - if [ -d "update/lib/_update_scss" ]; then - node "update/lib/_update_scss" - fi + if [ -d "update/lib/_update_scss" ]; then + node "update/lib/_update_scss" + fi + if [ -f "update/lib/_update_javascripts.js" ]; then + node "update/lib/_update_javascripts" + fi } if [ "$0" == "${BASH_SOURCE:-$0}" ]