Skip to content

Commit 4ff09ab

Browse files
Use an AsyncGenerator for the public API (#36)
1 parent 2c11479 commit 4ff09ab

File tree

12 files changed

+176
-44
lines changed

12 files changed

+176
-44
lines changed

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"printWidth": 150,
2+
"printWidth": 120,
33
"trailingComma": "all",
44
"parser": "typescript",
55
"singleQuote": true

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Audit the filesize of items specified in package.json.",
55
"author": "Kristofer Baxter",
66
"license": "Apache-2.0",
7-
"main": "dist/filesize",
7+
"main": "dist/api.js",
88
"module": "dist/api.mjs",
99
"files": [
1010
"dist"
@@ -88,5 +88,8 @@
8888
"pre-push": "yarn npm-run-all clean build test",
8989
"pre-commit": "lint-staged"
9090
}
91+
},
92+
"publishConfig": {
93+
"access": "public"
9194
}
9295
}

src/api.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import Config from './validation/Config';
1919
import { Context, SizeMap } from './validation/Condition';
2020
import compress from './compress';
2121

22-
export async function report(projectPath: string): Promise<[SizeMap, SizeMap]> {
22+
export async function* report(
23+
projectPath: string,
24+
fileModifier: ((contents: string) => string) | null,
25+
): AsyncGenerator<[SizeMap, SizeMap]> {
2326
const conditions = [Project, Config];
2427
let context: Context = {
2528
projectPath,
@@ -31,6 +34,8 @@ export async function report(projectPath: string): Promise<[SizeMap, SizeMap]> {
3134
compressed: new Map(),
3235
// Stores the basis of comparison.
3336
comparison: new Map(),
37+
fileModifier,
38+
fileContents: new Map(),
3439
};
3540

3641
for (const condition of conditions) {
@@ -40,6 +45,11 @@ export async function report(projectPath: string): Promise<[SizeMap, SizeMap]> {
4045
}
4146
}
4247

43-
await compress(context);
48+
const compressResults = compress(context, false);
49+
let nextResult = await compressResults.next();
50+
while (!nextResult.done) {
51+
yield [context.compressed, context.comparison];
52+
nextResult = await compressResults.next();
53+
}
4454
return [context.compressed, context.comparison];
4555
}

src/compress.ts

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ interface CompressionItem {
4848
* @param error Error from compressing an Item
4949
* @param size actual size for this comparison
5050
*/
51-
function store(report: Report | null, context: Context, item: CompressionItem, error: Error | null, size: number): boolean {
51+
function store(
52+
report: Report | null,
53+
context: Context,
54+
item: CompressionItem,
55+
error: Error | null,
56+
size: number,
57+
): boolean {
5258
if (error !== null) {
5359
LogError(`Could not compress '${item.path}' with '${item.compression}'.`);
5460
return false;
@@ -70,46 +76,64 @@ function store(report: Report | null, context: Context, item: CompressionItem, e
7076
}
7177

7278
/**
73-
* Given a context, compress all Items within splitting work eagly per cpu core to achieve some concurrency.
74-
* @param context Finalized Valid Context from Configuration
79+
* Compress an Item and report status to the console.
80+
* @param item Configuration for an Item.
7581
*/
76-
export default async function compress(context: Context): Promise<boolean> {
77-
/**
78-
* Compress an Item and report status to the console.
79-
* @param item Configuration for an Item.
80-
*/
81-
async function compressor(item: CompressionItem): Promise<boolean> {
82-
const contents = await readFile(item.path);
83-
if (contents) {
84-
const buffer = Buffer.from(contents, 'utf8');
82+
async function compressor(report: Report | null, context: Context, item: CompressionItem): Promise<boolean> {
83+
const contents = context.fileContents.get(item.path);
84+
if (contents) {
85+
const buffer = Buffer.from(contents, 'utf8');
8586

86-
switch (item.compression) {
87-
case 'brotli':
88-
return new Promise(resolve =>
89-
brotliCompress(buffer, BROTLI_OPTIONS, (error: Error | null, result: Buffer) =>
90-
resolve(store(report, context, item, error, result.byteLength)),
91-
),
92-
);
93-
case 'gzip':
94-
return new Promise(resolve =>
95-
gzip(buffer, GZIP_OPTIONS, (error: Error | null, result: Buffer) => resolve(store(report, context, item, error, result.byteLength))),
96-
);
97-
default:
98-
return store(report, context, item, null, buffer.byteLength);
99-
}
87+
switch (item.compression) {
88+
case 'brotli':
89+
return new Promise(resolve =>
90+
brotliCompress(buffer, BROTLI_OPTIONS, (error: Error | null, result: Buffer) =>
91+
resolve(store(report, context, item, error, result.byteLength)),
92+
),
93+
);
94+
case 'gzip':
95+
return new Promise(resolve =>
96+
gzip(buffer, GZIP_OPTIONS, (error: Error | null, result: Buffer) =>
97+
resolve(store(report, context, item, error, result.byteLength)),
98+
),
99+
);
100+
default:
101+
return store(report, context, item, null, buffer.byteLength);
100102
}
103+
}
101104

102-
return false;
105+
return false;
106+
}
107+
108+
/**
109+
* Store the original content so it isn't retrieved from FileSystem for each compression.
110+
* @param context
111+
* @param path
112+
*/
113+
async function storeOriginalFileContents(context: Context, path: string): Promise<void> {
114+
if (!context.fileContents.has(path)) {
115+
let content = await readFile(path);
116+
if (context.fileModifier !== null && content !== null) {
117+
content = context.fileModifier(content);
118+
}
119+
context.fileContents.set(path, content || '');
103120
}
121+
}
104122

105-
let report: Report | null = null;
123+
/**
124+
* Find all items to compress, and store them for future compression.
125+
* @param context
126+
* @param findDefaultSize
127+
*/
128+
async function findItemsToCompress(context: Context, findDefaultSize: boolean): Promise<Array<CompressionItem>> {
106129
const toCompress: Array<CompressionItem> = [];
107130
for (const [path, sizeMapValue] of context.compressed) {
108131
for (let iterator: number = 0; iterator < OrderedCompressionValues.length; iterator++) {
109132
const compression: Compression = OrderedCompressionValues[iterator] as Compression;
110133
const [size, maxSize] = sizeMapValue[iterator];
111-
if (compression === 'none') {
112-
await compressor({ path, compression, maxSize });
134+
await storeOriginalFileContents(context, path);
135+
if (findDefaultSize && compression === 'none') {
136+
await compressor(null, context, { path, compression, maxSize });
113137
}
114138
if (size !== undefined) {
115139
toCompress.push({
@@ -121,18 +145,35 @@ export default async function compress(context: Context): Promise<boolean> {
121145
}
122146
}
123147

124-
report = stdout.isTTY && toCompress.length < 30 ? new TTYReport(context) : new Report(context);
148+
return toCompress;
149+
}
150+
151+
/**
152+
* Given a context, compress all Items within splitting work eagly per cpu core to achieve some concurrency.
153+
* @param context Finalized Valid Context from Configuration
154+
*/
155+
export default async function* compress(context: Context, outputReport: boolean): AsyncGenerator<boolean, boolean> {
156+
const toCompress: Array<CompressionItem> = await findItemsToCompress(context, true);
157+
const report: Report | null = outputReport
158+
? null
159+
: stdout.isTTY && toCompress.length < 30
160+
? new TTYReport(context)
161+
: new Report(context);
125162
let success: boolean = true;
163+
126164
for (let iterator: number = 0; iterator < toCompress.length; iterator += COMPRESSION_CONCURRENCY) {
127165
if (iterator === 0) {
128-
report.update(context);
166+
report?.update(context);
129167
}
130-
let itemsSuccessful = await Promise.all(toCompress.slice(iterator, iterator + COMPRESSION_CONCURRENCY).map(compressor));
168+
let itemsSuccessful = await Promise.all(
169+
toCompress.slice(iterator, iterator + COMPRESSION_CONCURRENCY).map(item => compressor(report, context, item)),
170+
);
131171
if (itemsSuccessful.includes(false)) {
132172
success = false;
133173
}
174+
yield success;
134175
}
135176

136-
report.end();
177+
report?.end();
137178
return success;
138179
}

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const args = mri(process.argv.slice(2), {
4343
compressed: new Map(),
4444
// Stores the basis of comparison.
4545
comparison: new Map(),
46+
fileModifier: null,
47+
fileContents: new Map(),
4648
};
4749

4850
let errorOccured: boolean = false;
@@ -56,7 +58,17 @@ const args = mri(process.argv.slice(2), {
5658
}
5759

5860
if (!errorOccured) {
59-
if (!(await compress(context))) {
61+
const compressResults = compress(context, false);
62+
let successful: boolean = true;
63+
let nextResult: IteratorResult<boolean, boolean> = await compressResults.next();
64+
while (!nextResult.done) {
65+
if (!nextResult.value) {
66+
successful = false;
67+
}
68+
nextResult = await compressResults.next();
69+
}
70+
71+
if (!successful) {
6072
shutdown(6);
6173
}
6274
}

src/validation/Condition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export const SizeMapValueIndex = {
4747
};
4848
export type SizeMap = Map<path, SizeMapValue>;
4949

50+
export type FileContentsMap = Map<path, string>;
51+
5052
export interface Context {
5153
projectPath: string;
5254
packagePath: string;
@@ -57,4 +59,8 @@ export interface Context {
5759
compressed: SizeMap;
5860
// Stores the basis of comparison.
5961
comparison: SizeMap;
62+
// Allows the API to specify a method that alters content before analysis.
63+
fileModifier: ((contents: string) => string) | null;
64+
// Stores the contents of files, to avoid reading from disk per compression type.
65+
fileContents: FileContentsMap;
6066
}

test/config-validation/config-validation.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ test('missing package.json should fail', async t => {
2727
compressed: new Map(),
2828
comparison: new Map(),
2929
silent: false,
30+
fileModifier: null,
31+
fileContents: new Map(),
3032
};
3133
const message = await Config(context)();
3234

@@ -42,6 +44,8 @@ test('unparseable package.json should fail', async t => {
4244
compressed: new Map(),
4345
comparison: new Map(),
4446
silent: false,
47+
fileModifier: null,
48+
fileContents: new Map(),
4549
};
4650
const message = await Config(context)();
4751

@@ -57,6 +61,8 @@ test("missing 'filesize' key from package.json should fail", async t => {
5761
compressed: new Map(),
5862
comparison: new Map(),
5963
silent: false,
64+
fileModifier: null,
65+
fileContents: new Map(),
6066
};
6167
const message = await Config(context)();
6268

@@ -72,6 +78,8 @@ test("missing path from item in 'filesize' should fail", async t => {
7278
compressed: new Map(),
7379
comparison: new Map(),
7480
silent: false,
81+
fileModifier: null,
82+
fileContents: new Map(),
7583
};
7684
const message = await Config(context)();
7785

@@ -87,6 +95,8 @@ test("missing maxSize from item in 'filesize' should fail", async t => {
8795
compressed: new Map(),
8896
comparison: new Map(),
8997
silent: false,
98+
fileModifier: null,
99+
fileContents: new Map(),
90100
};
91101
const message = await Config(context)();
92102

@@ -102,6 +112,8 @@ test("missing compression from item in 'filesize' should fail", async t => {
102112
compressed: new Map(),
103113
comparison: new Map(),
104114
silent: false,
115+
fileModifier: null,
116+
fileContents: new Map(),
105117
};
106118
const message = await Config(context)();
107119

test/config-validation/track.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ test('including trackable items should succeed', async t => {
2929
compressed: new Map(),
3030
comparison: new Map(),
3131
silent: false,
32+
fileModifier: null,
33+
fileContents: new Map(),
3234
};
3335
const message = await Config(context)();
3436

@@ -44,6 +46,12 @@ test('trackable items uses glob to find files', async t => {
4446
const expected: SizeMap = new Map();
4547
expected.set(resolve('test/config-validation/fixtures/track-standalone/index.js'), sizes);
4648

47-
const results = await report('test/config-validation/fixtures/track-standalone');
48-
t.deepEqual(results[0], expected);
49+
const values = report('test/config-validation/fixtures/track-standalone', null);
50+
let next = await values.next();
51+
let results: SizeMap | undefined = undefined;
52+
while (!next.done) {
53+
results = next.value[0];
54+
next = await values.next();
55+
}
56+
t.deepEqual(results, expected);
4957
});

0 commit comments

Comments
 (0)