Skip to content

Commit

Permalink
Use glob() for all directory listings
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrotherham committed Nov 3, 2022
1 parent 1d58417 commit 2e1de86
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 285 deletions.
4 changes: 2 additions & 2 deletions app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ module.exports = async (options) => {
// Cache mapped components and examples
const [componentsData, componentNames, exampleNames, fullPageExamples] = await Promise.all([
getComponentsData(),
getDirectories(configPaths.components).then(listing => [...listing.keys()]),
getDirectories(configPaths.examples).then(listing => [...listing.keys()]),
getDirectories(configPaths.components),
getDirectories(configPaths.examples),
getFullPageExamples()
])

Expand Down
4 changes: 2 additions & 2 deletions app/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ describe(`http://localhost:${PORT}`, () => {
const response = await fetchPath('/')
const $ = cheerio.load(await response.text())

const componentsDirectory = await getDirectories(configPaths.components)
const componentNames = await getDirectories(configPaths.components)
const componentsList = $('li a[href^="/components/"]').get()

// Since we have an 'all' component link that renders the default example of all
// components, there will always be one more expected link.
const expectedComponentLinks = componentsDirectory.size + 1
const expectedComponentLinks = componentNames.length + 1
expect(componentsList.length).toEqual(expectedComponentLinks)
})
})
Expand Down
108 changes: 26 additions & 82 deletions lib/file-helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const { readdir, readFile, stat } = require('fs/promises')
const { join, parse, ParsedPath } = require('path')
const { readFile } = require('fs/promises')
const { join, parse, ParsedPath, relative } = require('path')
const { cwd } = require('process')
const { promisify } = require('util')
const glob = promisify(require('glob'))
const yaml = require('js-yaml')
const fm = require('front-matter')
const minimatch = require('minimatch')
Expand All @@ -18,41 +21,32 @@ const cache = global.cache || {}
* Directory listing for path
*
* @param {string} directoryPath
* @returns {Promise<DirectoryListing>} Directory listing
* @param {string} [pattern] - Minimatch pattern
* @param {import('glob').IOptions} [options] - Glob options
* @returns {Promise<string[]>} File paths
*/
const getListing = async (directoryPath) => {
const basenames = await readdir(directoryPath)

// Gather listing entries
const entries = await Promise.all(basenames.map(async basename => {
const entryPath = join(directoryPath, basename)

// File or directory entry
/** @type {DirectoryListingEntry} */
const entry = {
basename,
path: entryPath,
stats: await stat(entryPath)
}

// Follow child directory listing
if (entry.stats.isDirectory()) {
entry.entries = await getListing(entryPath)
}

return [basename, entry]
}))
const getListing = async (directoryPath, pattern = '**/*', options = {}) => {
const listing = await glob(pattern, {
allowEmpty: true,
cwd: join(cwd(), directoryPath),
matchBase: true,
nodir: true,
realpath: true,
...options
})

return new Map(entries)
return listing
.map((entryPath) => relative(directoryPath, entryPath))
.sort()
}

/**
* Directory listing (directories only)
*
* @param {string} directoryPath
* @returns {Promise<DirectoryListing>} Directory entries
* @returns {Promise<string[]>} File paths
*/
const getDirectories = async (directoryPath) => {
const getDirectories = (directoryPath) => {
const directories = cache.directories?.get(directoryPath)

// Retrieve from cache
Expand All @@ -61,29 +55,7 @@ const getDirectories = async (directoryPath) => {
}

// Read from disk
const listing = await getListing(directoryPath)

// Directories only
const entries = [...listing]
.filter(([, { stats }]) => stats.isDirectory())

return new Map(entries)
}

/**
* Directory listing (files only)
*
* @param {string} directoryPath
* @returns {Promise<DirectoryListing>} File entries
*/
const getFiles = async (directoryPath) => {
const listing = await getListing(directoryPath)

// Files only
const entries = [...listing]
.filter(([, { stats }]) => stats.isFile())

return new Map(entries)
return getListing(directoryPath, '*/', { nodir: false })
}

/**
Expand Down Expand Up @@ -119,17 +91,6 @@ const mapPathTo = (patterns, callback) => (entryPath) => {
: entryPath
}

/**
* Directory listing array mapper
* Flattens to array of paths
*
* @param {[string, DirectoryListingEntry]} Single - directory listing entry
* @returns {string[]} Returns path (or array of paths)
*/
const listingToArray = ([, entry]) => entry.entries
? [...entry.entries].flatMap(listingToArray)
: [entry.path]

/**
* Load single component data
*
Expand Down Expand Up @@ -170,8 +131,8 @@ const getComponentsData = async () => {
}

// Read from disk
const directories = await getDirectories(configPaths.components)
return Promise.all([...directories.keys()].map(getComponentData))
const componentNames = await getDirectories(configPaths.components)
return Promise.all(componentNames.map(getComponentData))
}

/**
Expand All @@ -183,7 +144,7 @@ const getFullPageExamples = async () => {
const directories = await getDirectories(configPaths.fullPageExamples)

// Add metadata (front matter) to each example
const examples = await Promise.all([...directories.keys()].map(async (exampleName) => {
const examples = await Promise.all(directories.map(async (exampleName) => {
const templatePath = join(configPaths.fullPageExamples, exampleName, 'index.njk')
const { attributes } = fm(await readFile(templatePath, 'utf8'))

Expand All @@ -207,28 +168,11 @@ module.exports = {
getComponentData,
getComponentsData,
getDirectories,
getFiles,
getFullPageExamples,
getListing,
listingToArray,
mapPathTo
}

/**
* Directory listing
*
* @typedef {Map<string, DirectoryListingEntry>} DirectoryListing
*/

/**
* Directory listing entry
*
* @typedef {object} DirectoryListingEntry
* @property {string} path - Relative path to file or directory
* @property {DirectoryListing} [entries] - Child directory listing for entry
* @property {import('fs').Stats} stats - Information about a file or directory
*/

/**
* Directory entry path mapper callback
*
Expand Down
13 changes: 9 additions & 4 deletions src/govuk/components/all.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { join } = require('path')
const { fetch } = require('undici')
const { WebSocket } = require('ws')

const { getDirectories } = require('../../../lib/file-helper')
const { getDirectories, getListing } = require('../../../lib/file-helper')
const { goToComponent } = require('../../../lib/puppeteer-helpers')

const configPaths = require('../../../config/paths.js')
Expand All @@ -10,6 +11,7 @@ describe('Visual regression via Percy', () => {
let percySnapshot

let componentsFiles
let componentNames

beforeAll(async () => {
// Polyfill fetch() detection, upload via WebSocket()
Expand All @@ -18,23 +20,26 @@ describe('Visual regression via Percy', () => {
percySnapshot = require('@percy/puppeteer')

// Component directory listing (with contents)
componentsFiles = await getDirectories(configPaths.components)
componentsFiles = await getListing(configPaths.components)

// Components list
componentNames = await getDirectories(configPaths.components)
})

afterAll(async () => {
await page.setJavaScriptEnabled(true)
})

it('generate screenshots', async () => {
for (const [componentName, { entries }] of componentsFiles) {
for (const componentName of componentNames) {
await page.setJavaScriptEnabled(true)

// Screenshot preview page (with JavaScript)
await goToComponent(page, componentName)
await percySnapshot(page, `js: ${componentName}`)

// Check for "JavaScript enabled" components
if (entries?.has(`${componentName}.mjs`)) {
if (componentsFiles.includes(join(componentName, `${componentName}.mjs`))) {
await page.setJavaScriptEnabled(false)

// Screenshot preview page (without JavaScript)
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/components.template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('Components', () => {
nunjucksEnvDefault = nunjucksEnv

// Components list
componentNames = [...(await getDirectories(configPaths.components)).keys()]
componentNames = await getDirectories(configPaths.components)
})

describe('Nunjucks environment', () => {
Expand Down
20 changes: 13 additions & 7 deletions src/govuk/components/components.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
const { getDirectories } = require('../../../lib/file-helper')
const { join } = require('path')

const { getListing } = require('../../../lib/file-helper')
const { renderSass } = require('../../../lib/jest-helpers')
const configPaths = require('../../../config/paths.js')

describe('Components', () => {
let componentsFiles
let sassFiles

beforeAll(async () => {
// Component directory listing (with contents)
componentsFiles = await getDirectories(configPaths.components)
sassFiles = await getListing(configPaths.components, '**/*.scss', {
ignore: [
'**/_all.scss',
'**/_index.scss'
]
})
})

describe('Sass render', () => {
it('renders CSS for all components', () => {
const file = `${configPaths.src}/components/_all.scss`
const file = join(configPaths.components, '_all.scss')

return expect(renderSass({ file })).resolves.toEqual(
expect.objectContaining({
Expand All @@ -23,8 +29,8 @@ describe('Components', () => {
})

it('renders CSS for each component', () => {
const sassTasks = [...componentsFiles].map(([componentName, { entries }]) => {
const file = entries?.get(`_${componentName}.scss`)?.path
const sassTasks = sassFiles.map((sassFilePath) => {
const file = join(configPaths.components, sassFilePath)

return expect(renderSass({ file })).resolves.toEqual(
expect.objectContaining({
Expand Down
58 changes: 38 additions & 20 deletions src/govuk/helpers/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
const glob = require('glob')
const path = require('path')
const { join } = require('path')

const sassdoc = require('sassdoc')

const { getListing } = require('../../../lib/file-helper')
const { renderSass } = require('../../../lib/jest-helpers')
const configPaths = require('../../../config/paths.js')

const sassFiles = glob.sync(`${configPaths.src}/helpers/**/*.scss`)

describe('The helpers layer', () => {
let sassFiles

beforeAll(async () => {
sassFiles = await getListing(configPaths.src, 'helpers/**/*.scss', {
ignore: ['**/_all.scss']
})
})

it('should not output any CSS', async () => {
const helpers = path.join(configPaths.src, 'helpers', '_all.scss')
const helpers = join(configPaths.src, 'helpers', '_all.scss')

const output = await renderSass({ file: helpers })
expect(output.css.toString()).toEqual('')
})

it.each(sassFiles)('%s renders to CSS without errors', (file) => {
return renderSass({ file })
it('renders CSS for all helpers', () => {
const sassTasks = sassFiles.map((sassFilePath) => {
const file = join(configPaths.src, sassFilePath)

return expect(renderSass({ file })).resolves.toEqual(
expect.objectContaining({
css: expect.any(Object),
stats: expect.any(Object)
})
)
})

return Promise.all(sassTasks)
})

describe('Sass documentation', () => {
it('associates everything with a "helpers" group', async () => {
return sassdoc.parse(path.join(configPaths.src, 'helpers', '*.scss'))
.then(docs => docs.forEach(doc => {
return expect(doc).toMatchObject({
// Include doc.context.name in the expected result when this fails,
// giving you the context to be able to fix it
context: {
name: doc.context.name
},
group: [
expect.stringMatching(/^helpers/)
]
})
}))
const docs = await sassdoc.parse(join(configPaths.src, 'helpers', '*.scss'))

for (const doc of docs) {
expect(doc).toMatchObject({
// Include doc.context.name in the expected result when this fails,
// giving you the context to be able to fix it
context: {
name: doc.context.name
},
group: [
expect.stringMatching(/^helpers/)
]
})
}
})
})
})
Loading

0 comments on commit 2e1de86

Please sign in to comment.