-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add simple tooling setup for webpack v5
- Loading branch information
1 parent
21a96af
commit bfec805
Showing
10 changed files
with
1,654 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* | ||
!.gitignore |
Oops, something went wrong.