183 lines
5.3 KiB
TypeScript
183 lines
5.3 KiB
TypeScript
import {
|
|
BuildOutput,
|
|
BuildStepOutput,
|
|
EntrypointGroup,
|
|
OutputAsset,
|
|
OutputFile,
|
|
} from '~/types';
|
|
import { every } from '~/core/utils/arrays';
|
|
import { normalizePath } from '~/core/utils/paths';
|
|
|
|
/**
|
|
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
|
|
*
|
|
* - Do nothing
|
|
* - CSS or JS file associated with an HTML page is changed - this is handled automatically by the
|
|
* dev server
|
|
* - Change isn't used by any of the entrypoints
|
|
* - Reload Content script
|
|
* - CSS or JS file associated with a content script
|
|
* - Background script will be told to reload the content script
|
|
* - Reload HTML file
|
|
* - HTML file itself is saved - HMR doesn't handle this because the HTML pages are pre-rendered
|
|
* - Chrome is OK reloading the page when the HTML file is changed without reloading the whole
|
|
* extension. Not sure about firefox, this might need to change to an extension reload
|
|
* - Reload extension
|
|
* - Background script is changed
|
|
* - Manifest is different
|
|
* - Restart browser
|
|
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
|
*/
|
|
export function detectDevChanges(
|
|
changedFiles: [event: string, path: string][],
|
|
currentOutput: BuildOutput | undefined,
|
|
): DevModeChange {
|
|
if (currentOutput == null) return { type: 'no-change' };
|
|
|
|
const changedSteps = new Set(
|
|
changedFiles.flatMap((changedFile) =>
|
|
findEffectedSteps(changedFile, currentOutput),
|
|
),
|
|
);
|
|
if (changedSteps.size === 0) return { type: 'no-change' };
|
|
|
|
const unchangedOutput: BuildOutput = {
|
|
manifest: currentOutput.manifest,
|
|
steps: [],
|
|
publicAssets: [],
|
|
};
|
|
const changedOutput: BuildOutput = {
|
|
manifest: currentOutput.manifest,
|
|
steps: [],
|
|
publicAssets: [],
|
|
};
|
|
|
|
for (const step of currentOutput.steps) {
|
|
if (changedSteps.has(step)) {
|
|
changedOutput.steps.push(step);
|
|
} else {
|
|
unchangedOutput.steps.push(step);
|
|
}
|
|
}
|
|
for (const asset of currentOutput.publicAssets) {
|
|
if (changedSteps.has(asset)) {
|
|
changedOutput.publicAssets.push(asset);
|
|
} else {
|
|
unchangedOutput.publicAssets.push(asset);
|
|
}
|
|
}
|
|
|
|
const isOnlyHtmlChanges =
|
|
changedFiles.length > 0 &&
|
|
every(changedFiles, ([_, file]) => file.endsWith('.html'));
|
|
if (isOnlyHtmlChanges) {
|
|
return {
|
|
type: 'html-reload',
|
|
cachedOutput: unchangedOutput,
|
|
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
|
};
|
|
}
|
|
|
|
const isOnlyContentScripts =
|
|
changedOutput.steps.length > 0 &&
|
|
every(
|
|
changedOutput.steps.flatMap((step) => step.entrypoints),
|
|
(entry) => entry.type === 'content-script',
|
|
);
|
|
if (isOnlyContentScripts) {
|
|
return {
|
|
type: 'content-script-reload',
|
|
cachedOutput: unchangedOutput,
|
|
changedSteps: changedOutput.steps,
|
|
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: 'extension-reload',
|
|
cachedOutput: unchangedOutput,
|
|
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* For a single change, return all the step of the build output that were effected by it.
|
|
*/
|
|
function findEffectedSteps(
|
|
changedFile: [event: string, path: string],
|
|
currentOutput: BuildOutput,
|
|
): DetectedChange[] {
|
|
const changes: DetectedChange[] = [];
|
|
const changedPath = normalizePath(changedFile[1]);
|
|
|
|
const isChunkEffected = (chunk: OutputFile): boolean =>
|
|
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
|
// fileName is normalized, relative bundle path
|
|
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
|
|
// If it's a chunk that depends on the changed file, it is effected
|
|
// moduleIds are absolute, normalized paths
|
|
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
|
|
|
|
for (const step of currentOutput.steps) {
|
|
const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
|
|
if (effectedChunk) changes.push(step);
|
|
}
|
|
|
|
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
|
|
isChunkEffected(chunk),
|
|
);
|
|
if (effectedAsset) changes.push(effectedAsset);
|
|
|
|
return changes;
|
|
}
|
|
|
|
/**
|
|
* Contains information about what files changed, what needs rebuilt, and the type of reload that is
|
|
* required.
|
|
*/
|
|
export type DevModeChange =
|
|
| NoChange
|
|
| HtmlReload
|
|
| ExtensionReload
|
|
| ContentScriptReload;
|
|
// | BrowserRestart
|
|
|
|
interface NoChange {
|
|
type: 'no-change';
|
|
}
|
|
|
|
interface RebuildChange {
|
|
/**
|
|
* The list of entrypoints that need rebuilt.
|
|
*/
|
|
rebuildGroups: EntrypointGroup[];
|
|
/**
|
|
* The previous output stripped of any files are going to change.
|
|
*/
|
|
cachedOutput: BuildOutput;
|
|
}
|
|
|
|
interface HtmlReload extends RebuildChange {
|
|
type: 'html-reload';
|
|
}
|
|
|
|
interface ExtensionReload extends RebuildChange {
|
|
type: 'extension-reload';
|
|
}
|
|
|
|
// interface BrowserRestart extends RebuildChange {
|
|
// type: 'browser-restart';
|
|
// }
|
|
|
|
interface ContentScriptReload extends RebuildChange {
|
|
type: 'content-script-reload';
|
|
changedSteps: BuildStepOutput[];
|
|
}
|
|
|
|
/**
|
|
* When figuring out what needs reloaded, this stores the step that was changed, or the public
|
|
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
|
|
* intermediate type.
|
|
*/
|
|
type DetectedChange = BuildStepOutput | OutputAsset;
|