Skip to content

Commit

Permalink
Analyze metafiles (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlansley authored Nov 9, 2023
1 parent 44f3a62 commit 780fadf
Show file tree
Hide file tree
Showing 7 changed files with 678 additions and 381 deletions.
622 changes: 317 additions & 305 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 17 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@checkdigit/typescript-config",
"version": "5.1.0",
"version": "5.1.1",
"description": "Check Digit standard Typescript configuration",
"prettier": "@checkdigit/prettier-config",
"engines": {
Expand All @@ -11,7 +11,7 @@
},
"peerDependencies": {
"@types/node": ">=18",
"esbuild": "0.19.4",
"esbuild": "0.19.5",
"typescript": ">=5.2.2 <5.3"
},
"repository": {
Expand Down Expand Up @@ -59,13 +59,13 @@
"devDependencies": {
"@apidevtools/json-schema-ref-parser": "^11.1.0",
"@checkdigit/prettier-config": "^4.1.0",
"@types/debug": "^4.1.9",
"@types/jest": "^29.5.5",
"@types/uuid": "^9.0.4",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@types/debug": "^4.1.10",
"@types/jest": "^29.5.7",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"debug": "^4.3.4",
"eslint": "^8.50.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"get-port": "^7.0.0",
"got": "^11.8.6",
Expand Down Expand Up @@ -96,6 +96,14 @@
"one-var": "off",
"sort-keys": "off",
"sort-imports": "off",
"max-lines": [
"error",
{
"max": 500,
"skipBlankLines": true,
"skipComments": true
}
],
"func-style": [
"error",
"declaration",
Expand Down Expand Up @@ -183,11 +191,7 @@
]
},
"collectCoverageFrom": [
"<rootDir>/src/**",
"!<rootDir>/src/**/*.spec.mts",
"!<rootDir>/src/**/*.test.mts",
"!<rootDir>/src/**/*.spec.ts",
"!<rootDir>/src/**/*.test.ts"
"<rootDir>/src/**"
],
"testMatch": [
"<rootDir>/src/**/*.spec.ts",
Expand Down
34 changes: 34 additions & 0 deletions src/builder/analyze.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// builder/analyze.mts

import { strict as assert } from 'node:assert';

import type { Metafile } from 'esbuild';

export default function analyze(metafile: Metafile) {
const source = new Set(Object.keys(metafile.inputs).filter((key) => !key.startsWith('node_modules')));
const modules = new Set(Object.keys(metafile.inputs).filter((key) => key.startsWith('node_modules')));

const [output] = Object.entries(metafile.outputs);
assert.ok(output !== undefined);
const [, bundle] = output;

const sourceBytes = Object.entries(bundle.inputs).reduce((bytes, [file, value]) => {
if (source.has(file)) {
return bytes + value.bytesInOutput;
}
return bytes;
}, 0);

const moduleBytes = Object.entries(bundle.inputs).reduce((bytes, [file, value]) => {
if (modules.has(file)) {
return bytes + value.bytesInOutput;
}
return bytes;
}, 0);

return {
sourceBytes,
moduleBytes,
totalBytes: bundle.bytes,
};
}
140 changes: 140 additions & 0 deletions src/builder/analyze.spec.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// builder/analyze.spec.mts

import { strict as assert } from 'node:assert';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { v4 as uuid } from 'uuid';

/*
* The below imports work, but tsc complains:
* TS5097: An import path can only end with a .mts extension when allowImportingTsExtensions is enabled
*
* This will be fixed once this library can be 100% ESM and all the .mts files are converted to .ts.
*/

// @ts-expect-error
import builder from './builder.mts';

// @ts-expect-error
import analyze from './analyze.mts';

const twoModules = {
[`index.ts`]: `import { hello } from './thing';\nexport default hello + 'world';\n`,
[`thing.ts`]: `export const hello = 'world';`,
};

const importExternalModule = {
[`index.ts`]: `
import { hello as test } from 'test-esm-module';
import util from 'node:util';
export const hello = { test, message: util.format('hello %s', 'world') };
`,
};

const testNodeModules = {
[`test-cjs-module`]: {
source: {
[`index.js`]: `module.exports.goodbye = 'world';`,
[`index.d.ts`]: `export declare const goodbye = "world";\n`,
},
},
[`test-esm-module`]: {
type: 'module',
source: {
[`index.js`]: `export const hello = 'world';`,
[`index.d.ts`]: `export declare const hello = "world";\n`,
},
},
} as const;

interface NodeModule {
[name: string]: {
type?: 'module' | 'commonjs';
source: {
[file: string]: string;
};
};
}

async function writeNodeModules(directory: string, nodeModules: NodeModule) {
const nodeModulesDirectory = path.join(directory, 'node_modules');
for (const [name, nodeModule] of Object.entries(nodeModules)) {
const nodeModuleDirectory = path.join(nodeModulesDirectory, name);
await fs.mkdir(nodeModuleDirectory, { recursive: true });
await fs.writeFile(
path.join(nodeModuleDirectory, 'package.json'),
JSON.stringify({
type: nodeModule.type ?? 'commonjs',
}),
);
for (const [file, content] of Object.entries(nodeModule.source)) {
await fs.writeFile(path.join(nodeModuleDirectory, file), content);
}
}
}

async function writeInput(directory: string, files: Record<string, string>): Promise<void> {
await fs.mkdir(directory, { recursive: true });
await Promise.all(Object.entries(files).map(([name, content]) => fs.writeFile(path.join(directory, name), content)));
}

describe('analyze', () => {
it('should bundle an ESM module that imports a second ESM module', async () => {
const id = uuid();
const inDir = path.join(os.tmpdir(), `in-dir-${id}`, 'src');
const outDir = path.join(os.tmpdir(), `out-dir-${id}`, 'build');
await writeInput(inDir, twoModules);
const result = await builder({ type: 'module', entryPoint: 'index.ts', outFile: 'index.mjs', inDir, outDir });
assert.ok(result.metafile !== undefined);
const analysis = analyze(result.metafile);
assert.ok(analysis.moduleBytes === 0);
assert.ok(analysis.sourceBytes > 0);
assert.ok(analysis.totalBytes > analysis.sourceBytes + analysis.moduleBytes);
});

it('should bundle an ESM module that imports external modules', async () => {
const id = uuid();
const moduleDir = path.join(os.tmpdir(), `in-dir-${id}`);
const inDir = path.join(moduleDir, 'src');
const outDir = path.join(os.tmpdir(), `out-dir-${id}`, 'build');
await writeInput(inDir, importExternalModule);
await writeNodeModules(moduleDir, testNodeModules);
const result = await builder({
type: 'module',
entryPoint: 'index.ts',
outFile: 'index.mjs',
workingDirectory: moduleDir,
inDir,
outDir,
});
assert.ok(result.metafile !== undefined);
const analysis = analyze(result.metafile);
assert.ok(analysis.sourceBytes > 0);
assert.ok(analysis.moduleBytes > 0);
assert.ok(analysis.totalBytes > analysis.sourceBytes + analysis.moduleBytes);
});

it('should bundle an ESM module that imports external modules, but excludes them', async () => {
const id = uuid();
const moduleDir = path.join(os.tmpdir(), `in-dir-${id}`);
const inDir = path.join(moduleDir, 'src');
const outDir = path.join(os.tmpdir(), `out-dir-${id}`, 'build');
await writeInput(inDir, importExternalModule);
await writeNodeModules(moduleDir, testNodeModules);
const result = await builder({
type: 'module',
entryPoint: 'index.ts',
outFile: 'index.mjs',
inDir,
outDir,
external: ['*'],
});
assert.ok(result.metafile !== undefined);
const analysis = analyze(result.metafile);
assert.ok(analysis.moduleBytes === 0);
assert.ok(analysis.sourceBytes > 0);
assert.ok(analysis.totalBytes > analysis.sourceBytes + analysis.moduleBytes);
});
});
76 changes: 72 additions & 4 deletions src/builder/builder.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,59 @@ const __filename = __fileURLToPath(import.meta.url);
const __dirname = __path.dirname(__filename);
const require = __createRequire(import.meta.url);`;

export type ImportKind =
| 'entry-point'
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
| 'import-rule'
| 'composes-from'
| 'url-token';

export interface Metafile {
inputs: {
[path: string]: {
bytes: number;
imports: {
path: string;
kind: ImportKind;
external?: boolean;
original?: string;
}[];
format?: 'cjs' | 'esm';
};
};
outputs: {
[path: string]: {
bytes: number;
inputs: {
[path: string]: {
bytesInOutput: number;
};
};
imports: {
path: string;
kind: ImportKind | 'file-loader';
external?: boolean;
}[];
exports: string[];
entryPoint?: string;
cssBundle?: string;
};
};
}

export interface OutputFile {
path: string;
text: string;
}

export interface BuildResult {
metafile?: Metafile | undefined;
outputFiles: OutputFile[];
}

export interface BuilderOptions {
/**
* whether to produce Typescript types, ESM or CommonJS code
Expand Down Expand Up @@ -55,6 +108,11 @@ export interface BuilderOptions {
* whether to include sourcemap
*/
sourceMap?: boolean | undefined;

/**
* working directory
*/
workingDirectory?: string | undefined;
}

/**
Expand Down Expand Up @@ -123,7 +181,8 @@ export default async function ({
external = [],
minify = false,
sourceMap,
}: BuilderOptions): Promise<string[]> {
workingDirectory = process.cwd(),
}: BuilderOptions): Promise<BuildResult> {
const messages: string[] = [];

assert.ok(
Expand Down Expand Up @@ -171,7 +230,10 @@ export default async function ({
useUnknownInCatchVariables: true,
exactOptionalPropertyTypes: true,
});
const emitResult = program.emit();
const declarationFiles: OutputFile[] = [];
const emitResult = program.emit(undefined, (fileName, data) => {
declarationFiles.push({ path: fileName, text: data });
});
const allDiagnostics = typescript.sortAndDeduplicateDiagnostics([
...typescript.getPreEmitDiagnostics(program),
...emitResult.diagnostics,
Expand All @@ -193,7 +255,9 @@ export default async function ({
}

if (type === 'types') {
return [];
return {
outputFiles: declarationFiles,
};
}

/**
Expand All @@ -203,8 +267,12 @@ export default async function ({
entryPoints: productionSourceFiles,
bundle: true,
minify,
absWorkingDir: workingDirectory,
platform: 'node',
format: type === 'module' ? 'esm' : 'cjs',
treeShaking: type === 'module',
write: false,
metafile: outFile !== undefined,
sourcesContent: false,
banner:
type === 'module' && outFile !== undefined
Expand Down Expand Up @@ -245,5 +313,5 @@ export default async function ({
throw new Error(`esbuild failed ${JSON.stringify(messages)}`);
}

return messages;
return buildResult;
}
Loading

0 comments on commit 780fadf

Please sign in to comment.