diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18531b3..6a82b18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,13 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 - - 10 - - 8 + - 18 + - 16 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.js b/index.js index 6aaa8ea..37231a5 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ -'use strict'; -const path = require('path'); -const through = require('through2'); -const vinylFile = require('vinyl-file'); -const revHash = require('rev-hash'); -const revPath = require('rev-path'); -const sortKeys = require('sort-keys'); -const modifyFilename = require('modify-filename'); -const Vinyl = require('vinyl'); -const PluginError = require('plugin-error'); +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import transformStream from 'easy-transform-stream'; +import {vinylFile} from 'vinyl-file'; +import revHash from 'rev-hash'; +import {revPath} from 'rev-path'; +import sortKeys from 'sort-keys'; +import modifyFilename from 'modify-filename'; +import Vinyl from 'vinyl'; +import PluginError from 'plugin-error'; function relativePath(base, filePath) { filePath = filePath.replace(/\\/g, '/'); @@ -35,9 +35,9 @@ function transformFilename(file) { file.path = modifyFilename(file.path, (filename, extension) => { const extIndex = filename.lastIndexOf('.'); - filename = extIndex === -1 ? - revPath(filename, file.revHash) : - revPath(filename.slice(0, extIndex), file.revHash) + filename.slice(extIndex); + filename = extIndex === -1 + ? revPath(filename, file.revHash) + : revPath(filename.slice(0, extIndex), file.revHash) + filename.slice(extIndex); return filename + extension; }); @@ -45,7 +45,7 @@ function transformFilename(file) { const getManifestFile = async options => { try { - return await vinylFile.read(options.path, options); + return await vinylFile(options.path, options); } catch (error) { if (error.code === 'ENOENT') { return new Vinyl(options); @@ -59,21 +59,18 @@ const plugin = () => { const sourcemaps = []; const pathMap = {}; - return through.obj((file, encoding, callback) => { + return transformStream({objectMode: true}, file => { if (file.isNull()) { - callback(null, file); - return; + return file; } if (file.isStream()) { - callback(new PluginError('gulp-rev', 'Streaming not supported')); - return; + throw new PluginError('gulp-rev', 'Streaming not supported'); } // This is a sourcemap, hold until the end if (path.extname(file.path) === '.map') { sourcemaps.push(file); - callback(); return; } @@ -81,15 +78,17 @@ const plugin = () => { transformFilename(file); pathMap[oldPath] = file.revHash; - callback(null, file); - }, function (callback) { + return file; + }, () => { + const files = []; + for (const file of sourcemaps) { let reverseFilename; // Attempt to parse the sourcemap's JSON to get the reverse filename try { reverseFilename = JSON.parse(file.contents.toString()).file; - } catch (_) {} + } catch {} if (!reverseFilename) { reverseFilename = path.relative(path.dirname(file.path), path.basename(file.path, '.map')); @@ -106,10 +105,10 @@ const plugin = () => { transformFilename(file); } - this.push(file); + files.push(file); } - callback(); + return files; }); }; @@ -123,15 +122,14 @@ plugin.manifest = (path_, options) => { merge: false, transformer: JSON, ...options, - ...path_ + ...path_, }; let manifest = {}; - return through.obj((file, encoding, callback) => { + return transformStream({objectMode: true}, file => { // Ignore all non-rev'd files if (!file.path || !file.revOrigPath) { - callback(); return; } @@ -139,37 +137,28 @@ plugin.manifest = (path_, options) => { const originalFile = path.join(path.dirname(revisionedFile), path.basename(file.revOrigPath)).replace(/\\/g, '/'); manifest[originalFile] = revisionedFile; - - callback(); - }, function (callback) { + }, async function * () { // No need to write a manifest file if there's nothing to manifest if (Object.keys(manifest).length === 0) { - callback(); return; } - (async () => { - try { - const manifestFile = await getManifestFile(options); + const manifestFile = await getManifestFile(options); - if (options.merge && !manifestFile.isNull()) { - let oldManifest = {}; + if (options.merge && !manifestFile.isNull()) { + let oldManifest = {}; - try { - oldManifest = options.transformer.parse(manifestFile.contents.toString()); - } catch (_) {} + try { + oldManifest = options.transformer.parse(manifestFile.contents.toString()); + } catch {} + + manifest = Object.assign(oldManifest, manifest); + } - manifest = Object.assign(oldManifest, manifest); - } + manifestFile.contents = Buffer.from(options.transformer.stringify(sortKeys(manifest), undefined, ' ')); - manifestFile.contents = Buffer.from(options.transformer.stringify(sortKeys(manifest), undefined, ' ')); - this.push(manifestFile); - callback(); - } catch (error) { - callback(error); - } - })(); + yield manifestFile; }); }; -module.exports = plugin; +export default plugin; diff --git a/package.json b/package.json index 869dcc1..afbd04c 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,16 @@ "description": "Static asset revisioning by appending content hash to filenames: unicorn.css => unicorn-d41d8cd98f.css", "license": "MIT", "repository": "sindresorhus/gulp-rev", + "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "sindresorhus.com" }, + "type": "module", + "exports": "./index.js", "engines": { - "node": ">=8" + "node": ">=16" }, "scripts": { "test": "xo && ava" @@ -34,19 +37,19 @@ "assets" ], "dependencies": { - "modify-filename": "^1.1.0", - "plugin-error": "^1.0.1", - "rev-hash": "^3.0.0", - "rev-path": "^2.0.0", - "sort-keys": "^4.0.0", - "through2": "^3.0.1", - "vinyl": "^2.1.0", - "vinyl-file": "^3.0.0" + "easy-transform-stream": "^1.0.0", + "modify-filename": "^2.0.0", + "plugin-error": "^2.0.1", + "rev-hash": "^4.0.0", + "rev-path": "^3.0.0", + "sort-keys": "^5.0.0", + "vinyl": "^3.0.0", + "vinyl-file": "^5.0.0" }, "devDependencies": { - "ava": "^2.3.0", - "p-event": "^4.1.0", - "xo": "^0.24.0" + "ava": "^5.1.0", + "p-event": "^5.0.1", + "xo": "^0.53.1" }, "peerDependencies": { "gulp": ">=4" diff --git a/readme.md b/readme.md index d87fe35..9d24566 100644 --- a/readme.md +++ b/readme.md @@ -16,10 +16,10 @@ $ npm install --save-dev gulp-rev ## Usage ```js -const gulp = require('gulp'); -const rev = require('gulp-rev'); +import gulp from 'gulp'; +import rev from 'gulp-rev'; -exports.default = () => ( +export default () => ( gulp.src('src/*.css') .pipe(rev()) .pipe(gulp.dest('dist')) @@ -83,10 +83,10 @@ The hash of each rev'd file is stored at `file.revHash`. You can use this for cu ### Asset manifest ```js -const gulp = require('gulp'); -const rev = require('gulp-rev'); +import gulp from 'gulp'; +import rev from 'gulp-rev'; -exports.default = () => ( +export default () => ( // By default, Gulp would pick `assets/css` as the base, // so we need to set it explicitly: gulp.src(['assets/css/*.css', 'assets/js/*.js'], {base: 'assets'}) @@ -110,10 +110,10 @@ An asset manifest, mapping the original paths to the revisioned paths, will be w By default, `rev-manifest.json` will be replaced as a whole. To merge with an existing manifest, pass `merge: true` and the output destination (as `base`) to `rev.manifest()`: ```js -const gulp = require('gulp'); -const rev = require('gulp-rev'); +import gulp from 'gulp'; +import rev from 'gulp-rev'; -exports.default = () => ( +export default () => ( // By default, Gulp would pick `assets/css` as the base, // so we need to set it explicitly: gulp.src(['assets/css/*.css', 'assets/js/*.js'], {base: 'assets'}) @@ -135,12 +135,12 @@ You can optionally call `rev.manifest('manifest.json')` to give it a different p Because of the way `gulp-concat` handles file paths, you may need to set `cwd` and `path` manually on your `gulp-concat` instance to get everything to work correctly: ```js -const gulp = require('gulp'); -const rev = require('gulp-rev'); -const sourcemaps = require('gulp-sourcemaps'); -const concat = require('gulp-concat'); +import gulp from 'gulp'; +import rev from 'gulp-rev'; +import sourcemaps from 'gulp-sourcemaps'; +import concat from 'gulp-concat'; -exports.default = () => ( +export default () => ( gulp.src('src/*.js') .pipe(sourcemaps.init()) .pipe(concat({path: 'bundle.js', cwd: ''})) @@ -163,13 +163,13 @@ Since the order of streams are not guaranteed, some plugins such as `gulp-concat This plugin does not support streaming. If you have files from a streaming source, such as Browserify, you should use [`gulp-buffer`](https://github.com/jeromew/gulp-buffer) before `gulp-rev` in your pipeline: ```js -const gulp = require('gulp'); -const browserify = require('browserify'); -const source = require('vinyl-source-stream'); -const buffer = require('gulp-buffer'); -const rev = require('gulp-rev'); +import gulp from 'gulp'; +import browserify from 'browserify'; +import source from 'vinyl-source-stream'; +import buffer from 'gulp-buffer'; +import rev from 'gulp-rev'; -exports.default = () => ( +export default () => ( browserify('src/index.js') .bundle({debug: true}) .pipe(source('index.min.js')) diff --git a/test/_helper.js b/test/_helper.js index b5456d7..5df1eaf 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -1,3 +1,4 @@ +import {Buffer} from 'node:buffer'; import Vinyl from 'vinyl'; export default function createFile({ @@ -8,13 +9,13 @@ export default function createFile({ revName, cwd, base, - contents = '' + contents = '', }) { const file = new Vinyl({ path, cwd, base, - contents: Buffer.from(contents) + contents: Buffer.from(contents), }); file.revOrigPath = revOrigPath; file.revOrigBase = revOrigBase; diff --git a/test/manifest.js b/test/manifest.js index a7289bd..101a5b5 100644 --- a/test/manifest.js +++ b/test/manifest.js @@ -1,8 +1,12 @@ -import path from 'path'; +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; import test from 'ava'; -import pEvent from 'p-event'; -import rev from '..'; -import createFile from './_helper'; +import {pEvent} from 'p-event'; +import rev from '../index.js'; +import createFile from './_helper.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const manifestFixture = './test.manifest-fixture.json'; const manifestFixturePath = path.join(__dirname, manifestFixture); @@ -14,14 +18,14 @@ test('builds a rev manifest file', async t => { stream.end(createFile({ path: 'unicorn-d41d8cd98f.css', - revOrigPath: 'unicorn.css' + revOrigPath: 'unicorn.css', })); const file = await data; t.is(file.relative, 'rev-manifest.json'); t.deepEqual( JSON.parse(file.contents.toString()), - {'unicorn.css': 'unicorn-d41d8cd98f.css'} + {'unicorn.css': 'unicorn-d41d8cd98f.css'}, ); }); @@ -32,7 +36,7 @@ test('allows naming the manifest file', async t => { stream.end(createFile({ path: 'unicorn-d41d8cd98f.css', - revOrigPath: 'unicorn.css' + revOrigPath: 'unicorn.css', })); const file = await data; @@ -42,13 +46,13 @@ test('allows naming the manifest file', async t => { test('appends to an existing rev manifest file', async t => { const stream = rev.manifest({ path: manifestFixturePath, - merge: true + merge: true, }); const data = pEvent(stream, 'data'); stream.end(createFile({ path: 'unicorn-d41d8cd98f.css', - revOrigPath: 'unicorn.css' + revOrigPath: 'unicorn.css', })); const file = await data; @@ -57,8 +61,8 @@ test('appends to an existing rev manifest file', async t => { JSON.parse(file.contents.toString()), { 'app.js': 'app-a41d8cd1.js', - 'unicorn.css': 'unicorn-d41d8cd98f.css' - } + 'unicorn.css': 'unicorn-d41d8cd98f.css', + }, ); }); @@ -68,37 +72,37 @@ test('does not append to an existing rev manifest by default', async t => { stream.end(createFile({ path: 'unicorn-d41d8cd98f.css', - revOrigPath: 'unicorn.css' + revOrigPath: 'unicorn.css', })); const file = await data; t.is(file.relative, manifestFixtureRelative); t.deepEqual( JSON.parse(file.contents.toString()), - {'unicorn.css': 'unicorn-d41d8cd98f.css'} + {'unicorn.css': 'unicorn-d41d8cd98f.css'}, ); }); test('sorts the rev manifest keys', async t => { const stream = rev.manifest({ path: manifestFixturePath, - merge: true + merge: true, }); const data = pEvent(stream, 'data'); stream.write(createFile({ path: 'unicorn-d41d8cd98f.css', - revOrigPath: 'unicorn.css' + revOrigPath: 'unicorn.css', })); stream.end(createFile({ path: 'pony-d41d8cd98f.css', - revOrigPath: 'pony.css' + revOrigPath: 'pony.css', })); const file = await data; t.deepEqual( Object.keys(JSON.parse(file.contents.toString())), - ['app.js', 'pony.css', 'unicorn.css'] + ['app.js', 'pony.css', 'unicorn.css'], ); }); @@ -113,7 +117,7 @@ test('respects directories', async t => { revOrigPath: path.join(__dirname, 'foo', 'unicorn.css'), revOrigBase: __dirname, origName: 'unicorn.css', - revName: 'unicorn-d41d8cd98f.css' + revName: 'unicorn-d41d8cd98f.css', })); stream.end(createFile({ cwd: __dirname, @@ -122,7 +126,7 @@ test('respects directories', async t => { revOrigBase: __dirname, revOrigPath: path.join(__dirname, 'bar', 'pony.css'), origName: 'pony.css', - revName: 'pony-d41d8cd98f.css' + revName: 'pony-d41d8cd98f.css', })); const MANIFEST = {}; @@ -146,7 +150,7 @@ test('respects files coming from directories with different bases', async t => { revOrigBase: path.join(__dirname, 'vendor1'), revOrigPath: path.join(__dirname, 'vendor1', 'foo', 'scriptfoo.js'), origName: 'scriptfoo.js', - revName: 'scriptfoo-d41d8cd98f.js' + revName: 'scriptfoo-d41d8cd98f.js', })); stream.end(createFile({ cwd: __dirname, @@ -155,7 +159,7 @@ test('respects files coming from directories with different bases', async t => { revOrigBase: path.join(__dirname, 'vendor2'), revOrigPath: path.join(__dirname, 'vendor2', 'bar', 'scriptbar.js'), origName: 'scriptfoo.js', - revName: 'scriptfoo-d41d8cd98f.js' + revName: 'scriptfoo-d41d8cd98f.js', })); const MANIFEST = {}; @@ -175,13 +179,13 @@ test('uses correct base path for each file', async t => { cwd: 'app/', base: 'app/', path: path.join('app', 'foo', 'scriptfoo-d41d8cd98f.js'), - revOrigPath: 'scriptfoo.js' + revOrigPath: 'scriptfoo.js', })); stream.end(createFile({ cwd: '/', base: 'assets/', path: path.join('/assets', 'bar', 'scriptbar-d41d8cd98f.js'), - revOrigPath: 'scriptbar.js' + revOrigPath: 'scriptbar.js', })); const MANIFEST = {}; diff --git a/test/rev.js b/test/rev.js index a47ba87..7cc967d 100644 --- a/test/rev.js +++ b/test/rev.js @@ -1,15 +1,15 @@ -import path from 'path'; +import path from 'node:path'; import test from 'ava'; -import pEvent from 'p-event'; -import rev from '..'; -import createFile from './_helper'; +import {pEvent, pEventIterator} from 'p-event'; +import rev from '../index.js'; +import createFile from './_helper.js'; test('revs files', async t => { const stream = rev(); const data = pEvent(stream, 'data'); stream.end(createFile({ - path: 'unicorn.css' + path: 'unicorn.css', })); const file = await data; @@ -22,7 +22,7 @@ test('adds the revision hash before the first `.` in the filename', async t => { const data = pEvent(stream, 'data'); stream.end(createFile({ - path: 'unicorn.css.map' + path: 'unicorn.css.map', })); const file = await data; @@ -35,7 +35,7 @@ test('stores the hashes for later', async t => { const data = pEvent(stream, 'data'); stream.end(createFile({ - path: 'unicorn.css' + path: 'unicorn.css', })); const file = await data; @@ -44,64 +44,82 @@ test('stores the hashes for later', async t => { t.is(file.revHash, 'd41d8cd98f'); }); -test.cb('handles sourcemaps transparently', t => { +test('handles sourcemaps transparently', async t => { const stream = rev(); - - stream.on('data', file => { - if (path.extname(file.path) === '.map') { - t.is(file.path, path.normalize('maps/pastissada-d41d8cd98f.css.map')); - t.end(); - } + const data = pEventIterator(stream, 'data', { + resolutionEvents: ['finish'], }); stream.write(createFile({ - path: 'pastissada.css' + path: 'pastissada.css', })); stream.end(createFile({ path: 'maps/pastissada.css.map', - contents: JSON.stringify({file: 'pastissada.css'}) + contents: JSON.stringify({file: 'pastissada.css'}), })); -}); - -test.cb('handles unparseable sourcemaps correctly', t => { - const stream = rev(); - stream.on('data', file => { + let sourcemapCount = 0; + for await (const file of data) { if (path.extname(file.path) === '.map') { - t.is(file.path, 'pastissada-d41d8cd98f.css.map'); - t.end(); + t.is(file.path, path.normalize('maps/pastissada-d41d8cd98f.css.map')); + sourcemapCount++; } + } + + t.is(sourcemapCount, 1); +}); + +test('handles unparseable sourcemaps correctly', async t => { + const stream = rev(); + const data = pEventIterator(stream, 'data', { + resolutionEvents: ['finish'], }); stream.write(createFile({ - path: 'pastissada.css' + path: 'pastissada.css', })); stream.end(createFile({ path: 'pastissada.css.map', - contents: 'Wait a minute, this is invalid JSON!' + contents: 'Wait a minute, this is invalid JSON!', })); -}); - -test.cb('okay when the optional sourcemap.file is not defined', t => { - const stream = rev(); - stream.on('data', file => { + let sourcemapCount = 0; + for await (const file of data) { if (path.extname(file.path) === '.map') { t.is(file.path, 'pastissada-d41d8cd98f.css.map'); - t.end(); + sourcemapCount++; } + } + + t.is(sourcemapCount, 1); +}); + +test('okay when the optional sourcemap.file is not defined', async t => { + const stream = rev(); + const data = pEventIterator(stream, 'data', { + resolutionEvents: ['finish'], }); stream.write(createFile({ - path: 'pastissada.css' + path: 'pastissada.css', })); stream.end(createFile({ path: 'pastissada.css.map', - contents: JSON.stringify({}) + contents: JSON.stringify({}), })); + + let sourcemapCount = 0; + for await (const file of data) { + if (path.extname(file.path) === '.map') { + t.is(file.path, 'pastissada-d41d8cd98f.css.map'); + sourcemapCount++; + } + } + + t.is(sourcemapCount, 1); }); test('handles a `.` in the folder name', async t => { @@ -109,7 +127,7 @@ test('handles a `.` in the folder name', async t => { const data = pEvent(stream, 'data'); stream.end(createFile({ - path: 'mysite.io/unicorn.css' + path: 'mysite.io/unicorn.css', })); const file = await data;