-
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 all 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.x | ||
| - 22.x | ||
| - latest | ||
| steps: | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { buildProcessPipeline } from "./util.js"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import path from "node:path"; | ||
|
|
||
| export const key = "static"; | ||
| 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); | ||
| } 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, | ||
| * ProcessFile | ||
| * } from "./types.ts" | ||
| * | ||
| * @import { | ||
| * FaucetPlugin, | ||
| * FaucetPluginOptions, | ||
| * WriteFileOpts, | ||
| * } from "faucet-pipeline-core/lib/types.ts" | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { AssetManager } from "faucet-pipeline-core/lib/types.ts" | ||
|
|
||
| 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,123 @@ | ||
| import { readdir, stat } from "node:fs/promises"; | ||
| import 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 { | ||
| * Filter, | ||
| * FileFinderOptions, | ||
| * ProcessFile | ||
| * } from "./types.ts" | ||
| * | ||
| * @import { | ||
| * AssetManager, | ||
| * FaucetPluginFunc, | ||
| * } from "faucet-pipeline-core/lib/types.ts" | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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:
https://github.com/faucet-pipeline/faucet-pipeline-core/blob/main/lib/plugins.js#L5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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.