- 
                Notifications
    
You must be signed in to change notification settings  - Fork 1
 
v2.2.0 #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
v2.2.0 #71
Changes from 6 commits
23eeb22
              e3f330e
              b5f35be
              ac3bda6
              c9522ba
              4e0bbb3
              5f5e8f0
              25e8ef9
              06b436b
              cd249cb
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -7,7 +7,7 @@ jobs: | |
| strategy: | ||
| matrix: | ||
| node-version: | ||
| - 18.x | ||
| - 20.19 | ||
| - 22.x | ||
| - latest | ||
| steps: | ||
| 
          
            
          
           | 
    ||
This file was deleted.
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { buildProcessPipeline } from "./util.js"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| 
     | 
||
| export const key = "static"; | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @FND Do you remember what this name is actually used for? I'm not sure it is used for plugins that are in the default list, only for the ones that are not: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was confused by this as well and haven't investigated it yet (or rather: I don't understand our implementation anymore without investing a fair amount of effort), but tests suggest that a plugin can override faucet-core's defaults. I'm not 100 % sure that interpretation is correct tough. But yes, this value is most likely inert unless it differs from faucet-core's baked-in defaults. It still seems proper for plugins to be self-descriptive though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you don't really need this option for the plugins that are defined in core, but only for those that are not.  | 
||
| export const bucket = "static"; | ||
| 
     | 
||
| /** @type FaucetPlugin<Config> */ | ||
| export function plugin(config, assetManager, options) { | ||
| let pipeline = config.map(copyConfig => { | ||
| let processFile = buildProcessFile(copyConfig, options); | ||
| let { source, target, filter } = copyConfig; | ||
| return buildProcessPipeline(source, target, processFile, assetManager, filter); | ||
| }); | ||
| 
     | 
||
| return filepaths => Promise.all(pipeline.map(copy => copy(filepaths))); | ||
| } | ||
| 
     | 
||
| /** | ||
| * Returns a function that copies a single file with optional compactor | ||
| * | ||
| * @param {Config} copyConfig | ||
| * @param {FaucetPluginOptions} options | ||
| * @returns {ProcessFile} | ||
| */ | ||
| function buildProcessFile(copyConfig, options) { | ||
| let compactors = (options.compact && copyConfig.compact) || {}; | ||
| 
     | 
||
| return async function(filename, | ||
| { source, target, targetDir, assetManager }) { | ||
| let sourcePath = path.join(source, filename); | ||
| let targetPath = path.join(target, filename); | ||
| let content; | ||
| 
     | 
||
| try { | ||
| content = await readFile(sourcePath); | ||
                
      
                  moonglum marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| } catch(err) { | ||
| // @ts-expect-error TS2345 | ||
| if(err.code !== "ENOENT") { | ||
| throw err; | ||
| } | ||
| console.error(`WARNING: \`${sourcePath}\` no longer exists`); | ||
| return; | ||
| } | ||
| 
     | 
||
| let fileExtension = path.extname(sourcePath).substr(1).toLowerCase(); | ||
| if(fileExtension && compactors[fileExtension]) { | ||
| let compactor = compactors[fileExtension]; | ||
| content = await compactor(content); | ||
| } | ||
| 
     | 
||
| /** @type WriteFileOpts */ | ||
| let options = { targetDir }; | ||
| if(copyConfig.fingerprint !== undefined) { | ||
| options.fingerprint = copyConfig.fingerprint; | ||
| } | ||
| return assetManager.writeFile(targetPath, content, options); | ||
| }; | ||
| } | ||
| 
     | 
||
| /** @import { | ||
| * Config, | ||
| * FaucetPlugin, | ||
| * FaucetPluginOptions, | ||
| * WriteFileOpts, | ||
| * ProcessFile | ||
| * } from "./types.ts" | ||
| **/ | ||
                
      
                  moonglum marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // faucet-pipeline-core types | ||
| export interface FaucetPlugin<T> { | ||
| (config: T[], assetManager: AssetManager, options: FaucetPluginOptions): FaucetPluginFunc | ||
| } | ||
| 
     | 
||
| export interface FaucetPluginFunc { | ||
| (filepaths: string[]): Promise<unknown> | ||
| } | ||
| 
     | 
||
| export interface FaucetPluginOptions { | ||
| browsers?: string[], | ||
| sourcemaps?: boolean, | ||
| compact?: boolean | ||
| } | ||
| 
     | 
||
| export interface AssetManager { | ||
| resolvePath: (path: string, opts?: ResolvePathOpts) => string | ||
| writeFile: (targetPath: string, content: Buffer, options: WriteFileOpts) => Promise<unknown> | ||
                
      
                  moonglum marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| } | ||
| 
     | 
||
| export interface ResolvePathOpts { | ||
| enforceRelative?: boolean | ||
| } | ||
| 
     | 
||
| export interface WriteFileOpts { | ||
| targetDir: string, | ||
| fingerprint?: boolean | ||
| } | ||
| 
     | 
||
| // faucet-pipeline-static types | ||
| export interface Config { | ||
| source: string, | ||
| target: string, | ||
| targetDir: string, | ||
| fingerprint?: boolean, | ||
| compact?: CompactorMap, | ||
| assetManager: AssetManager, | ||
| filter?: Filter | ||
| } | ||
| 
     | 
||
| export interface CompactorMap { | ||
| [fileExtension: string]: Compactor | ||
| } | ||
| 
     | 
||
| export interface Compactor { | ||
| (contact: Buffer): Promise<Buffer> | ||
| } | ||
| 
     | 
||
| export interface ProcessFile { | ||
| (filename: string, opts: ProcessFileOptions): Promise<unknown> | ||
| } | ||
| 
     | 
||
| export interface ProcessFileOptions { | ||
| source: string, | ||
| target: string, | ||
| targetDir: string, | ||
| assetManager: AssetManager, | ||
| } | ||
| 
     | 
||
| export interface FileFinderOptions { | ||
| skipDotfiles: boolean, | ||
| filter?: Filter | ||
| } | ||
| 
     | 
||
| export interface Filter { | ||
| (filename: string): boolean | ||
| } | ||
| 
         
      
  
    Contributor
      
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I generally try to keep things local to the respective file, i.e.  In this case, I would have added the following to the bottom of  /** @typedef {{ skipDotfiles: boolean, filter?: Filter }} FileFinderOptions */This not only reduces indirections (which can become quite taxing for the mind), it's also in line with deletability and means you need less expansive  That smell also suggests we might want to move  However, that's just me; YMMV.  | 
||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { readdir, stat } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| 
     | 
||
| /** | ||
| * Creates a processor for a single configuration | ||
| * | ||
| * @param {string} source - the source folder or file for this pipeline | ||
| * @param {string} target - the target folder or file for this pipeline | ||
| * @param {ProcessFile} processFile - process a single file | ||
| * @param {AssetManager} assetManager | ||
| * @param {Filter} [filter] - optional filter based on filenames | ||
| * @returns {FaucetPluginFunc} | ||
| */ | ||
| export function buildProcessPipeline(source, target, processFile, assetManager, filter) { | ||
| source = assetManager.resolvePath(source); | ||
| target = assetManager.resolvePath(target, { | ||
| enforceRelative: true | ||
| }); | ||
| let fileFinder = new FileFinder(source, { | ||
| skipDotfiles: true, | ||
| filter | ||
| }); | ||
| 
     | 
||
| return async filepaths => { | ||
| let [filenames, targetDir] = await Promise.all([ | ||
| (filepaths ? fileFinder.match(filepaths) : fileFinder.all()), | ||
| determineTargetDir(source, target) | ||
| ]); | ||
| 
     | 
||
| return Promise.all(filenames.map(filename => processFile(filename, { | ||
| assetManager, source, target, targetDir | ||
| }))); | ||
| }; | ||
| } | ||
| 
     | 
||
| /** | ||
| * If `source` is a directory, `target` is used as target directory - | ||
| * otherwise, `target`'s parent directory is used | ||
| * | ||
| * @param {string} source | ||
| * @param {string} target | ||
| * @returns {Promise<string>} | ||
| */ | ||
| async function determineTargetDir(source, target) { | ||
| let results = await stat(source); | ||
| return results.isDirectory() ? target : path.dirname(target); | ||
| } | ||
| 
     | 
||
| class FileFinder { | ||
| /** | ||
| * @param {string} root | ||
| * @param {FileFinderOptions} options | ||
| */ | ||
| constructor(root, { skipDotfiles, filter }) { | ||
| this._root = root; | ||
| 
     | 
||
| /** | ||
| * @param {string} filename | ||
| * @return {boolean} | ||
| */ | ||
| this._filter = filename => { | ||
| if(skipDotfiles && path.basename(filename).startsWith(".")) { | ||
| return false; | ||
| } | ||
| return filter ? filter(filename) : true; | ||
| }; | ||
| } | ||
| 
     | 
||
| /** | ||
| * A list of relative file paths within the respective directory | ||
| * | ||
| * @returns {Promise<string[]>} | ||
| */ | ||
| async all() { | ||
| let filenames = await tree(this._root); | ||
| return filenames.filter(this._filter); | ||
| } | ||
| 
     | 
||
| /** | ||
| * All file paths that match the filter function | ||
| * | ||
| * @param {string[]} filepaths | ||
| * @returns {Promise<string[]>} | ||
| */ | ||
| async match(filepaths) { | ||
| return filepaths.map(filepath => path.relative(this._root, filepath)). | ||
| filter(filename => !filename.startsWith("..")). | ||
| filter(this._filter); | ||
| } | ||
| } | ||
| 
     | 
||
| /** | ||
| * Flat list of all files of a directory tree | ||
| * | ||
| * @param {string} filepath | ||
| * @param {string} referenceDir | ||
| * @returns {Promise<string[]>} | ||
| */ | ||
| async function tree(filepath, referenceDir = filepath) { | ||
| let stats = await stat(filepath); | ||
| 
     | 
||
| if(!stats.isDirectory()) { | ||
| return [path.relative(referenceDir, filepath)]; | ||
| } | ||
| 
     | 
||
| let entries = await Promise.all((await readdir(filepath)).map(entry => { | ||
| return tree(path.join(filepath, entry), referenceDir); | ||
| })); | ||
| return entries.flat(); | ||
| } | ||
| 
     | 
||
| /** @import { | ||
| * AssetManager, | ||
| * FaucetPluginFunc, | ||
| * Filter, | ||
| * FileFinderOptions, | ||
| * ProcessFile | ||
| * } from "./types.ts" | ||
| **/ | 
Uh oh!
There was an error while loading. Please reload this page.