Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ export enum InjectPosition {
MIDDLE,
AFTER,
}

export type ToInjectItem = {
type: 'file' | 'code';
value: InjectedValue;
position?: InjectPosition;
fallback?: ToInjectItem;
};
} & (
| { position?: InjectPosition.BEFORE | InjectPosition.AFTER; injectIntoAllChunks?: boolean }
| { position: InjectPosition.MIDDLE }
);

export type TimeLogger = {
timer: Timer;
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/build-report/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ describe('Build Report Plugin', () => {
'escape-string-regexp/index.js',
'hard_project/main1.js',
'hard_project/main2.js',
'hard_project/src/dynamicChunk.js',
'hard_project/src/srcFile0.js',
'hard_project/src/srcFile1.js',
'hard_project/workspaces/app/workspaceFile0.js',
Expand Down Expand Up @@ -475,6 +476,7 @@ describe('Build Report Plugin', () => {
filename: 'hard_project/main1.js',
dependencies: [
'chalk/index.js',
'hard_project/src/dynamicChunk.js',
'hard_project/src/srcFile0.js',
'hard_project/workspaces/app/workspaceFile1.js',
],
Expand Down Expand Up @@ -643,7 +645,7 @@ describe('Build Report Plugin', () => {
});

const entriesList = [
{ entryName: 'app1', dependenciesLength: 9, mainFilesLength: 4 },
{ entryName: 'app1', dependenciesLength: 9, mainFilesLength: 5 },
{ entryName: 'app2', dependenciesLength: 0, mainFilesLength: 5 },
];

Expand Down
27 changes: 25 additions & 2 deletions packages/plugins/build-report/src/xpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,35 @@ export const getXpackPlugin =

// Build entries
const timeEntries = log.time('building entries');

// Helper to recursively get all chunks from a chunk group, including async chunks.
const getAllChunksFromGroup = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chunkGroup: any,
visited: Set<unknown> = new Set(),
): Chunk[] => {
if (visited.has(chunkGroup)) {
return [];
}
visited.add(chunkGroup);

const allChunks: Chunk[] = [...chunkGroup.chunks];

// Recursively get chunks from child chunk groups (async chunks).
for (const childGroup of chunkGroup.childrenIterable || []) {
allChunks.push(...getAllChunksFromGroup(childGroup, visited));
}

return allChunks;
};

for (const [name, entrypoint] of result.entrypoints) {
const entryOutputs: Map<string, Output> = new Map();
const entryInputs: Map<string, Input> = new Map();
let size = 0;
const entryFiles = entrypoint.chunks.flatMap(getChunkFiles);
// Get all chunks including async chunks from child chunk groups.
const allChunks = getAllChunksFromGroup(entrypoint);
const entryFiles = allChunks.flatMap(getChunkFiles);

// FIXME This is not a 100% reliable way to get the entry filename.
const entryFilename = entrypoint.chunks
Expand Down Expand Up @@ -482,7 +506,6 @@ export const getXpackPlugin =
outputs: Array.from(entryOutputs.values()),
type: entryFilename ? getType(entryFilename) : 'unknown',
};

entries.push(file);
}
timeEntries.end();
Expand Down
127 changes: 74 additions & 53 deletions packages/plugins/injection/src/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export const getEsbuildPlugin = (
namespace: PLUGIN_NAME,
},
async () => {
const content = getContentToInject(contentsToInject[InjectPosition.MIDDLE]);
const content = getContentToInject(contentsToInject, {
position: InjectPosition.MIDDLE,
});

return {
// We can't use an empty string otherwise esbuild will crash.
Expand All @@ -96,63 +98,82 @@ export const getEsbuildPlugin = (
return;
}

const banner = getContentToInject(contentsToInject[InjectPosition.BEFORE]);
const footer = getContentToInject(contentsToInject[InjectPosition.AFTER]);

if (!banner && !footer) {
const bannerForEntries = getContentToInject(contentsToInject, {
position: InjectPosition.BEFORE,
});
const footerForEntries = getContentToInject(contentsToInject, {
position: InjectPosition.AFTER,
});
const bannerForAllChunks = getContentToInject(contentsToInject, {
position: InjectPosition.BEFORE,
onAllChunks: true,
});
const footerForAllChunks = getContentToInject(contentsToInject, {
position: InjectPosition.AFTER,
onAllChunks: true,
});

if (
!bannerForEntries &&
!footerForEntries &&
!bannerForAllChunks &&
!footerForAllChunks
) {
// Nothing to inject.
return;
}

// Rewrite outputs with the injected content.
// Only keep the entry files.
const outputs: string[] = Object.entries(result.metafile.outputs)
.map(([p, o]) => {
const entryPoint = o.entryPoint;
if (!entryPoint) {
return;
}

const entry = entries.find((e) => e.resolved.endsWith(entryPoint));
if (!entry) {
return;
}

return getAbsolutePath(context.buildRoot, p);
})
.filter(Boolean) as string[];

// Write the content.
const proms = outputs
.filter((output) => {
const { base, ext } = path.parse(output);
const isOutputSupported = isFileSupported(ext);
if (!isOutputSupported) {
warnUnsupportedFile(log, ext, base);
}
return isOutputSupported;
})
.map(async (output) => {
try {
const source = await fsp.readFile(output, 'utf-8');
const data = await esbuild.transform(source, {
loader: 'default',
banner,
footer,
});

// FIXME: Handle sourcemaps.
await fsp.writeFile(output, data.code);
} catch (e) {
if (isNodeSystemError(e) && e.code === 'ENOENT') {
// When we are using sub-builds, the entry file of sub-builds may not exist
// Hence we should skip the file injection in this case.
log.warn(`Could not inject content in ${output}: ${e}`);
} else {
throw e;
const proms: Promise<void>[] = [];

// Process all output files
for (const [p, o] of Object.entries(result.metafile.outputs)) {
// Determine if this is an entry point
const isEntry =
o.entryPoint && entries.some((e) => e.resolved.endsWith(o.entryPoint!));

// Get the appropriate banner and footer
const banner = isEntry ? bannerForEntries : bannerForAllChunks;
const footer = isEntry ? footerForEntries : footerForAllChunks;

// Skip if nothing to inject for this chunk type
if (!banner && !footer) {
continue;
}

const absolutePath = getAbsolutePath(context.buildRoot, p);
const { base, ext } = path.parse(absolutePath);

// Check if file type is supported
if (!isFileSupported(ext)) {
warnUnsupportedFile(log, ext, base);
continue;
}

// Inject content
proms.push(
(async () => {
try {
const source = await fsp.readFile(absolutePath, 'utf-8');
const data = await esbuild.transform(source, {
loader: 'default',
banner,
footer,
});

// FIXME: Handle sourcemaps.
await fsp.writeFile(absolutePath, data.code);
} catch (e) {
if (isNodeSystemError(e) && e.code === 'ENOENT') {
// When we are using sub-builds, the entry file of sub-builds may not exist
// Hence we should skip the file injection in this case.
log.warn(`Could not inject content in ${absolutePath}: ${e}`);
} else {
throw e;
}
}
}
});
})(),
);
}

await Promise.all(proms);
});
Expand Down
24 changes: 21 additions & 3 deletions packages/plugins/injection/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,29 @@ describe('Injection Plugin Helpers', () => {

const results = await processInjections(items, mockLogger);
expect(Array.from(results.entries())).toEqual([
['code', { position: InjectPosition.BEFORE, value: codeContent }],
['existingFile', { position: InjectPosition.BEFORE, value: localFileContent }],
[
'code',
{
position: InjectPosition.BEFORE,
value: codeContent,
injectIntoAllChunks: false,
},
],
[
'existingFile',
{
position: InjectPosition.BEFORE,
value: localFileContent,
injectIntoAllChunks: false,
},
],
[
'existingDistantFile',
{ position: InjectPosition.BEFORE, value: distantFileContent },
{
position: InjectPosition.BEFORE,
value: distantFileContent,
injectIntoAllChunks: false,
},
],
]);

Expand Down
43 changes: 32 additions & 11 deletions packages/plugins/injection/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
DISTANT_FILE_RX,
SUPPORTED_EXTENSIONS,
} from './constants';
import type { ContentsToInject } from './types';
import type { ContentsToInject, ContentToInject } from './types';

const yellow = chalk.bold.yellow;

Expand Down Expand Up @@ -101,29 +101,50 @@ export const processInjections = async (
toInject: Map<string, ToInjectItem>,
log: Logger,
cwd: string = process.cwd(),
): Promise<Map<string, { position: InjectPosition; value: string }>> => {
const toReturn: Map<string, { position: InjectPosition; value: string }> = new Map();
): Promise<
Map<string, { injectIntoAllChunks: boolean; position: InjectPosition; value: string }>
> => {
const toReturn = new Map();

// Processing sequentially all the items.
for (const [id, item] of toInject.entries()) {
// eslint-disable-next-line no-await-in-loop
const value = await processItem(item, log, cwd);
if (value) {
toReturn.set(id, { value, position: item.position || InjectPosition.BEFORE });
const position = item.position || InjectPosition.BEFORE;
toReturn.set(id, {
value,
injectIntoAllChunks:
'injectIntoAllChunks' in item ? item.injectIntoAllChunks : false,
position,
});
}
}

return toReturn;
};

export const getContentToInject = (contentToInject: Map<string, string>) => {
if (contentToInject.size === 0) {
export const getContentToInject = (
contentToInject: ContentToInject[],
options: {
position: InjectPosition;
onAllChunks?: boolean;
},
) => {
const filtered = contentToInject.filter((content) => {
return (
content.position === options.position &&
(!options.onAllChunks || content.injectIntoAllChunks)
);
});

if (filtered.length === 0) {
return '';
}

const stringToInject = Array.from(contentToInject.values())
const stringToInject = filtered
// Wrapping it in order to avoid variable name collisions.
.map((content) => `(() => {${content}})();`)
.map((content) => `(() => {${content.value}})();`)
.join('\n\n');
return `${BEFORE_INJECTION}\n${stringToInject}\n${AFTER_INJECTION}`;
};
Expand All @@ -136,9 +157,9 @@ export const addInjections = async (
cwd: string = process.cwd(),
) => {
const results = await processInjections(toInject, log, cwd);
// Redistribute the content to inject in the right place.
for (const [id, value] of results.entries()) {
contentsToInject[value.position].set(id, value.value);
// Add processed content to the array
for (const value of results.values()) {
contentsToInject.push(value);
}
};

Expand Down
Loading