From eeead135bfe7fe0e97aa8fe75b2f1551fbdc756a Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Mon, 8 Jun 2026 17:11:30 +0300 Subject: [PATCH 1/5] fix: detect platform via defaultTargetPlatform to drop dart:io Replace dart:io Platform.is* with Flutter's WASM-safe defaultTargetPlatform through a pure @visibleForTesting resolvePlatform() mapper over an exhaustive TargetPlatform switch. Preserves the 7 platform strings and isMobile semantics. Tests exercise every branch via debugDefaultTargetPlatformOverride. --- lib/src/core/platform_service.dart | 52 +++++++++++--------------- test/core/platform_service_test.dart | 55 ++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/lib/src/core/platform_service.dart b/lib/src/core/platform_service.dart index 2b91cac..e838e12 100644 --- a/lib/src/core/platform_service.dart +++ b/lib/src/core/platform_service.dart @@ -1,5 +1,5 @@ -import 'package:flutter/foundation.dart' show kIsWeb; -import 'dart:io' show Platform; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb, visibleForTesting; /// Service to determine the current platform and whether it's mobile. /// Usage: @@ -9,7 +9,9 @@ import 'dart:io' show Platform; class WindPlatformService { // Private constructor WindPlatformService._internal() { - _initialize(); + final (resolvedPlatform, resolvedIsMobile) = resolvePlatform(); + platform = resolvedPlatform; + isMobile = resolvedIsMobile; } // Singleton instance @@ -25,36 +27,24 @@ class WindPlatformService { // Whether the platform is mobile (iOS or Android) late final bool isMobile; - // Initialize platform information - void _initialize() { + /// Resolves the current platform name and mobile flag from Flutter's + /// [defaultTargetPlatform]. Exposed for testing via + /// [debugDefaultTargetPlatformOverride]. + @visibleForTesting + static (String, bool) resolvePlatform() { if (kIsWeb) { - platform = "web"; - isMobile = false; - return; + // kIsWeb is always false under flutter test on a native host; this line + // is structurally unreachable from flutter test. + return ('web', false); // coverage:ignore-line } - if (Platform.isIOS) { - // CI runs on Linux + dev on macOS; iOS body unreachable from flutter_test on the host. - platform = "ios"; // coverage:ignore-line - isMobile = true; // coverage:ignore-line - } else if (Platform.isAndroid) { - // CI runs on Linux + dev on macOS; Android body unreachable from flutter_test on the host. - platform = "android"; // coverage:ignore-line - isMobile = true; // coverage:ignore-line - } else if (Platform.isLinux) { - platform = "linux"; // NO pragma; reachable on CI host. - isMobile = false; // NO pragma; reachable on CI host. - } else if (Platform.isMacOS) { - platform = "macos"; // NO pragma; reachable on dev host. - isMobile = false; // NO pragma; reachable on dev host. - } else if (Platform.isWindows) { - // Wind has no Windows CI runner or dev host; Windows body unreachable from flutter_test in this project. - platform = "windows"; // coverage:ignore-line - isMobile = false; // coverage:ignore-line - } else { - // Fallback for unknown platforms; not reachable from any test host this project supports. - platform = "unknown"; // coverage:ignore-line - isMobile = false; // coverage:ignore-line - } + return switch (defaultTargetPlatform) { + TargetPlatform.iOS => ('ios', true), + TargetPlatform.android => ('android', true), + TargetPlatform.linux => ('linux', false), + TargetPlatform.macOS => ('macos', false), + TargetPlatform.windows => ('windows', false), + TargetPlatform.fuchsia => ('unknown', false), + }; } } diff --git a/test/core/platform_service_test.dart b/test/core/platform_service_test.dart index 843d393..a72e7d4 100644 --- a/test/core/platform_service_test.dart +++ b/test/core/platform_service_test.dart @@ -1,17 +1,18 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fluttersdk_wind/src/core/platform_service.dart'; /// Tests for [WindPlatformService]. /// -/// The service is a singleton — all tests share the same instance initialized -/// on first access. No reset is needed because the state is immutable after -/// `_initialize()` runs once. -/// -/// Platform-branch coverage note: iOS, Android, Windows, and the unknown -/// fallback bodies are pragma'd in the source (`// coverage:ignore-line`). -/// The Linux body is hit on CI; the macOS body is hit on dev. These tests -/// use set-membership and consistency checks that pass on both hosts. +/// The singleton is shared across tests; the `platform` and `isMobile` fields +/// are immutable after construction, so no reset is needed for those. +/// `resolvePlatform()` reads `defaultTargetPlatform` at call-time, so each +/// test overrides `debugDefaultTargetPlatformOverride` and resets in tearDown. void main() { + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); + group('WindPlatformService', () { test('factory constructor returns the same singleton instance', () { final s1 = WindPlatformService(); @@ -42,4 +43,42 @@ void main() { expect(service.isMobile, equals(expectedMobile)); }); }); + + group('WindPlatformService.resolvePlatform', () { + test('returns ios + isMobile=true for TargetPlatform.iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + expect(WindPlatformService.resolvePlatform(), ('ios', true)); + }); + + test('returns android + isMobile=true for TargetPlatform.android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + expect(WindPlatformService.resolvePlatform(), ('android', true)); + }); + + test('returns linux + isMobile=false for TargetPlatform.linux', () { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + + expect(WindPlatformService.resolvePlatform(), ('linux', false)); + }); + + test('returns macos + isMobile=false for TargetPlatform.macOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + + expect(WindPlatformService.resolvePlatform(), ('macos', false)); + }); + + test('returns windows + isMobile=false for TargetPlatform.windows', () { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + + expect(WindPlatformService.resolvePlatform(), ('windows', false)); + }); + + test('returns unknown + isMobile=false for TargetPlatform.fuchsia', () { + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + + expect(WindPlatformService.resolvePlatform(), ('unknown', false)); + }); + }); } From 9464eb98e6e9c066bf1b6191f24ef895d7512e68 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Mon, 8 Jun 2026 17:11:30 +0300 Subject: [PATCH 2/5] fix: resolve absolute-path images via conditional import to drop dart:io Move FileImage(File(path)) behind a 2-file conditional import (stub returns null on web/WASM; io arm returns FileImage on native). Removes the dart:io import and the redundant kIsWeb branch from background_parser. --- lib/src/parser/parsers/background_parser.dart | 16 ++++++---------- lib/src/parser/parsers/file_image_io.dart | 10 ++++++++++ lib/src/parser/parsers/file_image_stub.dart | 7 +++++++ test/parser/parsers/background_parser_test.dart | 9 +++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 lib/src/parser/parsers/file_image_io.dart create mode 100644 lib/src/parser/parsers/file_image_stub.dart diff --git a/lib/src/parser/parsers/background_parser.dart b/lib/src/parser/parsers/background_parser.dart index c187271..cde0b38 100644 --- a/lib/src/parser/parsers/background_parser.dart +++ b/lib/src/parser/parsers/background_parser.dart @@ -1,12 +1,10 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../../theme/wind_theme_data.dart'; import '../../utils/color_utils.dart'; import '../wind_context.dart'; import '../wind_style.dart'; +import 'file_image_stub.dart' if (dart.library.io) 'file_image_io.dart'; import 'wind_parser_interface.dart'; /// **Background Style Parser** @@ -251,13 +249,11 @@ class BackgroundParser implements WindParserInterface { imageUrlOrPath.startsWith('https://')) { imageProvider = NetworkImage(imageUrlOrPath); } else if (imageUrlOrPath.startsWith('/')) { - // An absolute filesystem path has no meaning on web, and `dart:io` - // `File` is unsupported there, so degrade gracefully by skipping it. - if (kIsWeb) { - // Web has no dart:io File; skip rather than throw at runtime. - return null; // coverage:ignore-line - } - imageProvider = FileImage(File(imageUrlOrPath)); + final provider = fileImageProvider(imageUrlOrPath); + // Null only on web/WASM where the `File` API is unavailable; degrade + // gracefully rather than throw. Unreachable from native flutter test. + if (provider == null) return null; // coverage:ignore-line + imageProvider = provider; } else { String assetPath = imageUrlOrPath; if (imageUrlOrPath.startsWith('~/') || diff --git a/lib/src/parser/parsers/file_image_io.dart b/lib/src/parser/parsers/file_image_io.dart new file mode 100644 index 0000000..ed98448 --- /dev/null +++ b/lib/src/parser/parsers/file_image_io.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +import 'package:flutter/widgets.dart' show FileImage, ImageProvider; + +/// Returns a [FileImage] for the given filesystem [path]. +/// +/// This arm is compiled only on native VM targets where dart:io File +/// is available. The stub arm in file_image_stub.dart is selected for +/// web and WASM targets via the conditional import in background_parser.dart. +ImageProvider? fileImageProvider(String path) => FileImage(File(path)); diff --git a/lib/src/parser/parsers/file_image_stub.dart b/lib/src/parser/parsers/file_image_stub.dart new file mode 100644 index 0000000..326675f --- /dev/null +++ b/lib/src/parser/parsers/file_image_stub.dart @@ -0,0 +1,7 @@ +import 'package:flutter/widgets.dart' show ImageProvider; + +/// Returns null on web/WASM targets where the `File` API is unavailable. +/// +/// The io arm in file_image_io.dart is selected instead on native VM targets +/// via the conditional import in background_parser.dart. +ImageProvider? fileImageProvider(String path) => null; diff --git a/test/parser/parsers/background_parser_test.dart b/test/parser/parsers/background_parser_test.dart index e187fb8..1fcfbb5 100644 --- a/test/parser/parsers/background_parser_test.dart +++ b/test/parser/parsers/background_parser_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fluttersdk_wind/src/parser/parsers/background_parser.dart'; +import 'package:fluttersdk_wind/src/parser/parsers/file_image_io.dart'; import 'package:fluttersdk_wind/src/parser/wind_parser.dart'; import 'package:fluttersdk_wind/src/parser/wind_context.dart'; import 'package:fluttersdk_wind/src/parser/wind_style.dart'; @@ -324,6 +325,14 @@ void main() { }); }); + group('fileImageProvider (io arm)', () { + test('returns a FileImage whose file.path matches the given path', () { + final provider = fileImageProvider('/abs/path/img.png'); + expect(provider, isA()); + expect((provider! as FileImage).file.path, '/abs/path/img.png'); + }); + }); + group('BackgroundParser.canParse', () { late BackgroundParser parser; From ad114f4be503c2c272b88e4c934e46aea8eb08e8 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Mon, 8 Jun 2026 17:11:30 +0300 Subject: [PATCH 3/5] ci: assert is:wasm-ready in the Lint & Test job Run pana and fail the job if the is:wasm-ready tag is absent, guarding against dart:io re-entering the import graph. run-only step (no new pinned action). --- .github/workflows/deploy.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aeeb841..0bc5a9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -67,6 +67,12 @@ jobs: - name: Validate publish archive run: dart pub publish --dry-run + - name: Assert WASM-ready (pana) + run: | + dart pub global activate pana + dart pub global run pana --json --no-warning . > pana.json + python3 -c "import json,sys; tags=json.load(open('pana.json')).get('tags',[]); sys.exit(0 if 'is:wasm-ready' in tags else 'is:wasm-ready missing from pana tags')" + changes: name: Detect demo-affecting changes runs-on: ubuntu-latest From be488d4e52e895fcc3ddd3a4aadfb3a0b3dbc062 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Mon, 8 Jun 2026 17:11:30 +0300 Subject: [PATCH 4/5] docs(changelog): note WASM compatibility (pana 160/160) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c16a06..4f80f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ First stable release. wind is utility-first, Tailwind-syntax styling for Flutter ### Fixed - Background image parser (`bg-[/abs/path]`): the `FileImage(File(...))` branch is now guarded by `kIsWeb`; on web, where `dart:io` `File` is unsupported, the image degrades gracefully (skipped) instead of throwing at runtime. Non-web behavior is unchanged. `pubspec.yaml` now declares explicit platform support (`android`, `ios`, `macos`, `web`, `linux`, `windows`) so pub.dev platform detection is not narrowed by the `dart:io` import graph. +- WASM compatibility: removed `dart:io` from the library import graph (`platform_service.dart` now uses `defaultTargetPlatform`; absolute-path `bg-[/...]` image resolution moved behind a conditional import). The package is now `is:wasm-ready`, raising the pana/pub.dev platform-support score to 20/20 (160/160). - `max-w-prose`: corrected value from 1040 px (65 × 16, an incorrect approximation) to 512 px, matching the actual parser output. Docs and skill references updated accordingly. - `WButton` / `WAnchor` `semanticLabel`: the `Semantics` node now sets `excludeSemantics: true` and lifts `onTap`/`onLongPress` onto itself when `semanticLabel` is set, so the label overrides any child text instead of concatenating with it under `MergeSemantics`, while activation is preserved. - `Wind.installDebugResolver()`: the resolver no longer crashes on a className-less W-widget. `WindDebugResolverImpl.resolve` guarded its dynamic `className` read, so a bare `WAnchor` (or `WBreakpoint` / `WindAnimationWrapper` / `WKeyboardActions`) in the tree no longer throws `NoSuchMethodError` and abort the entire `fluttersdk_dusk` / telescope diagnostic snapshot. From 8e511e06bc2f44306141b30be62d0298e6a67b2b Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Mon, 8 Jun 2026 17:22:01 +0300 Subject: [PATCH 5/5] ci: make wasm-ready assertion log tags + exit explicitly Address PR review: print the present tags and exit with an explicit integer instead of sys.exit(), so a CI failure shows which tags were found. Also add the (#95) reference to the CHANGELOG entry. --- .github/workflows/deploy.yml | 2 +- CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0bc5a9e..5b3f1fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,7 +71,7 @@ jobs: run: | dart pub global activate pana dart pub global run pana --json --no-warning . > pana.json - python3 -c "import json,sys; tags=json.load(open('pana.json')).get('tags',[]); sys.exit(0 if 'is:wasm-ready' in tags else 'is:wasm-ready missing from pana tags')" + python3 -c "import json, sys; tags = json.load(open('pana.json')).get('tags', []); ok = 'is:wasm-ready' in tags; print('OK: is:wasm-ready present' if ok else 'FAIL: is:wasm-ready missing; tags=' + repr(tags)); sys.exit(0 if ok else 1)" changes: name: Detect demo-affecting changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f80f2f..6b73883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ First stable release. wind is utility-first, Tailwind-syntax styling for Flutter ### Fixed - Background image parser (`bg-[/abs/path]`): the `FileImage(File(...))` branch is now guarded by `kIsWeb`; on web, where `dart:io` `File` is unsupported, the image degrades gracefully (skipped) instead of throwing at runtime. Non-web behavior is unchanged. `pubspec.yaml` now declares explicit platform support (`android`, `ios`, `macos`, `web`, `linux`, `windows`) so pub.dev platform detection is not narrowed by the `dart:io` import graph. -- WASM compatibility: removed `dart:io` from the library import graph (`platform_service.dart` now uses `defaultTargetPlatform`; absolute-path `bg-[/...]` image resolution moved behind a conditional import). The package is now `is:wasm-ready`, raising the pana/pub.dev platform-support score to 20/20 (160/160). +- WASM compatibility: removed `dart:io` from the library import graph (`platform_service.dart` now uses `defaultTargetPlatform`; absolute-path `bg-[/...]` image resolution moved behind a conditional import). The package is now `is:wasm-ready`, raising the pana/pub.dev platform-support score to 20/20 (160/160). (#95) - `max-w-prose`: corrected value from 1040 px (65 × 16, an incorrect approximation) to 512 px, matching the actual parser output. Docs and skill references updated accordingly. - `WButton` / `WAnchor` `semanticLabel`: the `Semantics` node now sets `excludeSemantics: true` and lifts `onTap`/`onLongPress` onto itself when `semanticLabel` is set, so the label overrides any child text instead of concatenating with it under `MergeSemantics`, while activation is preserved. - `Wind.installDebugResolver()`: the resolver no longer crashes on a className-less W-widget. `WindDebugResolverImpl.resolve` guarded its dynamic `className` read, so a bare `WAnchor` (or `WBreakpoint` / `WindAnimationWrapper` / `WKeyboardActions`) in the tree no longer throws `NoSuchMethodError` and abort the entire `fluttersdk_dusk` / telescope diagnostic snapshot.