Skip to content

Commit

Permalink
Add simple tooling setup for webpack v5
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace committed Apr 4, 2021
1 parent 21a96af commit bfec805
Show file tree
Hide file tree
Showing 10 changed files with 1,654 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
/docs
/__tests__/fixtures/cli-utils.js
/stubs/*

# Generated tooling jit files during testing
/jit/tooling/*/src/*
/jit/tooling/*/dist/*
/jit/tooling/*/tailwind.config.js
81 changes: 81 additions & 0 deletions jit/tests/tooling.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @ts-check

const { set } = require('lodash')
const { test, afterAll } = require('@jest/globals')
const { Tool, cleanupAllTools } = require('../tooling/Tool.js')

/** @param {import('../tooling/Tool.js').IntegrationTestedTool} toolName */
function buildTest(toolName) {
return async () => {
const tool = new Tool(toolName, {
config: {
mode: 'jit',
purge: ['./src/index.html'],
darkMode: false,
theme: {},
variants: {},
plugins: [],
},

files: [
{ path: './src/index.html', contents: `ml-4` },
{ path: './src/index.css', contents: `@tailwind utilities;` },
],
})

// 1. -- One-shot build --
await tool.cleanSlate()
await tool.build()
await tool.expectCss([
{
path: './dist/index.css',
expected: '.ml-4 { margin-left: 1rem; }',
},
])

// 2. -- Watching --
// 2.1. Intial Build
await tool.cleanSlate()
const { stop } = await tool.watch()

await tool.expectCss([
{
path: './dist/index.css',
expected: '.ml-4 { margin-left: 1rem; }',
},
])

// 2.2. Adding utilities to the html file should add to the generated CSS
await tool.writeFiles([{ path: './src/index.html', contents: `ml-2 ml-4` }])

await tool.expectCss([
{
path: './dist/index.css',

// These are out of order because utilities are generated on demand
expected: '.ml-4 { margin-left: 1rem; } .ml-2 { margin-left: 0.5rem; }',
},
])

// 2.3. Updating the config should update the css
await tool.updateConfig((config) => {
set(config, 'theme.extend.spacing', { 4: '1.5rem' })
})

await tool.expectCss([
{
path: './dist/index.css',

// Now these are in order because changing the config
// forces re-generation of the whole CSS file
expected: '.ml-2 { margin-left: 0.5rem; } .ml-4 { margin-left: 1.5rem; }',
},
])

await stop()
}
}

test('webpack v5', buildTest('webpack-v5'))

afterAll(cleanupAllTools)
248 changes: 248 additions & 0 deletions jit/tooling/Tool.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// @ts-check

const path = require('path')
const { expect } = require('@jest/globals')
const child = require('child_process')
const fs = require('fs')
const { promisify } = require('util')
const glob = require('fast-glob')
const exec = promisify(child.exec)
const writeFile = promisify(fs.writeFile)
const readFile = promisify(fs.readFile)
const deleteFile = promisify(fs.unlink)

/**
* @typedef {'webpack-v5' } IntegrationTestedTool
*/

/**
* @typedef {Record<string, Record<string, string | number | (string | number)[]>>} Theme
* @typedef {Record<string, string[]>} Variants
*
* @typedef {object} TailwindConfig
* @property {'jit' | 'aot'} [mode]
* @property {string|((selectorOrPrefix: string) => string)} [prefix]
* @property {boolean|string} [important]
* @property {string} [separator]
* @property {any[]} [presets]
* @property {false|string[]} [purge]
* @property {false|'class'|'media'} [darkMode]
* @property {Theme & {extend?: Theme}} [theme]
* @property {Variants & {extend?: Variants}} [variants]
* @property {Record<string, boolean>} [corePlugins]
* @property {any[]} [plugins]
*/

/**
* @typedef {object} ToolPrep
* @property {TailwindConfig} [config]
* @property {{path: string, contents: string}[]} [files]
*/

/** @type {import('./Tool.js').Tool[]} */
const all = []

module.exports.Tool = class Tool {
/**
*
* @param {IntegrationTestedTool} name
* @param {ToolPrep} [prep]
*/
constructor(name, prep = {}) {
this.name = name
this.dir = path.resolve(__dirname, `./${name}`)
this.prep = prep
this.lastUsedConfig = prep.config

/** @type {child.ChildProcess[]} */
this.processes = []

all.push(this)
}

/**
*
*/
async cleanSlate() {
// Delete the tailwind.config.js, everything under ./src, everything under ./dist
await this.deleteFiles([
{ path: './tailwind.config.js' },
{ path: './src/**/*' },
{ path: './dist/**/*' },
])

await this.writeFiles([
...this.prep.files,

{
path: './tailwind.config.js',
contents: `module.exports = ${JSON.stringify(this.prep.config)}`,
},
])
}

async waitForFilesToChange(timeout = 1000) {
// TODO: Can we actually wait for the files to change
await new Promise((resolve) => setTimeout(resolve, timeout))
}

/**
*
* @param {TailwindConfig | ((config: TailwindConfig) => TailwindConfig | void)} config
*/
async updateConfig(config) {
if (typeof config === 'function') {
const passedConfig = { ...this.lastUsedConfig }

config = config(passedConfig) || passedConfig
}

this.lastUsedConfig = config

await this.writeFiles([
{
path: './tailwind.config.js',
contents: `module.exports = ${JSON.stringify(config)}`,
},
])
}

/**
*
* @param {{path: string}[]} files
* @returns {Promise<(string|Error|null)[]>}
*/
async readFiles(files) {
return await Promise.all(
files.map(async (file) => {
try {
return await readFile(path.resolve(this.dir, `./${file.path}`), { encoding: 'utf-8' })
} catch (err) {
if (err.code === 'ENOENT') {
return err.message
}

return err
}
})
)
}

/**
*
* @param {{path: string}[]} files
*/
async deleteFiles(files) {
// Find all matching files
const paths = await Promise.all(
files.map((file) => glob(path.resolve(this.dir, `./${file.path}`)))
)

/** @type {string[]} */
// @ts-ignore
const filePaths = paths.flatMap((paths) => paths)

// Delete them all matching files
return await Promise.all(filePaths.map((path) => deleteFile(path)))
}

/**
*
* @param {{path: string, contents: string}[]} files
*/
async writeFiles(files) {
return await Promise.all(
files.map((file) => writeFile(path.resolve(this.dir, `./${file.path}`), file.contents))
)
}

/**
*
* @param {string} script
* @param {Record<string, string>} [env]
*/
run(script, env = {}) {
const promise = exec(`npm run ${script}`, {
cwd: this.dir,
env: {
...process.env,
...env,
},
})

this.processes.push(promise.child)

promise.catch(() => {
// prevent unhandled rejection failure exceptions
})

return promise
}

async build() {
return await this.run('build', {
TAILWIND_MODE: 'build',
NODE_ENV: 'development',
})
}

async watch() {
const p = this.run('watch', {
TAILWIND_MODE: 'watch',
NODE_ENV: 'development',
})

// TODO: Detect that the tool's watcher AND tailwind's watcher have
// both properly started instead of a hardcoded timer
await new Promise((resolve) => setTimeout(resolve, 2000))

return {
stop: () => waitForExit(p.child, () => p.child.kill()),
}
}

async cleanup() {
await Promise.all(
this.processes.map((process) => waitForExit(process, () => process.kill()))
)
}

// Expectation Helpers

/**
*
* @param {{path: string, expected: string}[]} files
*/
async expectCss(files) {
await this.waitForFilesToChange()

const results = await this.readFiles(files)

for (const [index, file] of Object.entries(files)) {
// @ts-ignore
expect(results[index]).toMatchFormattedCss(file.expected)
}
}
}

module.exports.cleanupAllTools = async function cleanupAllTools() {
await Promise.all(all.map(tool => tool.cleanup()))
}

/**
*
* @param {child.ChildProcess} process
* @param {() => any|Promise<any>} callback
*/
async function waitForExit(process, callback) {
if (process.exitCode !== null || process.signalCode !== null) {
return Promise.resolve({ code: process.exitCode, signal: process.signalCode })
}

const p = new Promise((resolve) =>
process.on('exit', (code, signal) => resolve({ code, signal }))
)

await callback()
await p
}
1 change: 1 addition & 0 deletions jit/tooling/webpack-v5/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
2 changes: 2 additions & 0 deletions jit/tooling/webpack-v5/dist/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
Loading

0 comments on commit bfec805

Please sign in to comment.