Skip to content

Commit c76dca7

Browse files
fix(output-targets): restore stats output target (#3030)
This file, when generated, provides a json file that folks can use in order to pull in what files were generated and how large those files were, as well as some of the configuration options passed to the compiler when it was invoked.
1 parent 686ef49 commit c76dca7

17 files changed

+367
-36
lines changed

src/compiler/build/build-ctx.ts

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export class BuildContext implements d.BuildCtx {
1717
componentGraph = new Map<string, string[]>();
1818
config: d.Config;
1919
data: any = {};
20+
buildStats?: d.CompilerBuildStats = undefined;
21+
esmBrowserComponentBundle: d.BundleModule[];
22+
esmComponentBundle: d.BundleModule[];
23+
es5ComponentBundle: d.BundleModule[];
24+
systemComponentBundle: d.BundleModule[];
25+
commonJsComponentBundle: d.BundleModule[];
2026
diagnostics: d.Diagnostic[] = [];
2127
dirsAdded: string[] = [];
2228
dirsDeleted: string[] = [];

src/compiler/build/build-finish.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as d from '../../declarations';
22
import { generateBuildResults } from './build-results';
3+
import { generateBuildStats, writeBuildStats } from './build-stats';
34
import { isFunction, isRemoteUrl } from '@utils';
45
import { IS_NODE_ENV } from '../sys/environment';
56
import { relative } from 'path';
@@ -12,6 +13,7 @@ export const buildFinish = async (buildCtx: d.BuildCtx) => {
1213
messages: buildCtx.buildMessages.slice(),
1314
progress: 1,
1415
};
16+
1517
buildCtx.compilerCtx.events.emit('buildLog', buildLog);
1618

1719
return results;
@@ -31,6 +33,12 @@ const buildDone = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx:
3133
// create the build results data
3234
buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx);
3335

36+
// After the build results are available on the buildCtx, call the stats and set it.
37+
// We will use this later to write the files.
38+
buildCtx.buildStats = generateBuildStats(config, buildCtx);
39+
40+
await writeBuildStats(config, buildCtx.buildStats);
41+
3442
buildCtx.debug(`${aborted ? 'aborted' : 'finished'} build, ${buildCtx.buildResults.duration}ms`);
3543

3644
// log any errors/warnings

src/compiler/build/build-stats.ts

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { byteSize, sortBy } from '@utils';
2+
import type * as d from '../../declarations';
3+
import { isOutputTargetStats } from '../output-targets/output-utils';
4+
5+
/**
6+
* Generates the Build Stats from the buildCtx. Writes any files to the file system.
7+
* @param config the project build configuration
8+
* @param buildCtx An instance of the build which holds the details about the build
9+
* @returns CompilerBuildStats or an Object including diagnostics.
10+
*/
11+
export function generateBuildStats(
12+
config: d.Config,
13+
buildCtx: d.BuildCtx
14+
): d.CompilerBuildStats | { diagnostics: d.Diagnostic[] } {
15+
const buildResults = buildCtx.buildResults;
16+
17+
let jsonData: d.CompilerBuildStats | { diagnostics: d.Diagnostic[] };
18+
19+
try {
20+
if (buildResults.hasError) {
21+
jsonData = {
22+
diagnostics: buildResults.diagnostics,
23+
};
24+
} else {
25+
const stats: d.CompilerBuildStats = {
26+
timestamp: buildResults.timestamp,
27+
compiler: {
28+
name: config.sys.name,
29+
version: config.sys.version,
30+
},
31+
app: {
32+
namespace: config.namespace,
33+
fsNamespace: config.fsNamespace,
34+
components: Object.keys(buildResults.componentGraph).length,
35+
entries: Object.keys(buildResults.componentGraph).length,
36+
bundles: buildResults.outputs.reduce((total, en) => total + en.files.length, 0),
37+
outputs: getAppOutputs(config, buildResults),
38+
},
39+
options: {
40+
minifyJs: config.minifyJs,
41+
minifyCss: config.minifyCss,
42+
hashFileNames: config.hashFileNames,
43+
hashedFileNameLength: config.hashedFileNameLength,
44+
buildEs5: config.buildEs5,
45+
},
46+
formats: {
47+
esmBrowser: sanitizeBundlesForStats(buildCtx.esmBrowserComponentBundle),
48+
esm: sanitizeBundlesForStats(buildCtx.esmComponentBundle),
49+
es5: sanitizeBundlesForStats(buildCtx.es5ComponentBundle),
50+
system: sanitizeBundlesForStats(buildCtx.systemComponentBundle),
51+
commonjs: sanitizeBundlesForStats(buildCtx.commonJsComponentBundle),
52+
},
53+
components: getComponentsFileMap(config, buildCtx),
54+
entries: buildCtx.entryModules,
55+
componentGraph: buildResults.componentGraph,
56+
sourceGraph: getSourceGraph(config, buildCtx),
57+
rollupResults: buildCtx.rollupResults,
58+
collections: getCollections(config, buildCtx),
59+
};
60+
61+
jsonData = stats;
62+
}
63+
} catch (e) {
64+
jsonData = {
65+
diagnostics: [e.message],
66+
};
67+
}
68+
69+
return jsonData;
70+
}
71+
72+
/**
73+
* Writes the files from the stats config to the file system
74+
* @param config the project build configuration
75+
* @param buildCtx An instance of the build which holds the details about the build
76+
* @returns
77+
*/
78+
export async function writeBuildStats(config: d.Config, data: d.CompilerBuildStats | { diagnostics: d.Diagnostic[] }) {
79+
const statsTargets = config.outputTargets.filter(isOutputTargetStats);
80+
81+
await Promise.all(
82+
statsTargets.map(async (outputTarget) => {
83+
const result = await config.sys.writeFile(outputTarget.file, JSON.stringify(data, null, 2));
84+
85+
if (result.error) {
86+
config.logger.warn([`Stats failed to write file to ${outputTarget.file}`]);
87+
}
88+
})
89+
);
90+
}
91+
92+
function sanitizeBundlesForStats(bundleArray: ReadonlyArray<d.BundleModule>): ReadonlyArray<d.CompilerBuildStatBundle> {
93+
if (!bundleArray) {
94+
return [];
95+
}
96+
97+
return bundleArray.map((bundle) => {
98+
return {
99+
key: bundle.entryKey,
100+
components: bundle.cmps.map((c) => c.tagName),
101+
bundleId: bundle.output.bundleId,
102+
fileName: bundle.output.fileName,
103+
imports: bundle.rollupResult.imports,
104+
// code: bundle.rollupResult.code, // (use this to debug)
105+
// Currently, this number is inaccurate vs what seems to be on disk.
106+
originalByteSize: byteSize(bundle.rollupResult.code),
107+
};
108+
});
109+
}
110+
111+
function getSourceGraph(config: d.Config, buildCtx: d.BuildCtx) {
112+
let sourceGraph: d.BuildSourceGraph = {};
113+
114+
sortBy(buildCtx.moduleFiles, (m) => m.sourceFilePath).forEach((moduleFile) => {
115+
const key = relativePath(config, moduleFile.sourceFilePath);
116+
sourceGraph[key] = moduleFile.localImports.map((localImport) => relativePath(config, localImport)).sort();
117+
});
118+
119+
return sourceGraph;
120+
}
121+
122+
function getAppOutputs(config: d.Config, buildResults: d.CompilerBuildResults) {
123+
return buildResults.outputs.map((output) => {
124+
return {
125+
name: output.type,
126+
files: output.files.length,
127+
generatedFiles: output.files.map((file) => relativePath(config, file)),
128+
};
129+
});
130+
}
131+
132+
function getComponentsFileMap(config: d.Config, buildCtx: d.BuildCtx) {
133+
return buildCtx.components.map((component) => {
134+
return {
135+
tag: component.tagName,
136+
path: relativePath(config, component.jsFilePath),
137+
source: relativePath(config, component.sourceFilePath),
138+
elementRef: component.elementRef,
139+
componentClassName: component.componentClassName,
140+
assetsDirs: component.assetsDirs,
141+
dependencies: component.dependencies,
142+
dependents: component.dependents,
143+
directDependencies: component.directDependencies,
144+
directDependents: component.directDependents,
145+
docs: component.docs,
146+
encapsulation: component.encapsulation,
147+
excludeFromCollection: component.excludeFromCollection,
148+
events: component.events,
149+
internal: component.internal,
150+
legacyConnect: component.legacyConnect,
151+
legacyContext: component.legacyContext,
152+
listeners: component.listeners,
153+
methods: component.methods,
154+
potentialCmpRefs: component.potentialCmpRefs,
155+
properties: component.properties,
156+
shadowDelegatesFocus: component.shadowDelegatesFocus,
157+
states: component.states,
158+
};
159+
});
160+
}
161+
162+
function getCollections(config: d.Config, buildCtx: d.BuildCtx) {
163+
return buildCtx.collections
164+
.map((c) => {
165+
return {
166+
name: c.collectionName,
167+
source: relativePath(config, c.moduleDir),
168+
tags: c.moduleFiles.map((m) => m.cmps.map((cmp: d.ComponentCompilerMeta) => cmp.tagName)).sort(),
169+
};
170+
})
171+
.sort((a, b) => {
172+
if (a.name < b.name) return -1;
173+
if (a.name > b.name) return 1;
174+
return 0;
175+
});
176+
}
177+
178+
function relativePath(config: d.Config, file: string) {
179+
return config.sys.normalizePath(config.sys.platformPath.relative(config.rootDir, file));
180+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type * as d from '@stencil/core/declarations';
2+
import { mockConfig, mockCompilerCtx, mockBuildCtx } from '@stencil/core/testing';
3+
import { generateBuildResults } from '../build-results';
4+
import { generateBuildStats } from '../build-stats';
5+
import path from 'path';
6+
7+
describe('generateBuildStats', () => {
8+
const root = path.resolve('/');
9+
const config = mockConfig();
10+
let compilerCtx: d.CompilerCtx;
11+
let buildCtx: d.BuildCtx;
12+
13+
beforeEach(() => {
14+
compilerCtx = mockCompilerCtx(config);
15+
buildCtx = mockBuildCtx(config, compilerCtx);
16+
});
17+
18+
it('should return a structured json object', async () => {
19+
buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx);
20+
21+
const result: d.CompilerBuildStats = generateBuildStats(config, buildCtx);
22+
23+
if (result.hasOwnProperty('timestamp')) {
24+
delete result.timestamp;
25+
}
26+
27+
if (result.hasOwnProperty('compiler') && result.compiler.hasOwnProperty('version')) {
28+
delete result.compiler.version;
29+
}
30+
31+
expect(result).toStrictEqual({
32+
app: { bundles: 0, components: 0, entries: 0, fsNamespace: undefined, namespace: 'Testing', outputs: [] },
33+
collections: [],
34+
compiler: { name: 'in-memory' },
35+
componentGraph: {},
36+
components: [],
37+
entries: [],
38+
formats: { commonjs: [], es5: [], esm: [], esmBrowser: [], system: [] },
39+
options: {
40+
buildEs5: false,
41+
hashFileNames: false,
42+
hashedFileNameLength: undefined,
43+
minifyCss: false,
44+
minifyJs: false,
45+
},
46+
rollupResults: undefined,
47+
sourceGraph: {},
48+
});
49+
});
50+
51+
it('should return diagnostics if an error is hit', async () => {
52+
buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx);
53+
54+
buildCtx.buildResults.hasError = true;
55+
buildCtx.buildResults.diagnostics = ['Something bad happened'];
56+
57+
const result = generateBuildStats(config, buildCtx);
58+
59+
expect(result).toStrictEqual({
60+
diagnostics: ['Something bad happened'],
61+
});
62+
});
63+
});

src/compiler/entries/component-graph.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as d from '../../declarations';
22
import { getScopeId } from '../style/scope-css';
33

4-
export const generateModuleGraph = (cmps: d.ComponentCompilerMeta[], bundleModules: d.BundleModule[]) => {
4+
export const generateModuleGraph = (cmps: d.ComponentCompilerMeta[], bundleModules: ReadonlyArray<d.BundleModule>) => {
55
const cmpMap = new Map<string, string[]>();
66
cmps.forEach((cmp) => {
77
const bundle = bundleModules.find((b) => b.cmps.includes(cmp));

src/compiler/output-targets/dist-lazy/generate-cjs.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const generateCjs = async (
1111
buildCtx: d.BuildCtx,
1212
rollupBuild: RollupBuild,
1313
outputTargets: d.OutputTargetDistLazy[]
14-
) => {
14+
): Promise<d.UpdatedLazyBuildCtx> => {
1515
const cjsOutputs = outputTargets.filter((o) => !!o.cjsDir);
1616

1717
if (cjsOutputs.length > 0) {
@@ -25,7 +25,8 @@ export const generateCjs = async (
2525
const results = await generateRollupOutput(rollupBuild, esmOpts, config, buildCtx.entryModules);
2626
if (results != null) {
2727
const destinations = cjsOutputs.map((o) => o.cjsDir);
28-
await generateLazyModules(
28+
29+
buildCtx.commonJsComponentBundle = await generateLazyModules(
2930
config,
3031
compilerCtx,
3132
buildCtx,
@@ -36,16 +37,19 @@ export const generateCjs = async (
3637
false,
3738
'.cjs'
3839
);
40+
3941
await generateShortcuts(compilerCtx, results, cjsOutputs);
4042
}
4143
}
44+
45+
return { name: 'cjs', buildCtx };
4246
};
4347

4448
const generateShortcuts = (
4549
compilerCtx: d.CompilerCtx,
4650
rollupResult: d.RollupResult[],
4751
outputTargets: d.OutputTargetDistLazy[]
48-
) => {
52+
): Promise<void[]> => {
4953
const indexFilename = rollupResult.find((r) => r.type === 'chunk' && r.isIndex).fileName;
5054
return Promise.all(
5155
outputTargets.map(async (o) => {

src/compiler/output-targets/dist-lazy/generate-esm-browser.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const generateEsmBrowser = async (
1010
buildCtx: d.BuildCtx,
1111
rollupBuild: RollupBuild,
1212
outputTargets: d.OutputTargetDistLazy[]
13-
) => {
13+
): Promise<d.UpdatedLazyBuildCtx> => {
1414
const esmOutputs = outputTargets.filter((o) => !!o.esmDir && !!o.isBrowserBuild);
1515
if (esmOutputs.length) {
1616
const outputTargetType = esmOutputs[0].type;
@@ -29,7 +29,7 @@ export const generateEsmBrowser = async (
2929

3030
if (output != null) {
3131
const es2017destinations = esmOutputs.map((o) => o.esmDir);
32-
const componentBundle = await generateLazyModules(
32+
buildCtx.esmBrowserComponentBundle = await generateLazyModules(
3333
config,
3434
compilerCtx,
3535
buildCtx,
@@ -40,8 +40,8 @@ export const generateEsmBrowser = async (
4040
true,
4141
''
4242
);
43-
return componentBundle;
4443
}
4544
}
46-
return undefined;
45+
46+
return { name: 'esm-browser', buildCtx };
4747
};

0 commit comments

Comments
 (0)