Skip to content

Commit e3f330e

Browse files
committed
Vendor FileFinder to make this dual compatible, type the code
1 parent 23eeb22 commit e3f330e

File tree

5 files changed

+250
-16
lines changed

5 files changed

+250
-16
lines changed

index.js

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
1-
let { readFile, stat } = require("fs").promises;
2-
let path = require("path");
3-
let { FileFinder } = require("faucet-pipeline-core/lib/util/files/finder");
1+
let { readFile, stat } = require("node:fs/promises");
2+
let path = require("node:path");
3+
let { FileFinder } = require("./lib.js");
4+
/** @import {
5+
* Config,
6+
* AssetManager,
7+
* FaucetPlugin,
8+
* FaucetPluginOptions,
9+
* FaucetPluginFunc,
10+
* CompactorMap,
11+
* WriteFileOpts,
12+
* ProcessFile
13+
* } from "./types.ts"
14+
**/
415

516
module.exports = {
617
key: "static",
718
bucket: "static",
819
plugin: faucetStatic
920
};
1021

22+
/**
23+
* The static plugin copies files with an option to define compaction functions
24+
*
25+
* @type FaucetPlugin<Config>
26+
*/
1127
function faucetStatic(config, assetManager, { compact } = {}) {
1228
let copiers = config.map(copyConfig =>
1329
makeCopier(copyConfig, assetManager, { compact }));
1430

1531
return filepaths => Promise.all(copiers.map(copy => copy(filepaths)));
1632
}
1733

34+
/**
35+
* Create a copier for a single configuration
36+
*
37+
* @param {Config} copyConfig
38+
* @param {AssetManager} assetManager
39+
* @param {FaucetPluginOptions} options
40+
* @returns {FaucetPluginFunc}
41+
*/
1842
function makeCopier(copyConfig, assetManager, { compact } = {}) {
1943
let source = assetManager.resolvePath(copyConfig.source);
2044
let target = assetManager.resolvePath(copyConfig.target, {
@@ -25,47 +49,71 @@ function makeCopier(copyConfig, assetManager, { compact } = {}) {
2549
filter: copyConfig.filter
2650
});
2751
let { fingerprint } = copyConfig;
28-
let plugins = determinePlugins(compact, copyConfig);
52+
let compactors = determineCompactors(compact, copyConfig);
2953

3054
return filepaths => {
3155
return Promise.all([
3256
(filepaths ? fileFinder.match(filepaths) : fileFinder.all()),
3357
determineTargetDir(source, target)
3458
]).then(([fileNames, targetDir]) => {
3559
return processFiles(fileNames, {
36-
assetManager, source, target, targetDir, plugins, fingerprint
60+
assetManager, source, target, targetDir, compactors, fingerprint
3761
});
3862
});
3963
};
4064
}
4165

42-
function determinePlugins(compact, copyConfig) {
66+
/**
67+
* Determine which compactors should be used
68+
*
69+
* @param {boolean | undefined} compact
70+
* @param {Config} copyConfig
71+
* @returns {CompactorMap}
72+
*/
73+
function determineCompactors(compact, copyConfig) {
4374
if(!compact) {
4475
return {};
4576
}
4677

4778
return copyConfig.compact || {};
4879
}
4980

50-
// If `source` is a directory, `target` is used as target directory -
51-
// otherwise, `target`'s parent directory is used
81+
/**
82+
* If `source` is a directory, `target` is used as target directory -
83+
* otherwise, `target`'s parent directory is used
84+
*
85+
* @param {string} source
86+
* @param {string} target
87+
* @returns {Promise<string>}
88+
*/
5289
function determineTargetDir(source, target) {
5390
return stat(source).
5491
then(results => results.isDirectory() ? target : path.dirname(target));
5592
}
5693

94+
/**
95+
* @param {string[]} fileNames
96+
* @param {ProcessFile} config
97+
* @returns {Promise<unknown>}
98+
*/
5799
function processFiles(fileNames, config) {
58100
return Promise.all(fileNames.map(fileName => processFile(fileName, config)));
59101
}
60102

103+
/**
104+
* @param {string} fileName
105+
* @param {ProcessFile} opts
106+
* @returns {Promise<unknown>}
107+
*/
61108
async function processFile(fileName,
62-
{ source, target, targetDir, fingerprint, assetManager, plugins }) {
109+
{ source, target, targetDir, fingerprint, assetManager, compactors }) {
63110
let sourcePath = path.join(source, fileName);
64111
let targetPath = path.join(target, fileName);
65112

66113
try {
67114
var content = await readFile(sourcePath); // eslint-disable-line no-var
68115
} catch(err) {
116+
// @ts-ignore
69117
if(err.code !== "ENOENT") {
70118
throw err;
71119
}
@@ -74,18 +122,27 @@ async function processFile(fileName,
74122
}
75123

76124
let type = determineFileType(sourcePath);
77-
if(type && plugins[type]) {
78-
let plugin = plugins[type];
79-
content = await plugin(content);
125+
if(type && compactors[type]) {
126+
let compactor = compactors[type];
127+
content = await compactor(content);
80128
}
81129

130+
/**
131+
* @type WriteFileOpts
132+
*/
82133
let options = { targetDir };
83134
if(fingerprint !== undefined) {
84135
options.fingerprint = fingerprint;
85136
}
86137
return assetManager.writeFile(targetPath, content, options);
87138
}
88139

140+
/**
141+
* The filetype is the lower case file extension
142+
*
143+
* @param {string} sourcePath
144+
* @returns {string}
145+
*/
89146
function determineFileType(sourcePath) {
90147
return path.extname(sourcePath).substr(1).toLowerCase();
91148
}

lib.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
let { readdir, stat } = require("node:fs/promises");
2+
let path = require("node:path");
3+
/** @import { FileFinderOptions } from "./types.ts" */
4+
5+
exports.FileFinder = class FileFinder {
6+
/**
7+
* @param {string} directory
8+
* @param {FileFinderOptions} options?
9+
*/
10+
constructor(directory, { skipDotfiles, filter = () => true } = {}) {
11+
this.directory = directory;
12+
/**
13+
* @param {string} filename
14+
* @return {boolean}
15+
*/
16+
this.filter = filename => {
17+
if(skipDotfiles && isDotfile(filename)) {
18+
return false;
19+
}
20+
return filter(filename);
21+
};
22+
}
23+
24+
/**
25+
* a list of relative file paths within the respective directory
26+
*
27+
* @returns {Promise<string[]>}
28+
*/
29+
all() {
30+
return tree(this.directory).
31+
then(filenames => filenames.filter(this.filter));
32+
}
33+
34+
/**
35+
* all file paths that match the filter function
36+
*
37+
* @param {string[]} filepaths
38+
* @returns {Promise<string[]>}
39+
*/
40+
match(filepaths) {
41+
return filesWithinDirectory(this.directory, filepaths).
42+
then(filepaths => filepaths.filter(this.filter));
43+
}
44+
};
45+
46+
/**
47+
* @param {string} filepath
48+
* @param {string} referenceDir
49+
* @returns {Promise<string[]>}
50+
*/
51+
function tree(filepath, referenceDir = filepath) {
52+
return stat(filepath).
53+
then(res => {
54+
if(!res.isDirectory()) {
55+
return [path.relative(referenceDir, filepath)];
56+
}
57+
58+
return readdir(filepath).
59+
then(entries => {
60+
let res = Promise.all(entries.map(entry => {
61+
return tree(path.join(filepath, entry), referenceDir);
62+
}));
63+
return res.then(flatten);
64+
});
65+
});
66+
}
67+
68+
/**
69+
* @param {string} directory
70+
* @param {string[]} files
71+
* @returns {Promise<string[]>}
72+
*/
73+
function filesWithinDirectory(directory, files) {
74+
return new Promise(resolve => {
75+
resolve(files.
76+
map(filepath => path.relative(directory, filepath)).
77+
filter(filename => !filename.startsWith("..")));
78+
});
79+
}
80+
81+
/**
82+
* @param {string} filename
83+
* @returns {boolean}
84+
*/
85+
function isDotfile(filename) {
86+
return path.basename(filename).startsWith(".");
87+
}
88+
89+
/**
90+
* @param {string[][]} arr
91+
* @returns {string[]}
92+
*/
93+
function flatten(arr) {
94+
/**
95+
* I'm deeply sorry for this
96+
* @type string[]
97+
*/
98+
const akwardlyTypedStringArray = [];
99+
return akwardlyTypedStringArray.concat.apply([], arr);
100+
}

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
"description": "static files for faucet-pipeline",
55
"main": "index.js",
66
"scripts": {
7-
"test": "npm run lint && npm run test:cli",
7+
"test": "npm run lint && npm run typecheck && npm run test:cli",
88
"test:cli": "./test/run",
9-
"lint": "eslint --cache index.js test && echo ✓"
9+
"lint": "eslint --cache index.js test && echo ✓",
10+
"typecheck": "tsc"
1011
},
1112
"repository": {
1213
"type": "git",
@@ -22,11 +23,13 @@
2223
"node": ">= 20"
2324
},
2425
"dependencies": {
25-
"faucet-pipeline-core": "^3.0.0"
26+
"faucet-pipeline-core": "^2.0.0 || ^3.0.0"
2627
},
2728
"devDependencies": {
29+
"@types/node": "^22.13.10",
2830
"eslint-config-fnd": "^1.13.0",
2931
"json-diff": "^1.0.0",
30-
"release-util-fnd": "^3.0.0"
32+
"release-util-fnd": "^3.0.0",
33+
"typescript": "^5.8.2"
3134
}
3235
}

tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"checkJs": true,
5+
"noEmit": true,
6+
"strict": true,
7+
"allowImportingTsExtensions": true,
8+
"lib": ["dom", "es2023"],
9+
},
10+
"exclude": [
11+
"./test/**",
12+
]
13+
}

types.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// faucet-pipeline-core types
2+
export interface FaucetPlugin<T> {
3+
(config: T[], assetManager: AssetManager, options: FaucetPluginOptions): FaucetPluginFunc
4+
}
5+
6+
export interface FaucetPluginFunc {
7+
(filepaths: string[]): Promise<unknown>
8+
}
9+
10+
export interface FaucetPluginOptions {
11+
browsers?: string[],
12+
sourcemaps?: boolean,
13+
compact?: boolean
14+
}
15+
16+
export interface AssetManager {
17+
resolvePath: (path: string, opts?: ResolvePathOpts) => string
18+
writeFile: (targetPath: string, content: Buffer, options: WriteFileOpts) => Promise<unknown>
19+
}
20+
21+
export interface ResolvePathOpts {
22+
enforceRelative?: boolean
23+
}
24+
25+
export interface WriteFileOpts {
26+
targetDir: string,
27+
fingerprint?: boolean
28+
}
29+
30+
// faucet-pipeline-static types
31+
export interface Config {
32+
source: string,
33+
target: string,
34+
targetDir: string,
35+
fingerprint?: boolean,
36+
compact?: CompactorMap,
37+
assetManager: AssetManager,
38+
filter?: (fileName: string) => boolean
39+
}
40+
41+
export interface CompactorMap {
42+
[fileExtension: string]: Compactor
43+
}
44+
45+
export interface Compactor {
46+
(contact: Buffer): Promise<Buffer>
47+
}
48+
49+
export interface ProcessFile {
50+
source: string,
51+
target: string,
52+
targetDir: string,
53+
fingerprint?: boolean,
54+
compactors: CompactorMap,
55+
assetManager: AssetManager,
56+
}
57+
58+
export interface FileFinderOptions {
59+
skipDotfiles?: boolean,
60+
filter?: (filename: string) => boolean
61+
}

0 commit comments

Comments
 (0)