diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb9c50b..b86845c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,11 @@ jobs: strategy: matrix: os: [ubuntu, macos, windows] - dart-channel: [stable, dev] + dart-channel: [stable] + # TODO(nweiz): Re-enable this when + # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228 + # is addressed. + # dart-channel: [stable, dev] shard: [0, 1, 2] fail-fast: false @@ -49,7 +53,11 @@ jobs: strategy: matrix: - dart-channel: [stable, dev] + dart-channel: [stable] + # TODO(nweiz): Re-enable this when + # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228 + # is addressed. + # dart-channel: [stable, dev] fail-fast: false runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3e502..df8483e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Add `isNodeJs` and `process` getters to `package:cli_pkg/js.dart`. + ## 2.5.0 * Add a `wrapJSException()` function in the new `package:cli_pkg/js.dart` diff --git a/browser_library_test/test/browser_test_shared.dart b/browser_library_test/test/browser_test_shared.dart index f45e8f7..f8d6222 100644 --- a/browser_library_test/test/browser_test_shared.dart +++ b/browser_library_test/test/browser_test_shared.dart @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:cli_pkg/js.dart'; import 'package:js/js.dart'; import 'package:test/test.dart'; @@ -50,4 +51,12 @@ void main() { test("the default dependency is not loaded", () { expect(loadedDefaultDependency, isFalse); }); + + test("isNodeJs returns 'false'", () { + expect(isNodeJs, isFalse); + }); + + test("process returns 'null'", () { + expect(process, isNull); + }); } diff --git a/browser_library_test/test/node_js_helpers_in_browser_test.dart b/browser_library_test/test/node_js_helpers_in_browser_test.dart new file mode 100644 index 0000000..e858de8 --- /dev/null +++ b/browser_library_test/test/node_js_helpers_in_browser_test.dart @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@TestOn('browser') + +import 'dart:js_interop'; +import 'dart:js_util'; + +import 'package:cli_pkg/js.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() => delete(globalThis, 'process')); + + const nonNodeJsProcessTestCases = >>{ + 'an empty process': {}, + 'a process with empty release': {'release': {}}, + 'a process with non-Node.JS release name': { + 'release': {'name': 'hello'} + }, + }; + + const fakeNodeJsProcess = { + 'release': {'name': 'node'} + }; + + group('isNodeJs', () { + test("returns 'false' from the browser", () { + expect(isNodeJs, isFalse); + }); + + for (final entry in nonNodeJsProcessTestCases.entries) { + final caseName = entry.key; + final processJson = entry.value.jsify(); + + test("returns 'false' when $caseName exists in the 'window'", () { + setProperty(globalThis, 'process', processJson); + expect(isNodeJs, isFalse); + }); + } + + test("returns 'true' with a fake Node.JS process", () { + setProperty(globalThis, 'process', fakeNodeJsProcess.jsify()); + expect(isNodeJs, isTrue); + }); + }); + + group('process', () { + test("returns 'null' from the browser", () { + expect(process, isNull); + }); + + for (final entry in nonNodeJsProcessTestCases.entries) { + final caseName = entry.key; + final processJson = entry.value.jsify(); + + test("returns 'null' when $caseName exists in the 'window'", () { + setProperty(globalThis, 'process', processJson); + expect(process, isNull); + }); + } + + test("returns a fake process if it fakes being a Node.JS environment", () { + setProperty(globalThis, 'process', fakeNodeJsProcess.jsify()); + expect(process.jsify().dartify(), fakeNodeJsProcess); + }); + }); +} diff --git a/lib/js.dart b/lib/js.dart index 3d75ca2..ca380bb 100644 --- a/lib/js.dart +++ b/lib/js.dart @@ -12,7 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:js/js.dart'; import 'package:js/js_util.dart'; +import 'package:node_interop/process.dart'; + +@JS('process') +external final Process? _process; // process is null in the browser + +/// This extension adds `maybe` getters that return non-nullable +/// properties with a nullable type. +extension PartialProcess on Process { + /// Returns [release] as nullable. + Release? get maybeRelease => release; +} + +/// Whether the script is being executed in a Node.JS environment. +bool get isNodeJs => _process?.maybeRelease?.name == 'node'; + +/// Returns the NodeJs [Process] value only when the script is running in +/// Node.JS, otherwise returns `null`. +/// +/// By checking whether the script is running in Node.JS we can avoid returning +/// a non-[Process] object if the script is running in a browser, and there is a +/// different `process` object in the `window`. This can happen when a library +/// or framework adds a global variable named `process`. For example, Next.JS +/// adds [`process.env`] to access environment variables on the browser. +/// +/// [`process.env`]: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser +/// +/// The getter can still return a non-[Process] object if the script is running +/// in a browser and there is a `process` object in the `window` with the value +/// `{release: {name: 'node'}}`. In this case the script must trust the browser +/// is emulating a Node.JS environment. +/// +/// Note: If you use this, add a dependency on `node_interop` to ensure you get +/// a version compatible with your usage. +Process? get process => isNodeJs ? _process : null; /// Whether this Dart code is running in a strict mode context. /// diff --git a/pubspec.yaml b/pubspec.yaml index b81ed79..61384a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: cli_pkg -version: 2.5.0 +version: 2.6.0 description: Grinder tasks for releasing Dart CLI packages. homepage: https://github.com/google/dart_cli_pkg diff --git a/test/npm_test.dart b/test/npm_test.dart index 80bf7af..5b4cecd 100644 --- a/test/npm_test.dart +++ b/test/npm_test.dart @@ -1135,6 +1135,50 @@ void main() { test("handles a thrown undefined", () => assertCatchesGracefully('BigInt(undefined)')); }); + + test("isNodeJs returns `true` when running in Node.JS", () async { + await d.package(pubspec, _enableNpm, [ + _packageJson, + d.dir("bin", [ + d.file("foo.dart", """ + import 'package:cli_pkg/js.dart'; + + void main() { + print(isNodeJs); + } + """) + ]), + ]).create(); + + await (await grind(["pkg-npm-dev"])).shouldExit(); + + var process = await TestProcess.start( + "node$dotExe", [d.path("my_app/build/npm/foo.js")]); + expect(process.stdout, emitsInOrder(["true", emitsDone])); + expect(process.shouldExit(0), completes); + }); + + test("process returns a Process when running in Node.JS", () async { + await d.package(pubspec, _enableNpm, [ + _packageJson, + d.dir("bin", [ + d.file("foo.dart", """ + import 'package:cli_pkg/js.dart'; + + void main() { + print(process?.release.name); + } + """) + ]), + ]).create(); + + await (await grind(["pkg-npm-dev"])).shouldExit(); + + var process = await TestProcess.start( + "node$dotExe", [d.path("my_app/build/npm/foo.js")]); + expect(process.stdout, emitsInOrder(["node", emitsDone])); + expect(process.shouldExit(0), completes); + }); }); }