1- let path = require ( "path" ) ;
2- let { FileFinder } = require ( "faucet-pipeline-core/lib/util/files/finder" ) ;
3- let sharp = require ( "sharp" ) ;
4- let svgo = require ( "svgo" ) ;
5- let { stat, readFile } = require ( "fs" ) . promises ;
6- let { abort } = require ( "faucet-pipeline-core/lib/util" ) ;
1+ import path from "node:path" ;
2+ import sharp from "sharp" ;
3+ import svgo from "svgo" ;
4+ import { readFile } from "node:fs/promises" ;
5+ import { buildProcessPipeline } from "faucet-pipeline-static/lib/util.js" ;
6+ import { abort , repr } from "faucet-pipeline-core/lib/util/index.js" ;
7+
8+ export const key = "images" ;
9+ export const bucket = "static" ;
10+
11+ export function plugin ( config , assetManager ) {
12+ let pipeline = config . map ( optimizerConfig => {
13+ let processFile = buildProcessFile ( optimizerConfig ) ;
14+ let { source, target } = optimizerConfig ;
15+ let filter = optimizerConfig . filter ||
16+ withFileExtension ( "avif" , "jpg" , "jpeg" , "png" , "webp" , "svg" ) ;
17+ return buildProcessPipeline ( source , target , processFile , assetManager , filter ) ;
18+ } ) ;
19+
20+ return filepaths => Promise . all ( pipeline . map ( optimize => optimize ( filepaths ) ) ) ;
21+ }
722
823// we can optimize the settings here, but some would require libvips
924// to be compiled with additional stuff
@@ -51,99 +66,54 @@ let settings = {
5166 avif : { }
5267} ;
5368
54- module . exports = {
55- key : "images" ,
56- bucket : "static" ,
57- plugin : faucetImages
58- } ;
59-
60- function faucetImages ( config , assetManager ) {
61- let optimizers = config . map ( optimizerConfig =>
62- makeOptimizer ( optimizerConfig , assetManager ) ) ;
63-
64- return filepaths => Promise . all ( optimizers . map ( optimize => optimize ( filepaths ) ) ) ;
65- }
66-
67- function makeOptimizer ( optimizerConfig , assetManager ) {
68- let source = assetManager . resolvePath ( optimizerConfig . source ) ;
69- let target = assetManager . resolvePath ( optimizerConfig . target , {
70- enforceRelative : true
71- } ) ;
72- let fileFinder = new FileFinder ( source , {
73- skipDotfiles : true ,
74- filter : optimizerConfig . filter ||
75- withFileExtension ( "avif" , "jpg" , "jpeg" , "png" , "webp" , "svg" )
76- } ) ;
77- let {
78- autorotate,
79- fingerprint,
80- format,
81- width,
82- height,
83- crop,
84- quality,
85- scale,
86- suffix
87- } = optimizerConfig ;
88-
89- return async filepaths => {
90- let [ fileNames , targetDir ] = await Promise . all ( [
91- ( filepaths ? fileFinder . match ( filepaths ) : fileFinder . all ( ) ) ,
92- determineTargetDir ( source , target )
93- ] ) ;
94- return processFiles ( fileNames , {
95- assetManager,
96- source,
97- target,
98- targetDir,
99- fingerprint,
100- variant : {
101- autorotate, format, width, height, crop, quality, scale, suffix
102- }
103- } ) ;
69+ /**
70+ * Returns a function that processes a single file
71+ */
72+ function buildProcessFile ( config ) {
73+ return async function ( filename ,
74+ { source, target, targetDir, assetManager } ) {
75+ let sourcePath = path . join ( source , filename ) ;
76+ let targetPath = determineTargetPath ( path . join ( target , filename ) , config ) ;
77+
78+ let format = config . format ? config . format : extname ( filename ) ;
79+
80+ let output = format === "svg" ?
81+ await optimizeSVG ( sourcePath ) :
82+ await optimizeBitmap ( sourcePath , format , config ) ;
83+
84+ let writeOptions = { targetDir } ;
85+ if ( config . fingerprint !== undefined ) {
86+ writeOptions . fingerprint = config . fingerprint ;
87+ }
88+ return assetManager . writeFile ( targetPath , output , writeOptions ) ;
10489 } ;
10590}
10691
107- // If `source` is a directory, `target` is used as target directory -
108- // otherwise, `target`'s parent directory is used
109- async function determineTargetDir ( source , target ) {
110- let results = await stat ( source ) ;
111- return results . isDirectory ( ) ? target : path . dirname ( target ) ;
112- }
113-
114- async function processFiles ( fileNames , config ) {
115- return Promise . all ( fileNames . map ( fileName => processFile ( fileName , config ) ) ) ;
116- }
117-
118- async function processFile ( fileName ,
119- { source, target, targetDir, fingerprint, assetManager, variant } ) {
120- let sourcePath = path . join ( source , fileName ) ;
121- let targetPath = determineTargetPath ( path . join ( target , fileName ) , variant ) ;
122-
123- let format = variant . format ? variant . format : extname ( fileName ) ;
124-
125- let output = format === "svg" ?
126- await optimizeSVG ( sourcePath ) :
127- await optimizeBitmap ( sourcePath , format , variant ) ;
128-
129- let writeOptions = { targetDir } ;
130- if ( fingerprint !== undefined ) {
131- writeOptions . fingerprint = fingerprint ;
132- }
133- return assetManager . writeFile ( targetPath , output , writeOptions ) ;
134- }
135-
92+ /**
93+ * Optimize a single SVG
94+ *
95+ * @param {string } sourcePath
96+ * @returns {Promise<string> }
97+ */
13698async function optimizeSVG ( sourcePath ) {
13799 let input = await readFile ( sourcePath ) ;
138100
139101 try {
140102 let output = await svgo . optimize ( input , settings . svg ) ;
141103 return output . data ;
142104 } catch ( error ) {
143- abort ( `Only SVG can be converted to SVG: ${ sourcePath } ` ) ;
105+ abort ( `Only SVG can be converted to SVG: ${ repr ( sourcePath ) } ` ) ;
144106 }
145107}
146108
109+ /**
110+ * Optimize a single bitmap image
111+ *
112+ * @param {string } sourcePath
113+ * @param {string } format
114+ * @param {Object } options
115+ * @returns {Promise<Buffer> }
116+ */
147117async function optimizeBitmap ( sourcePath , format ,
148118 { autorotate, width, height, scale, quality, crop } ) {
149119 let image = sharp ( sourcePath ) ;
@@ -153,7 +123,12 @@ async function optimizeBitmap(sourcePath, format,
153123
154124 if ( scale ) {
155125 let metadata = await image . metadata ( ) ;
156- image . resize ( { width : metadata . width * scale , height : metadata . height * scale } ) ;
126+ if ( metadata . width && metadata . height ) {
127+ image . resize ( {
128+ width : metadata . width * scale ,
129+ height : metadata . height * scale
130+ } ) ;
131+ }
157132 }
158133
159134 if ( width || height ) {
@@ -176,12 +151,17 @@ async function optimizeBitmap(sourcePath, format,
176151 image . avif ( { ...settings . avif , quality } ) ;
177152 break ;
178153 default :
179- abort ( `unsupported format ${ format } . We support: AVIF, JPG, PNG, WebP, SVG` ) ;
154+ abort ( `unsupported format ${ repr ( format ) } . We support: AVIF, JPG, PNG, WebP, SVG` ) ;
180155 }
181156
182157 return image . toBuffer ( ) ;
183158}
184159
160+ /**
161+ * @param {string } filepath
162+ * @param {Object } options
163+ * @returns {string }
164+ */
185165function determineTargetPath ( filepath , { format, suffix = "" } ) {
186166 format = format ? `.${ format } ` : "" ;
187167 let directory = path . dirname ( filepath ) ;
@@ -190,11 +170,20 @@ function determineTargetPath(filepath, { format, suffix = "" }) {
190170 return path . join ( directory , `${ basename } ${ suffix } ${ extension } ${ format } ` ) ;
191171}
192172
173+ /**
174+ * @param {...string } extensions
175+ * @returns {Filter }
176+ */
193177function withFileExtension ( ...extensions ) {
194178 return filename => extensions . includes ( extname ( filename ) ) ;
195179}
196180
197- // extname follows this annoying idea that the dot belongs to the extension
181+ /**
182+ * File extension of a filename without the dot
183+ *
184+ * @param {string } filename
185+ * @returns {string }
186+ */
198187function extname ( filename ) {
199188 return path . extname ( filename ) . slice ( 1 ) . toLowerCase ( ) ;
200189}
0 commit comments