Skip to content

Reduce devtools extension build size via safe engine-asset pruning - #10063

Open
NemesisDevX wants to merge 8 commits into
flutter:masterfrom
NemesisDevX:fix/9897-extension-build-size
Open

NemesisDevX wants to merge 8 commits into
flutter:masterfrom
NemesisDevX:fix/9897-extension-build-size

Conversation

@NemesisDevX

@NemesisDevX NemesisDevX commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9897

flutter build web copies every built web engine artifact into build/web/canvaskit, so the packaged extension/devtools/build output is ~44 MB even though a given extension never loads most of those files at runtime.

This PR makes build_and_copy prune what is provably or explicitly unneeded from the copied destination only (the source build/web output is untouched):

  • .js.symbols debug files are always removed — the bootstrap loader never fetches them.
  • skwasm*/wimp* renderer variants are removed only when every _flutter.buildConfig literal found in flutter_bootstrap.js or index.html provably contains no builds entry with "renderer": "skwasm". The loader selects engine resources from that configuration, and Skwasm can be selected by dart2wasm builds (flutter build web --wasm) and by dart2js builds that opt in via --dart-define=FLUTTER_WEB_USE_SKIA=false --dart-define=FLUTTER_WEB_USE_SKWASM=true — neither requires a root-level .wasm artifact. When no readable configuration exists, or any discovered configuration is malformed, dynamically assigned, or could be mutated at runtime — through reassignment, builds access, aliases, destructuring, Object.assign/Reflect, computed keys, for...of entry writes, or dynamic execution — the variants are kept (fail closed). The proof covers only the two inspected files.
  • A new opt-in --prune-local-canvaskit flag removes the entire canvaskit/ directory for extensions whose owners can assert the default CDN configuration applies (no custom bootstrap, index.html initialization, or runtime configuration overrides). It is rejected when combined with --no-web-resources-cdn.

Full pruning is deliberately opt-in: whether a build loads CanvasKit from the CDN depends on mutable runtime configuration (bootstrap reassignment/mutation, load({config: ...}), index.html initialization) that cannot be proven statically, so the default stays conservative in every mode. The skwasm/wimp check is narrower: those files are only fetched when a builds entry selects the skwasm renderer, which the emitted configuration declares statically.

Measured on the example standalone_extension (fresh flutter build web, head 9ace743):

Mode Before After
CDN (default) 43 MB 23 MB (symbols + unreachable wasm pruned, runtime set kept)
CDN + --prune-local-canvaskit 43 MB 7 MB
--tree-shake-icons 44 MB 21 MB (MaterialIcons 1.6 MB -> 8.4 KB)
--no-web-resources-cdn 44 MB 23 MB (local runtime set intact)

Testing on head 9ace743: dart test build_and_copy 27/27 (config-driven cases: dart2js+CanvasKit prunes; dart2js+Skwasm with no .wasm artifact, --wasm mixed builds, missing/malformed/dynamic/reassigned/mutated configs, alias/Object.assign/destructured/computed/for...of-entry mutations of builds, index.html-inlined configs all keep; plus the rejected flag combination and a read-only for...of loader control), dart test validate, flutter test api 12/12, dart analyze/dart format/git diff --check clean, dt presubmit passed, fresh end-to-end build_and_copy runs reproduce 43 MB -> 23 MB default and 43 MB -> 7 MB opt-in with SHA-256-verified file trees (only intended engine variants removed, zero content changes), and real flutter_bootstrap.js outputs verify stock builds prune while DevTools' own custom bootstrap correctly keeps with the real flutter_bootstrap.js (verified the loader's let i = _flutter.buildConfig alias + read-only for...of i.builds still allows pruning). Not run: browser-hosted test/web/ suite.

Follow-up commits (addressing review feedback)

  • 0360371 — avoids the redundant recursive listing and per-file deletions when the opt-in flag is set (one listing to sum sizes, then delete the directory).
  • cf0fe41, 2bb30c0 — changelog wording and dart format fix-ups.
  • d3e06ea — replaces the .wasm-artifact heuristic with _flutter.buildConfig renderer inspection (fail-closed), renames the flag to --prune-local-canvaskit, documents the flags in the README, replaces the no-op bootstrap tests with config-driven cases, and applies review cleanups (@visibleForTesting, shared constants, copyright year, changelog style).
  • 4d227b9 — extends the inspection to fail closed on aliased, destructured, Object.assign/Reflect, computed-key, and for...of entry mutations of builds, and hardens the token match (optional chaining, whitespace, globalThis-style prefixes).
  • 5584801 — treats every value-position _flutter.buildConfig reference (call argument, array/object literal, spread, ternary, ??) as an escape that fails closed; .prop/?.prop reads and ; statement ends remain the only safe continuations.
  • 9ace743 — fails closed on _flutter.loader.load({...}) calls carrying config/renderer/builds overrides (DevTools' own flutter_bootstrap.js does runtime renderer selection this way), any _flutter[...]/global[...] computed access, quoted watched-key strings, .renderer writes (incl. prototype pollution), {renderer: ...} keys, and non-string/case-variant renderer values.

Parts of this PR were assisted by AI review tooling; all changes were reviewed and verified before landing.

Pre-launch Checklist

General checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read the Flutter Style Guide recently, and have followed its advice.
  • I signed the CLA.
  • I updated/added relevant documentation (doc comments with ///).

Issues checklist

  • I listed at least one issue that this PR fixes in the description above.
  • I did not list at least one issue with the contributions-welcome or good-first-issue label. I understand this means my PR might take longer to be reviewed.

Tests checklist

  • I added new tests to check the change I am making...

AI-tooling checklist

  • OR I did use AI tooling, and...
    • I read the AI contributions guidelines and agree to follow them.
    • I reviewed all AI-generated code before opening this PR.
    • I understand and am able to discuss the code in this PR.
    • I have verifed the accuracy of any AI-generated text included in the PR description.
    • I commit to verifying the accuracy of any AI-generated code or text that I upload in response to review comments.

Feature-change checklist

  • This PR does not change the DevTools UI or behavior and...
    • I added the release-notes-not-required label or left a comment requesting the label be added.

@google-cla

google-cla Bot commented Nov 1, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

`flutter build web` copies every built web engine artifact into
build/web/canvaskit, so the packaged extension/devtools/build output is
~44 MB even though a given build never loads most of those files at
runtime (flutter#9897).

This change makes build_and_copy prune what is provably or explicitly
unneeded from the copied destination (source build output is untouched):

- .js.symbols debug files are always removed; the bootstrap loader never
  fetches them.
- skwasm/wimp renderer files are removed only when the build output
  contains no .wasm application artifact at its root, which proves the
  wasm renderer cannot be selected independently of any runtime
  configuration.
- A new opt-in --prune-cdn-canvaskit flag removes the entire canvaskit/
  directory for extensions whose owners can assert the default CDN
  configuration applies (no custom bootstrap, index.html initialization
  or runtime config overrides). It is rejected when combined with
  --no-web-resources-cdn.

The default remains conservative: whether a build loads CanvasKit from
the CDN depends on mutable runtime configuration that cannot be proven
statically, so full pruning requires explicit opt-in.

Measured on the example standalone extension (flutter build web):
default CDN build 44 -> 23 MB (or 7 MB with --prune-cdn-canvaskit),
offline 23 MB, --tree-shake-icons 21 MB.

Assisted-by: Devin AI
@NemesisDevX
NemesisDevX force-pushed the fix/9897-extension-build-size branch from 0402666 to 8ed122f Compare November 1, 2026 10:09
@NemesisDevX
NemesisDevX marked this pull request as ready for review November 1, 2026 13:03

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces size-reduction features for DevTools extensions by pruning unused web engine resources during the build process. It adds new flags (--tree-shake-icons, --web-resources-cdn, and --prune-cdn-canvaskit) to the build_and_copy command, implements the pruning logic in _build_and_copy.dart, and includes comprehensive unit tests. Feedback on the changes suggests optimizing the pruneCopiedBuildOutput method to avoid redundant directory listings and file deletions when pruneCdnCanvasKit is enabled.

NemesisDevX and others added 3 commits November 1, 2026 15:09
dart format reflows the root-level .wasm artifact check introduced in
0360371; no behavior change.

Assisted-by: Devin AI
@srawlins

Copy link
Copy Markdown
Contributor

@kenzieschmoll WDYT of this feature?

@kenzieschmoll kenzieschmoll left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! Reducing published extension size is worth doing, and --no-web-resources-cdn is useful on its own so extensions built with the default template can load CanvasKit offline. Pruning only the copied destination and keeping the full canvaskit/ prune opt-in are the right defaults.

Before landing, we need to fix the reasoning for the skwasm prune, remove several no-op tests, and document the new flags in the README. Details inline.

Comment on lines +173 to +178
/// * `skwasm`/`wimp` renderer files are only reachable when the output
/// contains a `.wasm` application artifact at the build root, which is
/// emitted only for `dart2wasm` builds (as `main.dart.wasm`, possibly
/// content-hashed). Its absence proves — independently of any mutable
/// bootstrap configuration — that the wasm renderer cannot be
/// selected, so those variants are removed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] The absence of a root-level .wasm file does not prove the skwasm renderer isn't used. flutter build web supports --dart-define=FLUTTER_WEB_USE_SKIA=false --dart-define=FLUTTER_WEB_USE_SKWASM=true for dart2js builds (see WebRendererMode.fromDartDefines in flutter_tools), which emits "compileTarget":"dart2js","renderer":"skwasm" without main.dart.wasm. The flutter.js loader picks loadCanvasKit or loadSkwasm based on build.renderer, not whether a .wasm file exists.

Pruning is only safe today because build_and_copy runs flutter build web --release without --wasm or dart-defines, so the output is always dart2js + canvaskit.

To support building extensions with wasm in the future, please check the renderer values in _flutter.buildConfig (emitted into flutter_bootstrap.js): only remove the skwasm/wimp files when no build uses the skwasm renderer, and keep everything if the config cannot be read. Please also add a test for a build config containing both dart2wasm + skwasm and dart2js + canvaskit builds (as emitted by flutter build web --wasm), and update the doc comment, CHANGELOG entry, and PR description to match.

@NemesisDevX NemesisDevX Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d3e06ea. The .wasm-artifact heuristic is removed: pruneCopiedBuildOutput now statically inspects every _flutter.buildConfig = {...} literal in flutter_bootstrap.js and index.html and removes the skwasm*/wimp* variants only when no builds entry declares "renderer": "skwasm". Missing, malformed, dynamically-assigned, or builds-mutating configurations all fail closed and keep the variants (plain loader reads of the config are ignored).

Follow-ups in 4d227b9, 5584801, and 9ace743: adversarial audits found the inspection still failed open when the config object was mutated through an alias (const config = _flutter.buildConfig; config.builds.push(...)), Object.assign/destructuring/computed keys/for...of entry writes, value-position escapes (call args, spread, ??, ternary), explicit _flutter.loader.load({config: ...}) overrides, _flutter[...]/global[...] computed access, quoted builds/buildConfig/renderer key strings used as variables, .renderer writes including prototype pollution, and non-string or case-variant renderer values. All of these now fail closed. Notably, DevTools' own build/web/flutter_bootstrap.js appends custom bootstrap code that selects the renderer at runtime via _flutter.loader.load({config: rendererConfig}) — the inspection now keeps engine variants for exactly this pattern while stock flutter build web output (whose load() call only passes serviceWorkerSettings) still prunes.

New tests cover: dart2js+CanvasKit (prunes, real emitted config incl. the {} dry-run entry and skwasm entries in wasmHashes), dart2js+Skwasm via dart-defines with no root .wasm artifact (keeps — your exact case), the flutter build web --wasm mixed dart2wasm/skwasm + dart2js/canvaskit config (keeps), missing/malformed/dynamic/reassigned/mutated configs (keep), every mutation/escape/override vector above (keep), index.html-inlined configs, read-only loader controls (let i = _flutter.buildConfig + for...of i.builds, === comparison), and union behavior when the two files disagree. Verified end-to-end on fresh flutter build web output: 43 MB -> 23 MB default, 43 MB -> 7 MB with --prune-local-canvaskit, SHA-256 file-tree comparison shows only intended removals. Doc comment, CHANGELOG, and PR description updated to match.

Comment on lines +228 to +301
group('retains assets for configuration that cannot be proven safe', () {
for (final (description, contents) in [
(
'a config object reassigned after the emitted statement',
'_flutter.buildConfig = {"engineRevision":"abc123","builds":[]};\n'
'_flutter.buildConfig = runtimeOffline;\n'
'_flutter.loader.load({});\n',
),
(
'a mutated build configuration',
'_flutter.buildConfig = {"engineRevision":"abc123","builds":[]};\n'
'_flutter.buildConfig.useLocalCanvasKit = true;\n'
'_flutter.loader.load({});\n',
),
(
'an escaped configuration option key',
'_flutter.buildConfig = {"engineRevision":"abc123","builds":[]};\n'
r'_flutter.loader.load({conf\u0069g: {canvasKitBaseUrl: "./"}});'
'\n',
),
(
'a getter configuration option',
'_flutter.buildConfig = {"engineRevision":"abc123","builds":[]};\n'
'_flutter.loader.load({'
'get config() { return window.runtimeOptions; }});\n',
),
(
'a dynamically appended wasm build entry',
'_flutter.buildConfig = {"engineRevision":"abc123","builds":'
'[{"compileTarget":"dart2js","renderer":"canvaskit"}]};\n'
'_flutter.buildConfig.builds.push('
'{"compileTarget":"dart2wasm"});\n'
'_flutter.loader.load({});\n',
),
]) {
test(description, () {
File(
p.join(buildDir.path, 'flutter_bootstrap.js'),
).writeAsStringSync(contents);
// A wasm artifact exists, so the wasm renderer is reachable.
File(
p.join(buildDir.path, 'main.dart.wasm'),
).writeAsStringSync('// wasm');

BuildExtensionCommand.pruneCopiedBuildOutput(
buildDir.path,
pruneCdnCanvasKit: false,
);

expectSymbolsRemoved();
expectRuntimeAssetsKept();
expect(
File(
p.join(buildDir.path, 'canvaskit', 'skwasm.wasm'),
).existsSync(),
isTrue,
);
});
}

test('a canvasKitBaseUrl in index.html initialization', () {
File(p.join(buildDir.path, 'index.html')).writeAsStringSync(
'<script>_flutter.loader.load({config: {'
'canvasKitBaseUrl: "canvaskit/"}});</script>',
);

BuildExtensionCommand.pruneCopiedBuildOutput(
buildDir.path,
pruneCdnCanvasKit: false,
);

expectRuntimeAssetsKept();
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] pruneCopiedBuildOutput never reads flutter_bootstrap.js or index.html, so varying their contents has no effect. The five bootstrap tests only pass because they create main.dart.wasm (already covered by default keeps wasm renderer when main.dart.wasm exists), and the index.html test duplicates default keeps all runtime CanvasKit variants.

Please remove this group so it doesn't imply the implementation inspects the bootstrap config.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Those fixtures varied flutter_bootstrap.js/index.html contents that the implementation never read, so they proved nothing. They are replaced by cases that drive the new _flutter.buildConfig inspection and assert on the resulting file tree — e.g. a dart2js+skwasm config must keep skwasm.wasm, a dart2js+canvaskit config must remove it — so each test fails if its production behavior is broken.

Comment on lines +216 to +226
test('works without a bootstrap file', () {
// The pruning decision never depends on flutter_bootstrap.js.
BuildExtensionCommand.pruneCopiedBuildOutput(
buildDir.path,
pruneCdnCanvasKit: false,
);

expectSymbolsRemoved();
expectWasmRendererRemoved();
expectRuntimeAssetsKept();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] The fake build output never includes a bootstrap file, making this identical to default keeps all runtime CanvasKit variants. Please remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with a meaningful fail-closed case: with no _flutter.buildConfig present in either flutter_bootstrap.js or index.html, the skwasm/wimp variants are kept — which is the required behavior when the configuration cannot be read.

Comment on lines +54 to 64
..addFlag(
_pruneCdnCanvasKitKey,
help:
'Delete the bundled canvaskit/ directory from the copied '
'extension build output. Only use this when the app is known to '
'load all web engine resources from the CDN at runtime: the '
'default `flutter build web` configuration with no custom '
'bootstrap, no custom index.html initialization, and no runtime '
'configuration that could select local resources. Cannot be '
'combined with --no-web-resources-cdn.',
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] Please document the three new flags under "Publish your package with a DevTools extension" in packages/devtools_extensions/README.md, including when --prune-cdn-canvaskit is safe to use (not just in --help).

[NIT] This name sounds like it prunes CDN assets. Consider --prune-local-canvaskit.

@NemesisDevX NemesisDevX Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d3e06ea. Renamed to --prune-local-canvaskit (it removes local assets, not CDN ones) and documented --tree-shake-icons, --web-resources-cdn/--no-web-resources-cdn, and the pruning flag under "Publish your package with a DevTools extension", including when full local CanvasKit removal is safe (default flutter build web CDN configuration, no custom bootstrap/index.html init or runtime overrides) versus when it is not.

/// Callers must ensure the app uses the default `flutter build web`
/// CDN configuration with no custom bootstrap, `index.html`
/// initialization, or runtime configuration overrides.
static void pruneCopiedBuildOutput(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Please add @visibleForTesting since this is only public for testing, and consider extracting named constants for 'canvaskit', '.js.symbols', 'skwasm', and 'wimp'.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added @visibleForTesting to pruneCopiedBuildOutput and extracted _canvaskitDirName, _jsSymbolsSuffix, _skwasmAssetPrefix, _wimpAssetPrefix, and _skwasmRendererName.

Comment on lines +240 to +241
static void _log(String message) =>
stdout.writeln('[build_and_copy] $message');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] '[build_and_copy]' duplicates name. Please share a constant for both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted _commandName ('build_and_copy'), now shared by the name getter and the [build_and_copy] log prefix.

@@ -0,0 +1,334 @@
// Copyright 2025 The Flutter Authors

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Use 2026 for the copyright year in new files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the new test file's copyright to 2026.

Comment on lines +12 to +25
* Reduce extension build output size by pruning web engine resources that are
never requested at runtime. `.js.symbols` debug files are always removed,
and `skwasm`/`wimp` renderer files are removed when the build output
contains no root-level `.wasm` application artifact (so the wasm
renderer is not selected). A new opt-in `--prune-cdn-canvaskit` flag
additionally removes the entire local `canvaskit/` directory (~36 MB) for
builds that are known to load all engine resources from the CDN at
runtime; it cannot be combined with `--no-web-resources-cdn`.
* Add a `--tree-shake-icons` flag to the `build_and_copy` command that allows
the compiler to tree-shake icon fonts (disabled by default for backwards
compatibility). Passing it can shrink `MaterialIcons-Regular.otf` by ~99%.
* Add a `--web-resources-cdn` / `--no-web-resources-cdn` flag to the
`build_and_copy` command for building extensions that do not load CanvasKit
from the CDN (e.g. for offline use).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Please format entries as single unwrapped lines to match the rest of the file, and move the detailed reasoning from the first entry to the doc comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog entries are now single unwrapped lines matching the file's style, and the detailed reasoning moved into the pruneCopiedBuildOutput doc comment.

The previous heuristic treated the absence of a root-level .wasm artifact
as proof that the skwasm renderer is unreachable, but dart2js builds can
select Skwasm through FLUTTER_WEB_USE_SKIA/SKWASM dart-defines without
emitting a .wasm artifact.

Parse the renderer values of every _flutter.buildConfig literal emitted
into flutter_bootstrap.js and index.html, and only remove the
skwasm*/wimp* variants when no build declares the skwasm renderer. The
variants are kept whenever the configuration is missing, malformed,
dynamic, or mutates builds, since the loader's selection cannot be
proven statically in those cases.

Also rename --prune-cdn-canvaskit to --prune-local-canvaskit (it removes
local assets, not CDN ones), document the build_and_copy flags in the
README, replace the bootstrap tests that varied unread file contents
with cases that exercise the new inspection, and apply review cleanups
(visibleForTesting, shared constants, changelog style).

Assisted-by: Devin AI
@NemesisDevX
NemesisDevX force-pushed the fix/9897-extension-build-size branch from 889a780 to d3e06ea Compare November 13, 2026 23:31
The buildConfig inspection only checked each _flutter.buildConfig
occurrence in isolation, so mutations through an alias
(const config = _flutter.buildConfig; config.builds.push(...)),
destructuring, Object.assign/Reflect calls, computed ['builds']
access, or for-of entry writes escaped detection and could prune
skwasm/wimp assets that a mutated configuration still needs.

Scan each inspected file for builds-mutation syntax after blanking
the parsed literals: every .builds access must be a read-only
for-of iteration whose body does not write to the loop variable,
and computed keys, builds/buildConfig object keys, destructuring,
Object.*/Reflect.* mutations, dynamic execution, and computed
config access all fail closed. The token now also matches
optional-chained, whitespace-separated, and globalThis/self-style
prefixed forms, and a comma after a non-assignment reference is
treated as an escape.

Verified against a real flutter_bootstrap.js: the loader's
let i = _flutter.buildConfig alias and its read-only
for (const u of i.builds) iteration still allow pruning, so the
measured 43 MB -> 23 MB default output is unchanged. Adds nine
regression tests covering the alias, Object.assign, destructuring,
computed-member, for-of entry-write, defineProperty, and
globalThis vectors plus a read-only for-of control.

Assisted-by: Devin AI
The escape check only flagged a comma after a non-assignment
_flutter.buildConfig reference, but the object leaks the same way
through call arguments (f(_flutter.buildConfig)), array/object
literals, spread, ternaries, and ?? — any position where the
reference itself becomes a value. Widen the pattern to flag
`,`, `)`, `]`, `}`, `:`, `&`, `|`, and `?` not followed by `.`
(i.e. ?? but not ?.), keeping `.prop`/`?.prop` reads and `;`
statement ends as the only safe continuations.

Verified against real flutter_bootstrap.js output: the loader's
?.wasmHashes read, `let i = _flutter.buildConfig` alias, string
literal mention, and `;`-terminated statements all still pass, so
standard CanvasKit builds keep pruning (fresh standalone_extension
build/web output: skwasm/wimp removed). Stale example outputs
built with an older SDK loader that calls i.builds.find() were
already kept before this change and still are — the for-of-only
rule is unchanged. Adds four tests: call-arg, spread, ?? escapes
kept; === comparison still prunes.

Assisted-by: Devin AI
An adversarial audit of the buildConfig inspection found six more
fail-open vectors on real and synthetic inputs:

- Custom bootstrap code appended to flutter_bootstrap.js can call
  _flutter.loader.load({config: ...}) with runtime-derived renderer
  selection — devtools_app's own build output does exactly this
  (shouldUseSkwasm() -> config override). The options object is now
  brace-matched and scanned for config/renderer/builds/buildConfig
  keys; the stock loader's serviceWorkerSettings-only call still
  passes, so standard builds keep pruning.
- _flutter[expr] computed member access bypassed the buildConfig
  token entirely; any bracket access on _flutter now fails closed.
- global['_flutter']-style computed global access was not covered;
  `global` joins the computed-access pattern.
- Quoted 'builds'/'buildConfig'/'renderer' key strings anywhere in
  the file could smuggle watched properties through variables
  (const k = 'builds'; config[k].push(...)); they now fail closed.
- .renderer writes (entry.renderer = 'skwasm', including prototype
  pollution like Object.prototype.renderer) are now flagged; reads
  in the loader's for-of loop are unaffected.
- {renderer: ...} object keys are watched alongside builds/
  buildConfig, and builds entries with a present-but-non-string
  renderer fail closed; renderer comparison is case-insensitive.

Verified against real flutter_bootstrap.js output: stock builds
(standalone_extension, foo_devtools_extension, devtools_app source
build/web) still prune skwasm/wimp while devtools_app's own custom
bootstrap — which genuinely selects the renderer at runtime — now
correctly keeps them. Adds seven regression tests (six failed before
this change). End-to-end build_and_copy on fresh flutter build web
output: 43 MB -> 23 MB default, 43 MB -> 7 MB with
--prune-local-canvaskit; SHA-256 tree comparison shows only the
intended engine-variant removals and zero content changes.

Assisted-by: Devin AI

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[devtools_extensions] build_and_copy ships ~14 MB of assets the extension never loads (CanvasKit variants + un-tree-shaken icon font)

3 participants