From 9c12255c002a5bfae9fe8eb1f37d777562d66368 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 2 Sep 2026 23:17:03 +0200 Subject: [PATCH 1/2] Move away from build based style hash --- packages/site_shared/build.yaml | 16 ----- .../lib/_sass/components/_code.scss | 2 +- packages/site_shared/lib/server_util.dart | 8 +++ .../lib/src/builders/styles_hash_builder.dart | 50 ---------------- .../lib/src/layouts/dash_layout.dart | 6 +- .../utils/cache_busted_build_asset_url.dart | 60 +++++++++++++++++++ packages/site_shared/pubspec.yaml | 1 - .../docs/lib/src/layouts/flutter_layout.dart | 4 -- sites/docs/lib/src/style_hash.dart | 5 -- sites/docs/pubspec.yaml | 1 - sites/www/lib/src/layouts/default_layout.dart | 8 +-- sites/www/lib/src/style_hash.dart | 5 -- 12 files changed, 77 insertions(+), 89 deletions(-) delete mode 100644 packages/site_shared/build.yaml create mode 100644 packages/site_shared/lib/server_util.dart delete mode 100644 packages/site_shared/lib/src/builders/styles_hash_builder.dart create mode 100644 packages/site_shared/lib/src/utils/cache_busted_build_asset_url.dart delete mode 100644 sites/docs/lib/src/style_hash.dart delete mode 100644 sites/www/lib/src/style_hash.dart diff --git a/packages/site_shared/build.yaml b/packages/site_shared/build.yaml deleted file mode 100644 index 68cf78f9cd5..00000000000 --- a/packages/site_shared/build.yaml +++ /dev/null @@ -1,16 +0,0 @@ -builders: - stylesHashBuilder: - import: "package:site_shared/src/builders/styles_hash_builder.dart" - builder_factories: ["stylesHashBuilder"] - build_extensions: - "web/assets/css/main.css": - - "lib/src/style_hash.dart" - "web/main.css": - - "lib/src/style_hash.dart" - auto_apply: dependents - build_to: source - required_inputs: - - ".css" - defaults: - dev_options: - fixed_hash: true diff --git a/packages/site_shared/lib/_sass/components/_code.scss b/packages/site_shared/lib/_sass/components/_code.scss index 580cb228f50..a33b66024a4 100644 --- a/packages/site_shared/lib/_sass/components/_code.scss +++ b/packages/site_shared/lib/_sass/components/_code.scss @@ -188,7 +188,7 @@ pre { list-style: none; position: relative; - // Caret at the beginning of the folding range. + // Caret at the beginning of the folding range. span.material-symbols { position: absolute; cursor: pointer; diff --git a/packages/site_shared/lib/server_util.dart b/packages/site_shared/lib/server_util.dart new file mode 100644 index 00000000000..be8240d576f --- /dev/null +++ b/packages/site_shared/lib/server_util.dart @@ -0,0 +1,8 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +/// Utilities meant to be used when running on the server. +library; + +export 'src/utils/cache_busted_build_asset_url.dart'; diff --git a/packages/site_shared/lib/src/builders/styles_hash_builder.dart b/packages/site_shared/lib/src/builders/styles_hash_builder.dart deleted file mode 100644 index 32480817dbd..00000000000 --- a/packages/site_shared/lib/src/builders/styles_hash_builder.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -import 'package:build/build.dart'; -import 'package:crypto/crypto.dart'; - -Builder stylesHashBuilder(BuilderOptions options) => StylesHashBuilder(options); - -class StylesHashBuilder implements Builder { - const StylesHashBuilder(this.options); - - final BuilderOptions options; - - @override - Map> get buildExtensions => { - 'web/assets/css/main.css': ['lib/src/style_hash.dart'], - 'web/main.css': ['lib/src/style_hash.dart'], - }; - - @override - Future build(BuildStep buildStep) async { - final String hashString; - - if (options.config['fixed_hash'] == true) { - hashString = ''; - } else { - final inputId = buildStep.inputId; - final bytes = await buildStep.readAsBytes(inputId); - final digest = sha256.convert(bytes); - hashString = base64Url.encode(digest.bytes).substring(0, 12); - } - - final outputContent = - ''' -// Generated by site_shared|stylesHashBuilder. Do not edit. -// dart format off - -/// The generated hash of the `main.css` file. -const generatedStylesHash = '$hashString'; -'''; - - await buildStep.writeAsString( - buildStep.allowedOutputs.single, - outputContent, - ); - } -} diff --git a/packages/site_shared/lib/src/layouts/dash_layout.dart b/packages/site_shared/lib/src/layouts/dash_layout.dart index db9f8fb9fd9..8b219bfd082 100644 --- a/packages/site_shared/lib/src/layouts/dash_layout.dart +++ b/packages/site_shared/lib/src/layouts/dash_layout.dart @@ -11,6 +11,7 @@ import 'package:jaspr_content/jaspr_content.dart'; import '../../components/common/client/cookie_notice.dart'; import '../../components/layout/banner.dart'; import '../../util.dart'; +import '../utils/cache_busted_build_asset_url.dart'; /// The base Jaspr Content layout for all sites. abstract class DashLayout implements PageLayout { @@ -52,7 +53,8 @@ abstract class DashLayout implements PageLayout { 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0', ]; - String get stylesHash; + /// The stylesheet URL for the primary site styles. + String get stylesUrl => '/assets/css/main.css'; Iterable buildExtraHead(Page page) => const []; @@ -149,7 +151,7 @@ abstract class DashLayout implements PageLayout { // Set site styles. link( rel: 'stylesheet', - href: '/assets/css/main.css?hash=$stylesHash', + href: cacheBustedBuildAssetUrl(stylesUrl), ), // Set site scripts. diff --git a/packages/site_shared/lib/src/utils/cache_busted_build_asset_url.dart b/packages/site_shared/lib/src/utils/cache_busted_build_asset_url.dart new file mode 100644 index 00000000000..6d3e9f794a0 --- /dev/null +++ b/packages/site_shared/lib/src/utils/cache_busted_build_asset_url.dart @@ -0,0 +1,60 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:jaspr/server.dart'; +import 'package:path/path.dart' as path; + +final Map _cacheBustedBuildAssetUrls = {}; + +/// The [assetUrl] with a content hash appended. +/// +/// Jaspr writes web assets to `build/jaspr` before it +/// starts the static site renderer. +/// This function reads the built asset and appends a +/// truncated SHA-256 hash as a query parameter. +/// Outside static generation, it returns [assetUrl] unchanged. +/// +/// If the built asset doesn't exist, throws a [FileSystemException]. +/// If the built asset is empty, throws a [StateError]. +String cacheBustedBuildAssetUrl(String assetUrl) { + if (!kGenerateMode) { + return assetUrl; + } + + return _cacheBustedBuildAssetUrls.putIfAbsent(assetUrl, () { + final assetUri = Uri.parse(assetUrl); + final assetPath = path.joinAll([ + 'build', + 'jaspr', + ...assetUri.pathSegments, + ]); + final assetFile = File(assetPath); + if (!assetFile.existsSync()) { + throw FileSystemException( + 'Built asset not found. Ensure web assets are built before rendering.', + assetFile.path, + ); + } + + final bytes = assetFile.readAsBytesSync(); + if (bytes.isEmpty) { + throw StateError('Built asset is empty: ${assetFile.path}'); + } + + final digest = sha256.convert(bytes); + final hash = base64Url.encode(digest.bytes).substring(0, 12); + return assetUri + .replace( + queryParameters: { + ...assetUri.queryParameters, + 'hash': hash, + }, + ) + .toString(); + }); +} diff --git a/packages/site_shared/pubspec.yaml b/packages/site_shared/pubspec.yaml index 002c5f8aff4..13f8a35fd0c 100644 --- a/packages/site_shared/pubspec.yaml +++ b/packages/site_shared/pubspec.yaml @@ -6,7 +6,6 @@ environment: sdk: ^3.13.0-0 dependencies: - build: ^4.0.7 collection: ^1.19.1 crypto: ^3.0.7 html: ^0.15.6 diff --git a/sites/docs/lib/src/layouts/flutter_layout.dart b/sites/docs/lib/src/layouts/flutter_layout.dart index 989a7b82df2..23be170e6c4 100644 --- a/sites/docs/lib/src/layouts/flutter_layout.dart +++ b/sites/docs/lib/src/layouts/flutter_layout.dart @@ -15,7 +15,6 @@ import '../components/layout/header.dart'; import '../components/layout/sidenav.dart'; import '../models/sidenav_model.dart'; import '../pages/markdown.dart'; -import '../style_hash.dart'; /// The base Jaspr Content layout for wrapping site content. abstract class FlutterDocsLayout extends DashLayout { @@ -42,9 +41,6 @@ abstract class FlutterDocsLayout extends DashLayout { @override String get analyticsId => 'UA-67589403-1'; - @override - String get stylesHash => generatedStylesHash; - String get defaultSidenav => 'default'; bool get allowBreadcrumbs => true; diff --git a/sites/docs/lib/src/style_hash.dart b/sites/docs/lib/src/style_hash.dart deleted file mode 100644 index bd8e030c02b..00000000000 --- a/sites/docs/lib/src/style_hash.dart +++ /dev/null @@ -1,5 +0,0 @@ -// Generated by site_shared|stylesHashBuilder. Do not edit. -// dart format off - -/// The generated hash of the `main.css` file. -const generatedStylesHash = ''; diff --git a/sites/docs/pubspec.yaml b/sites/docs/pubspec.yaml index 38323c04f2d..f5e19f9ca9c 100644 --- a/sites/docs/pubspec.yaml +++ b/sites/docs/pubspec.yaml @@ -7,7 +7,6 @@ environment: sdk: ^3.13.0-0 dependencies: - build: ^4.0.7 collection: ^1.19.1 html: ^0.15.6 http: ^1.6.0 diff --git a/sites/www/lib/src/layouts/default_layout.dart b/sites/www/lib/src/layouts/default_layout.dart index 8b70f99c290..dc613d54443 100644 --- a/sites/www/lib/src/layouts/default_layout.dart +++ b/sites/www/lib/src/layouts/default_layout.dart @@ -5,12 +5,12 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/server.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/server_util.dart'; import '../components/layout/footer.dart'; import '../components/layout/header.dart'; import '../components/pages/consultants_cookie_snack.dart'; import '../models/content/banner_content.dart'; -import '../style_hash.dart'; import '../utils/asset_utils.dart'; import '../utils/data_utils.dart'; @@ -176,9 +176,9 @@ class DefaultLayout extends PageLayout { rel: 'stylesheet', ), - // Project Styles - const link( - href: '/main.css?hash=$generatedStylesHash', + // Set site styles. + link( + href: cacheBustedBuildAssetUrl('/main.css'), rel: 'stylesheet', ), ], diff --git a/sites/www/lib/src/style_hash.dart b/sites/www/lib/src/style_hash.dart deleted file mode 100644 index bd8e030c02b..00000000000 --- a/sites/www/lib/src/style_hash.dart +++ /dev/null @@ -1,5 +0,0 @@ -// Generated by site_shared|stylesHashBuilder. Do not edit. -// dart format off - -/// The generated hash of the `main.css` file. -const generatedStylesHash = ''; From 6d6547098502756f3bbcd18c72d791d9bb8d1420 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 2 Sep 2026 23:20:57 +0200 Subject: [PATCH 2/2] Adjust API doc comment --- packages/site_shared/lib/src/layouts/dash_layout.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/site_shared/lib/src/layouts/dash_layout.dart b/packages/site_shared/lib/src/layouts/dash_layout.dart index 8b219bfd082..83364d11f4c 100644 --- a/packages/site_shared/lib/src/layouts/dash_layout.dart +++ b/packages/site_shared/lib/src/layouts/dash_layout.dart @@ -53,7 +53,7 @@ abstract class DashLayout implements PageLayout { 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0', ]; - /// The stylesheet URL for the primary site styles. + /// The stylesheet URL for the site's built styles. String get stylesUrl => '/assets/css/main.css'; Iterable buildExtraHead(Page page) => const [];