Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
41 changes: 41 additions & 0 deletions .github/workflows/js_interop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: package:js_interop
permissions: read-all

on:
# Run CI on pushes to the main branch and on PRs.
push:
branches: [ main ]
paths:
- '.github/workflows/js_interop.yaml'
- 'js_interop/**'
pull_request:
paths:
- '.github/workflows/js_interop.yaml'
- 'js_interop/**'
schedule:
- cron: "0 0 * * 0"

defaults:
run:
working-directory: js_interop/

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sdk: [3.9, beta, dev]
test_config: ['-p chrome', '-p chrome -c dart2wasm', '-p node']

steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
with:
sdk: ${{ matrix.sdk }}

- run: dart pub get
- run: dart format --output=none --set-exit-if-changed .
if: ${{ matrix.sdk == 'dev' }}
- run: dart analyze --fatal-infos
- run: dart test ${{ matrix.test_config }}
1 change: 1 addition & 0 deletions js_interop/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
platforms: [chrome, node]
1 change: 1 addition & 0 deletions js_interop/lib/js_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

export 'src/dart/date_time.dart';
export 'src/date.dart';
10 changes: 10 additions & 0 deletions js_interop/lib/src/dart/date_time.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import '../date.dart';

extension DateTimeToJSDate on DateTime {
/// Converts this to a [JSDate] that represents the same instant in time.
JSDate get toJS => JSDate.fromMillisecondsSinceEpoch(millisecondsSinceEpoch);
}
93 changes: 77 additions & 16 deletions js_interop/lib/src/date.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension type JSDate._(JSObject _) implements JSObject {
/// The [Date constructor] that returns the current date and time.
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
external JSDate.now();
external JSDate.nowAsDate();

/// The [Date constructor] with the number of milliseconds since the epoch of
/// January 1, 1970, UTC.
Expand All @@ -24,20 +24,83 @@ extension type JSDate._(JSObject _) implements JSObject {
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [from a string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#date_string
external JSDate.parse(String dateString);
external JSDate.parseAsDate(String dateString);

/// The [Date constructor] that copies its value [from an existing Date].
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [from an existing Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#date_object
external JSDate.from(JSDate other);

/// The [Date constructor] that uses [individual component integers].
/// The [Date constructor] that uses [individual component integers],
/// interpreted in the local time zone.
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [individual component integers]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#individual_date_and_time_component_values
external JSDate(int year, int month,
[int? day, int? hours, int? minutes, int? seconds, int? milliseconds]);
external JSDate.localDate(
int year,
int month, [
int? day,
int? hours,
int? minutes,
int? seconds,
int? milliseconds,
]);

/// The [Date constructor] that uses [individual component integers],
/// interpreted in the UTC time zone.
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [individual component integers]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#individual_date_and_time_component_values
factory JSDate.utcDate(
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we think JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...)) is bad enough that we should include this? I mostly ask because our treatment of nullability makes this factory a bit more complex. If we do want this we should document that treatment (e.g. "if day is null, we only pass year and month. if not and if hours is nullable,...").

Copy link
Member Author

Choose a reason for hiding this comment

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

I think given that there's already a local-time constructor, having a symmetrical one for UTC makes sense (especially because it's often going to be the safer option).

If we do want this we should document that treatment (e.g. "if day is null, we only pass year and month. if not and if hours is nullable,...").

It actually works in the opposite way: it passes all arguments through the last non-null. This makes the observable behavior identical to localDate: if you pass any intermediate nulls, you get an "Invalid Date". Since the behaviors match (and match the behavior of calling JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...)) I don't think it needs special documentation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see, I thought you were trying to avoid passing null regardless of whether the user passed it.

My follow-up question would've been "why not implement the body as JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...))", but it looks like someone decided to make the default for days 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters, so there is an observable difference between passing null and not passing anything. This LGTM then.

if you pass any intermediate nulls, you get an "Invalid Date"

Is this true? I'm constructing e.g. Date(2025, 11, null, 1) and that results in a valid date.

Copy link
Member Author

Choose a reason for hiding this comment

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

It looks like the behavior differs between null and undefined 😰. null gets coerced to 0, undefined gets coerced to NaN and produces an invalid date.

int year,
int month, [
int? day,
int? hours,
int? minutes,
int? seconds,
int? milliseconds,
]) {
var ms = switch ((day, hours, minutes, seconds, milliseconds)) {
Copy link
Member

Choose a reason for hiding this comment

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

Love me some switch!

(_, _, _, _, var milliseconds?) => JSDate.utcAsMillisecondsSinceEpoch(
year,
month,
day,
hours,
minutes,
seconds,
milliseconds,
),
(_, _, _, var seconds?, _) => JSDate.utcAsMillisecondsSinceEpoch(
year,
month,
day,
hours,
minutes,
seconds,
),
(_, _, var minutes?, _, _) => JSDate.utcAsMillisecondsSinceEpoch(
year,
month,
day,
hours,
minutes,
),
(_, var hours?, _, _, _) => JSDate.utcAsMillisecondsSinceEpoch(
year,
month,
day,
hours,
),
(var day?, _, _, _, _) => JSDate.utcAsMillisecondsSinceEpoch(
year,
month,
day,
),
_ => JSDate.utcAsMillisecondsSinceEpoch(year, month),
};
return JSDate.fromMillisecondsSinceEpoch(ms);
}

/// Dee [`Date.now()`].
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: See

///
Expand All @@ -55,13 +118,15 @@ extension type JSDate._(JSObject _) implements JSObject {
///
/// [`Date.utc()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC
@JS('UTC')
external static int utc(int year,
[int? month,
int? day,
int? hours,
int? minutes,
int? seconds,
int? milliseconds]);
external static int utcAsMillisecondsSinceEpoch(
int year, [
int? month,
int? day,
int? hours,
int? minutes,
int? seconds,
int? milliseconds,
]);

/// See [`Date.getDate()`] and [`Date.setDate()`].
///
Expand All @@ -81,14 +146,10 @@ extension type JSDate._(JSObject _) implements JSObject {
/// [`Date.getDay()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay
/// [`Date.setDay()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDay
int get day => _getDay();
set day(int value) => _setDay(value);

@JS('getDay')
external int _getDay();

@JS('setDay')
external void _setDay(int value);

/// See [`Date.getFullYear()`] and [`Date.setFullYear()`].
///
/// [`Date.getFullYear()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear
Expand Down
3 changes: 3 additions & 0 deletions js_interop/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ repository: https://github.com/dart-lang/web

environment:
sdk: ^3.9.0

dev_dependencies:
test: ^1.26.0
Loading