Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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', []); 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)"

Comment on lines +72 to +75

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 8e511e0: the step now prints the present tags and exits with an explicit integer (sys.exit(0 if ok else 1)), so a failure shows which tags were found.

changes:
name: Detect demo-affecting changes
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). (#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.
Expand Down
52 changes: 21 additions & 31 deletions lib/src/core/platform_service.dart
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand All @@ -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),
};
}
}
16 changes: 6 additions & 10 deletions lib/src/parser/parsers/background_parser.dart
Original file line number Diff line number Diff line change
@@ -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**
Expand Down Expand Up @@ -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('~/') ||
Expand Down
10 changes: 10 additions & 0 deletions lib/src/parser/parsers/file_image_io.dart
Original file line number Diff line number Diff line change
@@ -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<Object>? fileImageProvider(String path) => FileImage(File(path));
7 changes: 7 additions & 0 deletions lib/src/parser/parsers/file_image_stub.dart
Original file line number Diff line number Diff line change
@@ -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<Object>? fileImageProvider(String path) => null;
55 changes: 47 additions & 8 deletions test/core/platform_service_test.dart
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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));
});
});
}
9 changes: 9 additions & 0 deletions test/parser/parsers/background_parser_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<FileImage>());
expect((provider! as FileImage).file.path, '/abs/path/img.png');
});
});

group('BackgroundParser.canParse', () {
late BackgroundParser parser;

Expand Down