Skip to content
Open
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
2 changes: 0 additions & 2 deletions bin/cli
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,6 @@ async function runDev () {
async function runServe () {
warnIfNpmStart(argv, process.env)
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const { waitForPackagesCache } = require('../lib/plugins/packages')
await waitForPackagesCache()
require('../lib/build.js').generateAssetsSync()
require('../listen-on-port')
}
Expand Down
10 changes: 10 additions & 0 deletions lib/build.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ const sass = require('sass')
const { setPackagesCache } = require('./plugins/packages')
const { sassFunctions } = require('./build')

jest.mock('../package.json', () => {
return {
dependencies: {
'@govuk-prototype-kit/common-templates': '1.0.0',
"available-installed-plugin": "^1.0.0",
"available-prerelease-plugin": "2.0.0-rc.0"
}
}
})

describe('build', () => {
describe('sassFunctions', () => {
describe('plugin-version-satisfies', () => {
Expand Down
4 changes: 0 additions & 4 deletions lib/dev-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,13 @@ const {
} = require('./utils/paths')
const fs = require('fs')
const { logPerformanceSummaryOnce, startPerformanceTimer, endPerformanceTimer } = require('./utils/performance')
const { waitForPackagesCache } = require('./plugins/packages')

// Build watch and serve
async function runDevServer () {
await collectDataUsage()
let startupError

try {
// Wait for the package cache to be built before doing anything
// to ensure that `pluginVersionSatisfies` runs against accurate data
await waitForPackagesCache()
generateAssetsSync()
generateCssSync()
await utils.waitUntilFileExists(path.join(publicCssDir, 'application.css'), 5000)
Expand Down
11 changes: 0 additions & 11 deletions lib/manage-prototype-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const {
getAllPackages,
getDependentPackages,
getDependencyPackages,
waitForPackagesCache,
pluginVersionSatisfies
} = require('./plugins/packages')

Expand Down Expand Up @@ -148,15 +147,6 @@ function developmentOnlyMiddleware (req, res, next) {
}
}

// Middleware to ensure pages load when plugin cache has been initially loaded
async function pluginCacheMiddleware (req, res, next) {
await Promise.race([
waitForPackagesCache(),
new Promise((resolve) => setTimeout(resolve, 1000))
])
next()
}

const managementLinks = [
{
text: 'Manage your prototype',
Expand Down Expand Up @@ -775,7 +765,6 @@ module.exports = {
getPasswordHandler,
postPasswordHandler,
developmentOnlyMiddleware,
pluginCacheMiddleware,
getHomeHandler,
getTemplatesHandler,
getTemplatesViewHandler,
Expand Down
3 changes: 0 additions & 3 deletions lib/manage-prototype-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const {
postPluginsModeMiddleware,
postPluginsModeHandler,
postPluginsStatusHandler,
pluginCacheMiddleware,
postPluginsHandler
} = require('./manage-prototype-handlers')
const { packageDir, projectDir } = require('./utils/paths')
Expand Down Expand Up @@ -51,8 +50,6 @@ router.post('/password', postPasswordHandler)
// view when the prototype is not running in development
router.use(developmentOnlyMiddleware)

router.use(pluginCacheMiddleware)

router.get('/', getHomeHandler)

router.get('/templates', getTemplatesHandler)
Expand Down
78 changes: 20 additions & 58 deletions lib/plugins/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ const { getConfigForPackage } = require('../utils/requestHttps')
const { getProxyPluginConfig } = require('./plugin-utils')
const { sortByObjectKey, hasNewVersion } = require('../utils')

let packageTrackerInterval

const packagesCache = {}
let packagesCacheLastFetchTimeMs = 0;
const packagesCacheMaxAgeMs = 60 * 1000 // 1 minute

async function startPackageTracker () {
await updatePackagesInfo()
packageTrackerInterval = setInterval(updatePackagesInfo, 36000)
async function getOrFetchPackagesCache() {
if (Date.now() - packagesCacheLastFetchTimeMs > packagesCacheMaxAgeMs) {
await updatePackagesInfo()
packagesCacheLastFetchTimeMs = Date.now()
}
return packagesCache
}

async function updatePackagesInfo () {
Expand Down Expand Up @@ -176,71 +179,41 @@ function packageNameSort (pkgA, pkgB) {
* NOTE: Treats pre-releases version numbers as if they were the actual version
* to facilitate testing pre-releases
*
* WARNING: Needs to be run after the package cache is built.
* Use `waitForPackageCache` to make sure this function
* is called once the cache is ready.
*
* @param {string} pluginName
* @param {string} semverRange
*
* @returns {Boolean} `true` if a plugin with the name is installed and satisfies the range, `false` otherwise
* @internal
*/
function pluginVersionSatisfies (pluginName, semverRange) {
if (!Object.keys(packagesCache).length) {
throw new Error("The package cache need to be populated before checking plugins' version range")
}

const plugin = packagesCache[pluginName]
// No need to do a network fetch to npm for the installed version, it will be in package.json
// This will avoid a network request every time the app launches.
// We only need to fetch from npm if the package management pages are loaded
const installedVersion = projectPackage?.dependencies?.[pluginName]

if (!plugin?.pluginConfig) {
if (!installedVersion) {
return false
}

// Use `coerce` to treat pre-releases like if they were the actual package
return semver.satisfies(semver.coerce(plugin.installedVersion), semverRange)
return semver.satisfies(semver.coerce(installedVersion), semverRange)
}

async function getInstalledPackages () {
if (!Object.keys(packagesCache).length) {
await startPackageTracker()
}
await waitForPackagesCache()
const packagesCache = await getOrFetchPackagesCache();
return Object.values(packagesCache)
.filter(({ installed }) => installed)
.sort(packageNameSort)
.reduce(emphasizeBasePlugins, [])
}

async function getAllPackages () {
if (!Object.keys(packagesCache).length) {
await startPackageTracker()
}
await waitForPackagesCache()
const packagesCache = await getOrFetchPackagesCache();
return Object.values(packagesCache)
.sort(packageNameSort)
.reduce(emphasizeBasePlugins, [])
}

function sleep (ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

async function waitForPackagesCache () {
let numberOfRetries = 20
let waiting = !packageTrackerInterval
while (waiting) {
// If the packageTrackerInterval has been set, then packages cache has been populated at least once
waiting = !packageTrackerInterval && numberOfRetries > 0
numberOfRetries--
if (numberOfRetries === 0) {
console.log('Failed to load the package cache')
} else {
await sleep(250)
}
}
}

function normaliseDependencies (dependencies) {
return dependencies.map((dependency) => {
if (typeof dependency === 'string') {
Expand All @@ -256,19 +229,13 @@ async function getDependentPackages (packageName, version, mode) {
if (mode !== 'uninstall') {
return []
}
if (!Object.keys(packagesCache).length) {
await startPackageTracker()
}
await waitForPackagesCache()
const packagesCache = await getOrFetchPackagesCache();
return Object.values(packagesCache)
.filter(({ pluginDependencies }) => pluginDependencies?.some((pluginDependency) => pluginDependency === packageName || pluginDependency.packageName === packageName))
}

async function getDependencyPackages (packageName, version, mode) {
if (!Object.keys(packagesCache).length) {
await startPackageTracker()
}
await waitForPackagesCache()
await getOrFetchPackagesCache();
const pkg = await lookupPackageInfo(packageName, version)
let pluginDependencies = pkg?.pluginDependencies
if (version || mode === 'update') {
Expand All @@ -289,24 +256,19 @@ async function getDependencyPackages (packageName, version, mode) {
return dependencyPlugins.filter(({ installed }) => !installed)
}

if (!config.getConfig().isTest) {
startPackageTracker()
}

// Only used for unit tests
function setPackagesCache (packagesInfo) {
// Only used for unit tests
for (const key in packagesCache) {
delete packagesCache[key]
}
packagesInfo.forEach((packageInfo) => {
packagesCache[packageInfo.packageName] = packageInfo
})
packageTrackerInterval = true
packagesCacheLastFetchTimeMs = Date.now()
}

module.exports = {
setPackagesCache, // Only for unit testing purposes
waitForPackagesCache,
lookupPackageInfo,
getInstalledPackages,
getAllPackages,
Expand Down
67 changes: 15 additions & 52 deletions lib/plugins/packages.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const registryUrl = 'https://registry.npmjs.org/'
jest.mock('../../package.json', () => {
return {
dependencies: {
'@govuk-prototype-kit/common-templates': '1.0.0'
'@govuk-prototype-kit/common-templates': '1.0.0',
"available-installed-plugin": "^1.0.0",
"available-prerelease-plugin": "2.0.0-rc.0"
}
}
})
Expand Down Expand Up @@ -117,59 +119,20 @@ describe('packages', () => {
})

describe('pluginVersionSatisfies', () => {
describe('with an empty cache', () => {
it('throws an error', () => {
expect(() => pluginVersionSatisfies('available-installed-plugin', '>=1.0.0'))
.toThrow("The package cache need to be populated before checking plugins' version range")
})
})

describe('with a populated cache', () => {
const availableInstalledPackage = {
packageName: 'available-installed-package',
installed: true,
installedVersion: '1.0.0'
}
const availableInstalledPlugin = {
packageName: 'available-installed-plugin',
installed: true,
installedVersion: '1.0.0',
pluginConfig: {}
}
const availableUninstalledPackage = {
packageName: 'available-uninstalled-package',
installed: false
}
const availableInstalledPrereleasePlugin = {
packageName: 'available-prerelease-plugin',
installedVersion: '2.0.0-rc.0',
pluginConfig: {}
}
// See jest.mock('../../package.json') above

beforeEach(() => {
setPackagesCache([
availableInstalledPackage,
availableInstalledPlugin,
availableUninstalledPackage,
availableInstalledPrereleasePlugin
])
})

it('returns `true` if plugin is installed and matches the range', () => {
expect(pluginVersionSatisfies('available-installed-plugin', '>=1.0.0')).toBe(true)
})
it('returns `true` if pre-release for the minimum of the range is installed', () => {
expect(pluginVersionSatisfies('available-prerelease-plugin', '>=2.0.0')).toBe(true)
})
it('returns `false` if plugin is installed and version does not match the range', () => {
expect(pluginVersionSatisfies('available-installed-plugin', '<1.0.0')).toBe(false)
})
it('returns `false` if plugin is not installed', () => {
expect(pluginVersionSatisfies('available-uninstalled-package', '>=1.0.0')).toBe(false)
})
it('returns `false` if the package is installed but not a plugin', () => {
expect(pluginVersionSatisfies('available-installed-package', '>=1.0.0')).toBe(false)
})
it('returns `true` if plugin is installed and matches the range', () => {
expect(pluginVersionSatisfies('available-installed-plugin', '>=1.0.0')).toBe(true)
})
it('returns `true` if pre-release for the minimum of the range is installed', () => {
expect(pluginVersionSatisfies('available-prerelease-plugin', '>=2.0.0')).toBe(true)
})
it('returns `false` if plugin is installed and version does not match the range', () => {
expect(pluginVersionSatisfies('available-installed-plugin', '<1.0.0')).toBe(false)
})
it('returns `false` if plugin is not installed', () => {
expect(pluginVersionSatisfies('available-uninstalled-package', '>=1.0.0')).toBe(false)
})
})

Expand Down
5 changes: 0 additions & 5 deletions listen-on-port.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
// npm dependencies
const { runErrorServer } = require('./lib/errorServer')
const { waitForPackagesCache } = require('./lib/plugins/packages.js')
const { verboseLog } = require('./lib/utils/verboseLogger')

const config = require('./lib/config.js').getConfig(null, false)

;(async () => {
try {
// Wait for the package cache to be built before doing anything
// to ensure that `pluginVersionSatisfies` runs against accurate data
await waitForPackagesCache()

// local dependencies
const syncChanges = require('./lib/sync-changes')
const server = require('./server.js')
Expand Down