Compare commits

..

7 Commits

Author SHA1 Message Date
rxliuli ebe1f9da7e docs: Remove dangling "Remote Code" sidebar link (#2532)
✨ pkg.pr.new / Publish Test Packages (push) Waiting to run
📝 Publish Docs / Publish Docs (push) Waiting to run
🛡️ Validate / Checks (push) Waiting to run
🛡️ Validate / Builds (push) Waiting to run
🛡️ Validate / Build Demo (push) Waiting to run
🛡️ Validate / Tests (Windows) (push) Waiting to run
🛡️ Validate / Tests (Linux) (push) Waiting to run
🛡️ Validate / Template (react) (push) Waiting to run
🛡️ Validate / Template (solid) (push) Waiting to run
🛡️ Validate / Template (svelte) (push) Waiting to run
🛡️ Validate / Template (vanilla) (push) Waiting to run
🛡️ Validate / Template (vue) (push) Waiting to run
2026-07-30 16:38:23 +05:30
Aaron acc4cdec07 chore(deps): Replace async-mutex with superlock (#2529) 2026-07-29 09:12:36 -05:00
Aaron 8570ce29b0 chore(deps): Replace prompts with @topcli/prompts (#2528) 2026-07-28 17:38:45 -05:00
Aaron f7cce3e269 chore(deps): Replace open with tiny-open (#2527) 2026-07-28 17:10:22 -05:00
Aaron 315912ebf3 chore(deps): Drop import-meta-resolve (#2526) 2026-07-28 17:10:12 -05:00
Aaron 574c3bd90a docs: Update bundle size for 0.21 2026-07-28 13:57:00 -05:00
Aaron 8dc8fa6f51 chore: Add support for publish-browser-extension v6 2026-07-28 13:57:00 -05:00
13 changed files with 84 additions and 97 deletions
Generated
+11 -12
View File
@@ -233,8 +233,8 @@
"version": "1.2.8",
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"async-mutex": "^0.5.0",
"dequal": "^2.0.3",
"superlock": "^1.3.2",
},
"devDependencies": {
"@aklinker1/buildc": "catalog:",
@@ -294,12 +294,12 @@
"dependencies": {
"@1natsu/wait-element": "^4.1.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@topcli/prompts": "^4.0.0",
"@webext-core/fake-browser": "^2.0.1",
"@webext-core/isolated-element": "^1.1.3 || ^2 || ^3",
"@webext-core/match-patterns": "^2.0.0",
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^3.3.4",
"cac": "^6.7.14 || ^7.0.0",
"chokidar": "^5.0.0",
@@ -311,7 +311,6 @@
"get-port-please": "^3.2.0",
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
"hookable": "^6.1.0",
"import-meta-resolve": "^4.2.0",
"is-wsl": "^3.1.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
@@ -322,11 +321,11 @@
"normalize-path": "^3.0.0",
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"picomatch": "^4.0.3",
"prompts": "^2.4.2",
"publish-browser-extension": "^5.1.0",
"publish-browser-extension": "^5.1.0 || ^6.0.0",
"scule": "^1.3.0",
"superlock": "^1.3.2",
"tiny-open": "^1.3.0",
"tinyglobby": "^0.2.16",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0",
},
@@ -1063,6 +1062,8 @@
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
"@topcli/prompts": ["@topcli/prompts@4.0.0", "", {}, "sha512-kkKYPb4k/6kRdnESt77B5vJrrYnYHIYczwv0P1c+tfN/IWEx3KZeKEHoUq7sImIyBEXWdA2uakIroD9pmRJziQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -1319,8 +1320,6 @@
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="],
@@ -2111,8 +2110,6 @@
"promise-toolbox": ["promise-toolbox@0.21.0", "", { "dependencies": { "make-error": "^1.3.2" } }, "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -2245,8 +2242,6 @@
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
@@ -2301,6 +2296,8 @@
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"superlock": ["superlock@1.3.2", "", {}, "sha512-bLQRyrv/rT9ACCjPWyJJaE2+EMciIjnbiGWf2PHT1UoQYkoMfRJgTqLJedy16g8Q8xK5lZFRAl1devVxp4opjA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"svelte": ["svelte@5.50.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WCxzm3BBf+Ase6RwiDPR4G36cM4Kb0NuhmLK6x44I+D6reaxizDDg8kBkk4jT/19+Rgmc44eZkOvMO6daoSFIw=="],
@@ -2313,6 +2310,8 @@
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"tiny-open": ["tiny-open@1.3.0", "", {}, "sha512-GUFS8yjJZq0oWqCKCJVHcBgMpmi2WEGXY1le3E5ncR0DsgTII5uUyxtfk8/vGyDDGBE42UYHR6Ocvlk8mkXSRg=="],
"tinybench": ["tinybench@6.0.1", "", {}, "sha512-cMdWsxmysdg8mNWf1pujiWl3TW0cU6m8QuNw55QlnP3I6N96Grb0wnu5N0syHIu3LbiVZCNqlfWzWDq84HZphA=="],
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
+2
View File
@@ -106,8 +106,10 @@ words:
- solidjs
- styl
- subdependency
- superlock
- tabstrip
- teleporting
- topcli
- treemap
- triaging
- umami
-1
View File
@@ -243,7 +243,6 @@ export default defineConfig({
menuItem('WXT Modules', 'wxt-modules.md'),
menuItem('Frontend Frameworks', 'frontend-frameworks.md'),
menuItem('ES Modules', 'es-modules.md'),
menuItem('Remote Code', 'remote-code.md'),
menuItem('Unit Testing', 'unit-testing.md'),
menuItem('E2E Testing', 'e2e-testing.md'),
menuItem('Publishing', 'publishing.md'),
+1 -1
View File
@@ -45,7 +45,7 @@ Read through all the changes once before updating your code.
### Install Footprint
When installing `wxt` v0.20, it downloaded 98 MB and 366 packages to your `node_modules`. v0.21 cuts this down to 46MB / 313 packages - that's 46.9% the previous size! This is a good improvement, but frankly, not good enough. Why?
When installing `wxt` v0.20, it downloaded 98 MB and 366 packages to your `node_modules`. v0.21 cuts this down to 22MB / 156 packages - that's 22.4% the previous size! This is a good improvement, but frankly, not good enough. Why?
- `wxt init` is slow for new developers trying it out
- Less exposure to supply chain attacks
+5 -1
View File
@@ -38,6 +38,10 @@
# 0.19.0
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
# 0.21.0
# The `url:` import feature was removed, along with its docs page.
/guide/essentials/remote-code.html /guide/resources/upgrading.html#url-imports-removed
# https://github.com/wxt-dev/wxt/issues/704
# Generated via `pnpm docs:build && find docs/.vitepress/dist -type f -name "*.html"`
@@ -59,7 +63,7 @@
/guide/go-further/handling-updates.html /guide/essentials/testing-updates.html
/guide/go-further/custom-events.html /guide/essentials/content-scripts.html#dealing-with-spas
/guide/go-further/debugging.html /TODO
/guide/go-further/remote-code.html /guide/essentials/remote-code.html
/guide/go-further/remote-code.html /guide/resources/upgrading.html#url-imports-removed
/guide/go-further/vite.html /guide/essentials/config/vite.html
/guide/go-further/testing.html /guide/essentials/unit-testing.html
/guide/go-further/how-wxt-works.html /guide/resources/how-wxt-works.html
+2 -2
View File
@@ -33,8 +33,8 @@
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"async-mutex": "^0.5.0",
"dequal": "^2.0.3"
"dequal": "^2.0.3",
"superlock": "^1.3.2"
},
"devDependencies": {
"@aklinker1/buildc": "catalog:",
+3 -3
View File
@@ -7,7 +7,7 @@
* @module @wxt-dev/storage
*/
import { browser, type Browser } from '@wxt-dev/browser';
import { Mutex } from 'async-mutex';
import { withLock } from 'superlock';
import { dequal } from 'dequal/lite';
export const storage = createStorage();
@@ -487,12 +487,12 @@ function createStorage(): WxtStorage {
);
});
const initMutex = new Mutex();
const initLock = withLock();
const getFallback = () => opts?.fallback ?? opts?.defaultValue ?? null;
const getOrInitValue = () =>
initMutex.runExclusive(async () => {
initLock(async () => {
const value = await driver.getItem<any>(driverKey);
// Don't init value if it already exists or the init function isn't provided
if (value != null || opts?.init == null) return value;
+2 -2
View File
@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestProject } from '../utils';
import { resetBundleIncrement } from '../../src/core/builders/vite/plugins';
import open from 'open';
import open from 'tiny-open';
vi.mock('open');
vi.mock('tiny-open');
const openMock = vi.mocked(open);
vi.mock('ci-info', () => ({
+4 -5
View File
@@ -17,12 +17,12 @@
"dependencies": {
"@1natsu/wait-element": "^4.1.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@topcli/prompts": "^4.0.0",
"@webext-core/fake-browser": "^2.0.1",
"@webext-core/isolated-element": "^1.1.3 || ^2 || ^3",
"@webext-core/match-patterns": "^2.0.0",
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^3.3.4",
"cac": "^6.7.14 || ^7.0.0",
"chokidar": "^5.0.0",
@@ -34,7 +34,6 @@
"get-port-please": "^3.2.0",
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
"hookable": "^6.1.0",
"import-meta-resolve": "^4.2.0",
"is-wsl": "^3.1.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
@@ -45,11 +44,11 @@
"normalize-path": "^3.0.0",
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"picomatch": "^4.0.3",
"prompts": "^2.4.2",
"publish-browser-extension": "^5.1.0",
"publish-browser-extension": "^5.1.0 || ^6.0.0",
"scule": "^1.3.0",
"superlock": "^1.3.2",
"tiny-open": "^1.3.0",
"tinyglobby": "^0.2.16",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
+48 -58
View File
@@ -1,4 +1,4 @@
import prompts from 'prompts';
import { question, select } from '@topcli/prompts';
import { consola } from 'consola';
import { downloadTemplate } from 'giget';
import { readdir, rename } from 'node:fs/promises';
@@ -8,88 +8,78 @@ import { styleText } from 'node:util';
import { TextStyle } from '../utils/text-style';
export async function initialize(options: {
directory: string;
template: string;
packageManager: string;
directory?: string;
template?: string;
packageManager?: string;
}) {
consola.info('Initializing new project');
const templates = await listTemplates();
const defaultTemplate = templates.find(
(template) => template.name === options.template?.toLowerCase().trim(),
);
const inputTemplateName = options.template
? templates.find(
(template) => template.name === options.template!.toLowerCase().trim(),
)?.name
: undefined;
const input = await prompts(
[
{
name: 'directory',
type: () => (options.directory == null ? 'text' : undefined),
message: 'Project Directory',
initial: options.directory,
},
{
name: 'template',
type: () => (defaultTemplate == null ? 'select' : undefined),
message: 'Choose a template',
choices: templates.map((template) => ({
title: TEMPLATE_COLORS[template.name]
? styleText(TEMPLATE_COLORS[template.name], template.name)
: template.name,
value: template,
})),
},
{
name: 'packageManager',
type: () => (options.packageManager == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: styleText('magenta', 'bun'), value: 'bun' },
{ title: styleText('red', 'npm'), value: 'npm' },
{ title: styleText('yellow', 'pnpm'), value: 'pnpm' },
{ title: styleText('cyan', 'yarn'), value: 'yarn' },
],
},
],
{
onCancel: () => process.exit(1),
},
);
input.directory ||= options.directory || '.';
input.template ??= defaultTemplate;
input.packageManager ??= options.packageManager;
const directory =
options.directory ??
(await question('Project Directory', { defaultValue: '.' }));
if (!directory) throw Error('Directory is required');
const isExists = await pathExists(input.directory);
const templateName =
inputTemplateName ??
(await select('Choose a template', {
choices: templates.map((template) => ({
label: TEMPLATE_COLORS[template.name]
? styleText(TEMPLATE_COLORS[template.name], template.name)
: template.name,
value: template.name,
})),
}));
const template = templates.find((t) => t.name === templateName);
if (!template) throw Error('Unknown template: ' + templateName);
const packageManager =
options.packageManager ??
(await select('Package Manager', {
choices: [
{ label: styleText('magenta', 'bun'), value: 'bun' },
{ label: styleText('red', 'npm'), value: 'npm' },
{ label: styleText('yellow', 'pnpm'), value: 'pnpm' },
{ label: styleText('cyan', 'yarn'), value: 'yarn' },
],
}));
const isExists = await pathExists(directory);
if (isExists) {
const isEmpty =
(await readdir(input.directory)).filter((dir) => dir !== '.git')
.length === 0;
(await readdir(directory)).filter((dir) => dir !== '.git').length === 0;
if (!isEmpty) {
consola.error(
`The directory ${path.resolve(input.directory)} is not empty. Aborted.`,
`The directory ${path.resolve(directory)} is not empty. Aborted.`,
);
process.exit(1);
}
}
await cloneProject(input);
await cloneProject({ directory, template });
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
const cdPath = path.relative(process.cwd(), path.resolve(directory));
console.log();
consola.log(
`✨ WXT project created with the ${
TEMPLATE_COLORS[input.template.name]
? styleText(TEMPLATE_COLORS[input.template.name], input.template.name)
: input.template.name
TEMPLATE_COLORS[template.name]
? styleText(TEMPLATE_COLORS[template.name], template.name)
: template.name
} template.`,
);
console.log();
consola.log('Next steps:');
let step = 0;
if (cdPath !== '')
consola.log(` ${++step}.`, styleText('cyan', `cd ${cdPath}`));
consola.log(
` ${++step}.`,
styleText('cyan', `${input.packageManager} install`),
);
consola.log(` ${++step}.`, styleText('cyan', `${packageManager} install`));
console.log();
}
+2 -8
View File
@@ -1,5 +1,4 @@
import { loadConfig } from 'c12';
import { resolve as esmResolve } from 'import-meta-resolve';
import {
ConfigEnv,
InlineConfig,
@@ -546,10 +545,7 @@ async function getUnimportEslintOptions(
/** Returns the path to `node_modules/wxt`. */
function resolveWxtModuleDir() {
// TODO: Switch to import.meta.resolve() once the parent argument is unflagged
// (e.g. --experimental-import-meta-resolve) and all Node.js versions we support
// have it.
const url = esmResolve('wxt', import.meta.url);
const url = import.meta.resolve('wxt', import.meta.url);
// esmResolve() returns the "wxt/dist/index.mjs" file, not the package's root
// directory, which we want to return from this function.
@@ -605,9 +601,7 @@ export async function resolveWxtUserModules(
// Resolve node_modules modules
const npmModules = await Promise.all<WxtModuleWithMetadata<any>>(
modules.map(async (moduleId) => {
// Resolve before importing to allow for a local WXT clone to be
// symlinked into a project.
const resolvedModulePath = esmResolve(moduleId, importer);
const resolvedModulePath = import.meta.resolve(moduleId, importer);
const mod: { default: WxtModule<any> } = await import(
/* @vite-ignore */ resolvedModulePath
);
@@ -18,6 +18,7 @@ import { wxt } from '../../wxt';
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
import { isCI } from 'ci-info';
import { styleText } from 'node:util';
import open from 'tiny-open';
/**
* Builds the extension based on an internal config. No more config discovery is
@@ -92,7 +93,6 @@ export async function internalBuild(): Promise<BuildOutput> {
wxt.logger.info(
`Opening ${styleText('yellow', statsPath)} in browser...`,
);
const { default: open } = await import('open');
await open(wxt.config.analysis.outputFile);
}
}
@@ -1,4 +1,4 @@
import { Mutex } from 'async-mutex';
import { withLock } from 'superlock';
import { relative } from 'node:path';
import {
BuildOutput,
@@ -30,12 +30,12 @@ import { normalizePath } from './paths';
* when a file changes.
*/
export function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex();
const fileChangedLock = withLock();
const changeQueue: Array<[string, string]> = [];
let processLoop: Promise<void> | undefined;
const processQueue = async () => {
const reloading = fileChangedMutex.runExclusive(async () => {
const reloading = fileChangedLock(async () => {
const fileChanges = changeQueue
.splice(0, changeQueue.length)
.map(([_, file]) => file);