diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b964ca..52d0802 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,58 @@ env: MIN_COVERAGE: ${{ vars.MIN_COVERAGE }} jobs: - ci: + website: + name: Website Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ vars.DART_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: website/package-lock.json + + - name: Install dependencies + working-directory: website + run: npm ci + + - name: Get Playwright version + id: playwright-version + working-directory: website + run: echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: website + run: npx playwright install --with-deps chromium + + - name: Install Playwright deps only (cached) + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: Build website + working-directory: website + run: npm run build + + - name: Run tests + working-directory: website + run: npm test + + packages: name: Lint, Test & Build runs-on: ubuntu-latest steps: diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 2bb6a6d..73f4b7c 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -39,13 +39,13 @@ jobs: working-directory: website run: npm ci - - name: Generate API documentation + - name: Build website working-directory: website - run: ./scripts/generate-api-docs.sh + run: npm run build - - name: Build website + - name: Run website tests working-directory: website - run: npx @11ty/eleventy + run: bash scripts/test.sh - name: Setup Pages uses: actions/configure-pages@v4 diff --git a/.gitignore b/.gitignore index 88aef1d..f1ec095 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,15 @@ examples/mobile/rn/.expo/ # Website generated files website/_site/ website/src/api/ +website/src/zh/api/dart_node_core/ +website/src/zh/api/dart_node_express/ +website/src/zh/api/dart_node_react/ +website/src/zh/api/dart_node_react_native/ +website/src/zh/api/dart_node_ws/ +website/src/zh/api/dart_node_better_sqlite3/ +website/src/zh/api/dart_node_mcp/ +website/src/zh/api/dart_logging/ +website/src/zh/api/reflux/ website/.dart-doc-temp/ examples/frontend/coverage/ @@ -39,4 +48,9 @@ examples/too_many_cooks_vscode_extension/.vscode-test/ examples/reflux_demo/flutter_counter/test/failures/ -mutation-reports \ No newline at end of file +mutation-reports +.playwright-mcp/ + +website/playwright-report/ + +website/test-results/ diff --git a/CLAUDE.md b/CLAUDE.md index 92e34d2..23414cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,19 @@ Dart packages for building Node.js apps. Typed Dart layer over JS interop. - All packages require: `austerity` (linting), `nadz` (Result types) - `node_preamble` for dart2js Node.js compatibility +# Web & Translation + +- Optimize for AI Search and SEO +https://developers.google.com/search/blog/2025/05/succeeding-in-ai-search +https://developers.google.com/search/docs/fundamentals/seo-starter-guide + +- Always translate the English version to the target language directly. +- Be careful of cultural differences. +- Avoid literal translations that may offend the reader. +- Keep the code examples the same as the original but translate the comments to the target language +- Minimize CSS. +- Don't name CSS after sections. Name them after the HTML element + ## Codebase Structure ``` diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..050901a --- /dev/null +++ b/README_zh.md @@ -0,0 +1,48 @@ +# dart_node + +使用 Dart 编写完整技术栈:React Web 应用、基于 Expo 的 React Native 移动应用,以及 Node.js Express 后端。 + +[文档](https://melbournedeveloper.github.io/dart_node/) + +![React 和 React Native](images/dart_node.gif) + +## 包 + +| 包 | 描述 | +|---------|-------------| +| [dart_node_core](packages/dart_node_core) | 核心 JS 互操作工具 | +| [dart_node_express](packages/dart_node_express) | Express.js 绑定 | +| [dart_node_ws](packages/dart_node_ws) | WebSocket 绑定 | +| [dart_node_react](packages/dart_node_react) | React 绑定 | +| [dart_node_react_native](packages/dart_node_react_native) | React Native 绑定 | +| [dart_node_mcp](packages/dart_node_mcp) | MCP 服务器绑定 | +| [dart_node_better_sqlite3](packages/dart_node_better_sqlite3) | SQLite3 绑定 | +| [dart_jsx](packages/dart_jsx) | Dart JSX 转译器 | +| [reflux](packages/reflux) | Redux 风格状态管理 | +| [dart_logging](packages/dart_logging) | 结构化日志 | +| [dart_node_coverage](packages/dart_node_coverage) | dart2js 代码覆盖率 | + +## 工具 + +| 工具 | 描述 | +|------|-------------| +| [too-many-cooks](examples/too_many_cooks) | 多智能体协调 MCP 服务器 ([npm](https://www.npmjs.com/package/too-many-cooks)) | +| [Too Many Cooks VSCode](examples/too_many_cooks_vscode_extension) | 智能体可视化 VSCode 扩展 | + +## 快速开始 + +```bash +# 切换到本地依赖 +dart tools/switch_deps.dart local + +# 运行全部 +sh run_dev.sh +``` + +打开 http://localhost:8080/web/ + +**移动端:** 使用 VSCode 启动配置 `Mobile: Build & Run (Expo)` + +## 许可证 + +BSD 3-Clause 许可证。版权所有 (c) 2025,Christian Findlay。 diff --git a/packages/dart_jsx/README.md b/packages/dart_jsx/README.md index 4d94805..dd9685a 100644 --- a/packages/dart_jsx/README.md +++ b/packages/dart_jsx/README.md @@ -2,6 +2,13 @@ JSX transpiler for Dart - transforms JSX syntax to dart_node_react calls. +## Installation + +```yaml +dependencies: + dart_jsx: ^0.1.0 +``` + ## Usage Write JSX inside `jsx()` calls in your Dart files: diff --git a/packages/dart_jsx/README_zh.md b/packages/dart_jsx/README_zh.md new file mode 100644 index 0000000..41d2a2b --- /dev/null +++ b/packages/dart_jsx/README_zh.md @@ -0,0 +1,38 @@ +# dart_jsx + +Dart 的 JSX 转译器 - 将 JSX 语法转换为 dart_node_react 调用。 + +## 安装 + +```yaml +dependencies: + dart_jsx: ^0.1.0 +``` + +## 使用方法 + +在 Dart 文件中的 `jsx()` 调用内编写 JSX: + +```dart +final element = jsx(
+

Hello World

+ +
); +``` + +转译器将其转换为: + +```dart +final element = $div(className: 'app') >> [ + $h1 >> 'Hello World', + $button(onClick: handleClick) >> 'Click me', +]; +``` + +## VSCode 扩展 + +配套的 VSCode 扩展为 `.jsx` Dart 文件提供语法高亮。请参阅 [.vscode/extensions/dart-jsx](../../.vscode/extensions/dart-jsx)。 + +## dart_node 的一部分 + +[GitHub](https://github.com/MelbourneDeveloper/dart_node) diff --git a/packages/dart_logging/README.md b/packages/dart_logging/README.md index 4f8cae4..3022ee8 100644 --- a/packages/dart_logging/README.md +++ b/packages/dart_logging/README.md @@ -1,8 +1,14 @@ -# dart_logging -Pino-style structured logging with child loggers. +Pino-style structured logging with child loggers. Provides hierarchical logging with automatic context inheritance. -## Getting Started +## Installation + +```yaml +dependencies: + dart_logging: ^0.11.0-beta +``` + +## Quick Start ```dart import 'package:dart_logging/dart_logging.dart'; @@ -23,6 +29,94 @@ void main() { } ``` -## Part of dart_node +## Core Concepts + +### Logging Context + +Create a logging context with one or more transports: + +```dart +final context = createLoggingContext( + transports: [logTransport(logToConsole)], +); +``` + +### Log Levels + +Standard log levels are available (from lowest to highest severity): + +```dart +logger.trace('Very detailed trace info'); +logger.debug('Debugging info'); +logger.info('Information'); +logger.warn('Warning'); +logger.error('Error occurred'); +logger.fatal('Fatal error'); +``` + +### Structured Data + +Pass structured data with log messages: + +```dart +logger.info('User logged in', structuredData: {'userId': 123, 'email': 'user@example.com'}); +``` + +### Child Loggers + +Create child loggers that inherit and extend context: + +```dart +final requestLogger = logger.child({'requestId': 'abc-123'}); +requestLogger.info('Start'); // Includes requestId + +final userLogger = requestLogger.child({'userId': 456}); +userLogger.info('Action'); // Includes both requestId and userId +``` + +This is useful for adding context that applies to a scope (like a request handler). + +### Custom Transports + +Create custom transports to send logs to different destinations: + +```dart +void myTransport(LogEntry entry) { + // Send to external service, file, etc. + print('${entry.level}: ${entry.message}'); +} + +final context = createLoggingContext( + transports: [logTransport(myTransport)], +); +``` + +## Example: Express Server Logging + +```dart +import 'dart:js_interop'; +import 'package:dart_node_express/dart_node_express.dart'; +import 'package:dart_logging/dart_logging.dart'; + +void main() { + final logger = createLoggerWithContext( + createLoggingContext(transports: [logTransport(logToConsole)]), + ); + + final app = express(); + + app.use(middleware((req, res, next) { + final reqLogger = logger.child({'path': req.path, 'method': req.method}); + reqLogger.info('Request received'); + next(); + })); + + app.listen(3000, () { + logger.info('Server started', structuredData: {'port': 3000}); + }.toJS); +} +``` + +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_logging). diff --git a/packages/dart_logging/README_zh.md b/packages/dart_logging/README_zh.md new file mode 100644 index 0000000..b692868 --- /dev/null +++ b/packages/dart_logging/README_zh.md @@ -0,0 +1,122 @@ + +Pino 风格的结构化日志,支持子日志器。提供具有自动上下文继承的分层日志记录。 + +## 安装 + +```yaml +dependencies: + dart_logging: ^0.11.0-beta +``` + +## 快速开始 + +```dart +import 'package:dart_logging/dart_logging.dart'; + +void main() { + final context = createLoggingContext( + transports: [logTransport(logToConsole)], + ); + final logger = createLoggerWithContext(context); + + logger.info('Hello world'); + logger.warn('Something might be wrong'); + logger.error('Something went wrong'); + + // 具有继承上下文的子日志器 + final childLogger = logger.child({'requestId': 'abc-123'}); + childLogger.info('Processing request'); // requestId 自动包含 +} +``` + +## 核心概念 + +### 日志上下文 + +使用一个或多个传输创建日志上下文: + +```dart +final context = createLoggingContext( + transports: [logTransport(logToConsole)], +); +``` + +### 日志级别 + +提供标准日志级别(从最低到最高严重性): + +```dart +logger.trace('Very detailed trace info'); +logger.debug('Debugging info'); +logger.info('Information'); +logger.warn('Warning'); +logger.error('Error occurred'); +logger.fatal('Fatal error'); +``` + +### 结构化数据 + +在日志消息中传递结构化数据: + +```dart +logger.info('User logged in', structuredData: {'userId': 123, 'email': 'user@example.com'}); +``` + +### 子日志器 + +创建继承并扩展上下文的子日志器: + +```dart +final requestLogger = logger.child({'requestId': 'abc-123'}); +requestLogger.info('Start'); // 包含 requestId + +final userLogger = requestLogger.child({'userId': 456}); +userLogger.info('Action'); // 同时包含 requestId 和 userId +``` + +这对于添加适用于某个作用域(如请求处理程序)的上下文非常有用。 + +### 自定义传输 + +创建自定义传输以将日志发送到不同目的地: + +```dart +void myTransport(LogEntry entry) { + // 发送到外部服务、文件等 + print('${entry.level}: ${entry.message}'); +} + +final context = createLoggingContext( + transports: [logTransport(myTransport)], +); +``` + +## 示例:Express 服务器日志 + +```dart +import 'dart:js_interop'; +import 'package:dart_node_express/dart_node_express.dart'; +import 'package:dart_logging/dart_logging.dart'; + +void main() { + final logger = createLoggerWithContext( + createLoggingContext(transports: [logTransport(logToConsole)]), + ); + + final app = express(); + + app.use(middleware((req, res, next) { + final reqLogger = logger.child({'path': req.path, 'method': req.method}); + reqLogger.info('Request received'); + next(); + })); + + app.listen(3000, () { + logger.info('Server started', structuredData: {'port': 3000}); + }.toJS); +} +``` + +## 源代码 + +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_logging) 上获取。 diff --git a/packages/dart_node_better_sqlite3/README.md b/packages/dart_node_better_sqlite3/README.md index efe21fe..1882ef8 100644 --- a/packages/dart_node_better_sqlite3/README.md +++ b/packages/dart_node_better_sqlite3/README.md @@ -1,8 +1,21 @@ -# dart_node_better_sqlite3 -Typed Dart bindings for better-sqlite3. Synchronous SQLite3 with WAL mode. +Typed Dart bindings for [better-sqlite3](https://github.com/WiseLibs/better-sqlite3). Provides synchronous SQLite3 access with WAL mode support for Node.js applications. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_better_sqlite3: ^0.11.0-beta + nadz: ^0.9.0 +``` + +Also install the npm package: + +```bash +npm install better-sqlite3 +``` + +## Quick Start ```dart import 'package:dart_node_better_sqlite3/dart_node_better_sqlite3.dart'; @@ -35,14 +48,80 @@ void main() { } ``` -## Run +## Core Concepts + +### Opening a Database + +```dart +final db = switch (openDatabase('./my.db')) { + Success(:final value) => value, + Error(:final error) => throw Exception(error), +}; +``` + +Options can be passed for read-only mode, memory databases, etc. + +### Executing SQL + +For statements that don't return data: + +```dart +db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'); +db.exec('DROP TABLE IF EXISTS temp'); +``` + +### Prepared Statements + +For parameterized queries: + +```dart +final stmt = switch (db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')) { + Success(:final value) => value, + Error(:final error) => throw Exception(error), +}; + +stmt.run(['Alice', 'alice@example.com']); +stmt.run(['Bob', 'bob@example.com']); +``` + +### Querying Data + +```dart +final query = switch (db.prepare('SELECT * FROM users WHERE id = ?')) { + Success(:final value) => value, + Error(:final error) => throw Exception(error), +}; + +// Get single row +final row = query.get([1]); + +// Get all rows +final allRows = query.all([]); +``` + +### Transactions + +```dart +db.exec('BEGIN'); +try { + // Multiple operations... + db.exec('COMMIT'); +} catch (e) { + db.exec('ROLLBACK'); + rethrow; +} +``` + +## Compile and Run ```bash -npm install better-sqlite3 +# Compile Dart to JavaScript dart compile js -o app.js lib/main.dart + +# Run with Node.js node app.js ``` -## Part of dart_node +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_better_sqlite3). diff --git a/website/src/docs/sqlite/index.md b/packages/dart_node_better_sqlite3/README_zh.md similarity index 63% rename from website/src/docs/sqlite/index.md rename to packages/dart_node_better_sqlite3/README_zh.md index 178411c..e67916b 100644 --- a/website/src/docs/sqlite/index.md +++ b/packages/dart_node_better_sqlite3/README_zh.md @@ -1,30 +1,21 @@ ---- -layout: layouts/docs.njk -title: dart_node_better_sqlite3 -description: Typed Dart bindings for better-sqlite3. Synchronous SQLite3 with WAL mode for Node.js. -eleventyNavigation: - key: dart_node_better_sqlite3 - parent: Packages - order: 6 ---- -Typed Dart bindings for [better-sqlite3](https://github.com/WiseLibs/better-sqlite3). Provides synchronous SQLite3 access with WAL mode support for Node.js applications. +[better-sqlite3](https://github.com/WiseLibs/better-sqlite3) 的类型化 Dart 绑定。为 Node.js 应用程序提供支持 WAL 模式的同步 SQLite3 访问。 -## Installation +## 安装 ```yaml dependencies: - dart_node_better_sqlite3: ^0.2.0 + dart_node_better_sqlite3: ^0.11.0-beta nadz: ^0.9.0 ``` -Also install the npm package: +通过 npm 安装: ```bash npm install better-sqlite3 ``` -## Quick Start +## 快速开始 ```dart import 'package:dart_node_better_sqlite3/dart_node_better_sqlite3.dart'; @@ -57,9 +48,9 @@ void main() { } ``` -## Core Concepts +## 核心概念 -### Opening a Database +### 打开数据库 ```dart final db = switch (openDatabase('./my.db')) { @@ -68,20 +59,20 @@ final db = switch (openDatabase('./my.db')) { }; ``` -Options can be passed for read-only mode, memory databases, etc. +可以传递选项用于只读模式、内存数据库等。 -### Executing SQL +### 执行 SQL -For statements that don't return data: +对于不返回数据的语句: ```dart db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'); db.exec('DROP TABLE IF EXISTS temp'); ``` -### Prepared Statements +### 预处理语句 -For parameterized queries: +用于参数化查询: ```dart final stmt = switch (db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')) { @@ -93,7 +84,7 @@ stmt.run(['Alice', 'alice@example.com']); stmt.run(['Bob', 'bob@example.com']); ``` -### Querying Data +### 查询数据 ```dart final query = switch (db.prepare('SELECT * FROM users WHERE id = ?')) { @@ -101,19 +92,19 @@ final query = switch (db.prepare('SELECT * FROM users WHERE id = ?')) { Error(:final error) => throw Exception(error), }; -// Get single row +// 获取单行 final row = query.get([1]); -// Get all rows +// 获取所有行 final allRows = query.all([]); ``` -### Transactions +### 事务 ```dart db.exec('BEGIN'); try { - // Multiple operations... + // 多个操作... db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); @@ -121,16 +112,16 @@ try { } ``` -## Compile and Run +## 编译和运行 ```bash -# Compile Dart to JavaScript +# 将 Dart 编译为 JavaScript dart compile js -o app.js lib/main.dart -# Run with Node.js +# 使用 Node.js 运行 node app.js ``` -## Source Code +## 源代码 -The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_better_sqlite3). +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_better_sqlite3) 上获取。 diff --git a/packages/dart_node_better_sqlite3/pubspec.lock b/packages/dart_node_better_sqlite3/pubspec.lock index 75b8a68..4ee964c 100644 --- a/packages/dart_node_better_sqlite3/pubspec.lock +++ b/packages/dart_node_better_sqlite3/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_node_core" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" description: diff --git a/packages/dart_node_core/README.md b/packages/dart_node_core/README.md index 81669c4..a7b4a82 100644 --- a/packages/dart_node_core/README.md +++ b/packages/dart_node_core/README.md @@ -1,24 +1,93 @@ -# dart_node_core -Core JS interop utilities for Dart-to-JavaScript compilation. +`dart_node_core` is the foundation layer that all other dart_node packages build upon. It provides low-level JavaScript interop utilities, Node.js bindings, and console helpers. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_core: ^0.11.0-beta +``` + +## Core Utilities + +### Console Logging + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +void main() { + consoleLog('Hello, world!'); // stdout + consoleError('Something went wrong'); // stderr +} +``` + +### Requiring Node.js Modules ```dart import 'package:dart_node_core/dart_node_core.dart'; void main() { - // Require a Node.js module + // Load a Node.js built-in module final fs = requireModule('fs'); - // Convert Dart values to JS + // Load an npm package + final express = requireModule('express'); +} +``` + +### Accessing Global Objects + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +void main() { + // Access global JavaScript objects + final process = getGlobal('process'); +} +``` + +## Interop Helpers + +### Converting Between Dart and JavaScript + +Uses `dart:js_interop` for type-safe conversions: + +```dart +import 'dart:js_interop'; + +void main() { + // Dart to JS final jsString = 'hello'.toJS; + final jsNumber = 42.toJS; + final jsList = [1, 2, 3].jsify(); + + // JS to Dart + final dartString = jsString.toDart; +} +``` + +## FP Extensions + +Functional programming utilities: + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +String? getName() => 'World'; + +void main() { + // Pattern match on nullable values + String? name = getName(); + final result = name.match( + some: (n) => 'Hello, $n', + none: () => 'No name provided', + ); - // Work with JS objects - final result = fs.callMethod('readFileSync'.toJS, ['./file.txt'.toJS]); + // Apply transformations + final length = 'hello'.let((s) => s.length); } ``` -## Part of dart_node +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_core). diff --git a/packages/dart_node_core/README_zh.md b/packages/dart_node_core/README_zh.md new file mode 100644 index 0000000..80e2821 --- /dev/null +++ b/packages/dart_node_core/README_zh.md @@ -0,0 +1,93 @@ + +`dart_node_core` 是所有其他 dart_node 包的基础层。它提供底层 JavaScript 互操作工具、Node.js 绑定和控制台辅助功能。 + +## 安装 + +```yaml +dependencies: + dart_node_core: ^0.11.0-beta +``` + +## 核心工具 + +### 控制台日志 + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +void main() { + consoleLog('Hello, world!'); // 标准输出 + consoleError('Something went wrong'); // 标准错误输出 +} +``` + +### 加载 Node.js 模块 + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +void main() { + // 加载 Node.js 内置模块 + final fs = requireModule('fs'); + + // 加载 npm 包 + final express = requireModule('express'); +} +``` + +### 访问全局对象 + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +void main() { + // 访问全局 JavaScript 对象 + final process = getGlobal('process'); +} +``` + +## 互操作辅助工具 + +### Dart 和 JavaScript 之间的转换 + +使用 `dart:js_interop` 进行类型安全转换: + +```dart +import 'dart:js_interop'; + +void main() { + // Dart 转 JS + final jsString = 'hello'.toJS; + final jsNumber = 42.toJS; + final jsList = [1, 2, 3].jsify(); + + // JS 转 Dart + final dartString = jsString.toDart; +} +``` + +## 函数式编程扩展 + +函数式编程工具: + +```dart +import 'package:dart_node_core/dart_node_core.dart'; + +String? getName() => 'World'; + +void main() { + // 对可空值进行模式匹配 + String? name = getName(); + final result = name.match( + some: (n) => 'Hello, $n', + none: () => 'No name provided', + ); + + // 应用转换 + final length = 'hello'.let((s) => s.length); +} +``` + +## 源代码 + +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_core) 上获取。 diff --git a/packages/dart_node_coverage/README_zh.md b/packages/dart_node_coverage/README_zh.md new file mode 100644 index 0000000..ae1b6f9 --- /dev/null +++ b/packages/dart_node_coverage/README_zh.md @@ -0,0 +1,28 @@ +# dart_node_coverage + +用于使用 dart2js 编译并在 Node.js 中执行的 Dart 代码的代码覆盖率收集工具。 + +## 架构 + +此包提供 Dart 源代码的编译时插桩功能,以便在通过 dart2js 在 Node.js 中运行测试时启用行覆盖率跟踪。 + +详细架构文档请参阅 [lib/src/architecture.dart](lib/src/architecture.dart)。 + +## 主要功能 + +- **编译时插桩**:在 dart2js 编译前插入覆盖率探针 +- **LCOV 输出**:与 genhtml、coveralls 等兼容的标准格式 +- **与 dart test 集成**:与现有测试工作流程配合使用 +- **禁用时零运行时开销**:无插桩则无成本 + +## 工作原理 + +1. **分析** Dart 源代码以识别可执行行 +2. **插桩** 源代码,插入覆盖率探针调用 +3. **编译** 使用 dart2js 编译插桩后的源代码 +4. **执行** 在 Node.js 中运行测试(自动收集覆盖率) +5. **生成** 从覆盖率数据生成 LCOV 报告 + +## 状态 + +此包处于早期开发阶段。架构已定义,实现正在进行中。 diff --git a/packages/dart_node_express/README.md b/packages/dart_node_express/README.md index 333a839..d075b82 100644 --- a/packages/dart_node_express/README.md +++ b/packages/dart_node_express/README.md @@ -1,32 +1,326 @@ # dart_node_express -Express.js bindings for Dart. Build Node.js HTTP servers entirely in Dart. +Type-safe Express.js bindings for Dart. Build HTTP servers and REST APIs entirely in Dart. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_express: ^0.11.0-beta +``` + +Also install Express via npm: + +```bash +npm install express +``` + +## Quick Start ```dart +import 'dart:js_interop'; import 'package:dart_node_express/dart_node_express.dart'; void main() { final app = express(); - app.get('/', (req, res) { - res.send('Hello from Dart!'); - }); + app.get('/', handler((req, res) { + res.send('Hello, Dart!'); + })); app.listen(3000, () { - print('Server running on http://localhost:3000'); + print('Server running on port 3000'); + }.toJS); +} +``` + +## Routing + +### Basic Routes + +```dart +app.get('/users', handler((req, res) { + res.jsonMap({'users': []}); +})); + +app.post('/users', handler((req, res) { + final body = req.body; + res.status(201); + res.jsonMap({'created': true}); +})); + +app.put('/users/:id', handler((req, res) { + final id = req.params['id']; + res.jsonMap({'updated': id}); +})); + +app.delete('/users/:id', handler((req, res) { + res.status(204); + res.end(); +})); +``` + +### Route Parameters + +```dart +app.get('/users/:userId/posts/:postId', handler((req, res) { + final userId = req.params['userId']; + final postId = req.params['postId']; + + res.jsonMap({ + 'userId': userId, + 'postId': postId, + }); +})); +``` + +### Query Parameters + +```dart +app.get('/search', handler((req, res) { + final query = req.query['q']; + final page = int.tryParse(req.query['page'] ?? '1') ?? 1; + + res.jsonMap({ + 'query': query, + 'page': page, }); +})); +``` + +## Request Object + +The `Request` object provides access to incoming request data: + +```dart +app.post('/api/data', handler((req, res) { + // Request body (requires body-parsing middleware) + final body = req.body; + + // Headers + final contentType = req.headers['content-type']; + + // URL path + final path = req.path; + + // HTTP method + final method = req.method; + + // Query string parameters + final params = req.query; + + res.jsonMap({'received': body}); +})); +``` + +## Response Object + +The `Response` object provides methods for sending responses: + +```dart +// Send text +res.send('Hello!'); + +// Send JSON (for Dart Maps, use jsonMap) +res.jsonMap({'message': 'Hello!'}); + +// Set status code (separate call from response) +res.status(201); +res.jsonMap({'created': true}); + +// Set headers +res.set('X-Custom-Header', 'value'); + +// Redirect +res.redirect('/new-location'); + +// End response without body +res.status(204); +res.end(); +``` + +## Middleware + +### Custom Middleware + +```dart +app.use(middleware((req, res, next) { + print('${req.method} ${req.path}'); + next(); +})); +``` + +### Chaining Middleware + +```dart +app.use(chain([ + middleware((req, res, next) { + print('First middleware'); + next(); + }), + middleware((req, res, next) { + print('Second middleware'); + next(); + }), +])); +``` + +### Request Context + +Store and retrieve values in the request context: + +```dart +// Set context in middleware +app.use(middleware((req, res, next) { + setContext(req, 'userId', '123'); + next(); +})); + +// Get context in handler +app.get('/profile', handler((req, res) { + final userId = getContext(req, 'userId'); + res.jsonMap({'userId': userId}); +})); +``` + +## Router + +Organize routes with the Router: + +```dart +Router createUserRouter() { + final router = Router(); + + router.get('/', handler((req, res) { + res.jsonMap({'users': []}); + })); + + router.post('/', handler((req, res) { + res.status(201); + res.jsonMap({'created': true}); + })); + + router.get('/:id', handler((req, res) { + res.jsonMap({'user': req.params['id']}); + })); + + return router; +} + +void main() { + final app = express(); + + // Mount the router + final router = createUserRouter(); + app.use('/api/users', router); + + app.listen(3000); } ``` -## Run +## Async Handlers -```bash -dart compile js -o server.js lib/main.dart -node server.js +Use async handlers for database calls and other async operations: + +```dart +app.get('/users', asyncHandler((req, res) async { + final users = await database.fetchUsers(); + res.jsonMap({'users': users}); +})); +``` + +The `asyncHandler` wrapper ensures errors are properly caught and passed to error middleware. + +## Validation + +Use the schema-based validation system: + +```dart +// Define a validated data type +typedef CreateUserData = ({String name, String email, int? age}); + +// Create a schema +final createUserSchema = schema( + { + 'name': string().minLength(2).maxLength(50), + 'email': string().email(), + 'age': optional(int_().positive()), + }, + (data) => ( + name: data['name'] as String, + email: data['email'] as String, + age: data['age'] as int?, + ), +); + +// Use validation middleware +app.post('/users', validateBody(createUserSchema)); +app.post('/users', handler((req, res) { + final result = getValidatedBody(req); + switch (result) { + case Success(:final value): + res.status(201); + res.jsonMap({'name': value.name, 'email': value.email}); + case Error(:final error): + res.status(400); + res.jsonMap({'error': error}); + } +})); +``` + +### Available Validators + +```dart +// String validators +string().minLength(2).maxLength(100).notEmpty().email().alphanumeric() + +// Integer validators +int_().min(0).max(100).positive().range(1, 10) + +// Boolean validators +bool_() + +// Optional wrapper +optional(string()) +``` + +## Complete Example + +```dart +import 'dart:js_interop'; +import 'package:dart_node_express/dart_node_express.dart'; + +void main() { + final app = express(); + + // Logging middleware + app.use(middleware((req, res, next) { + print('[${DateTime.now()}] ${req.method} ${req.path}'); + next(); + })); + + // Routes + app.get('/', handler((req, res) { + res.jsonMap({ + 'name': 'My API', + 'version': '1.0.0', + }); + })); + + app.get('/health', handler((req, res) { + res.jsonMap({'status': 'ok'}); + })); + + // Mount routers + app.use('/api/users', createUserRouter()); + + // Start server + app.listen(3000, () { + print('Server running on port 3000'); + }.toJS); +} ``` -## Part of dart_node +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_express). diff --git a/packages/dart_node_express/README_zh.md b/packages/dart_node_express/README_zh.md new file mode 100644 index 0000000..5f89bf0 --- /dev/null +++ b/packages/dart_node_express/README_zh.md @@ -0,0 +1,326 @@ +# dart_node_express + +类型安全的 Express.js 绑定。完全使用 Dart 构建 HTTP 服务器和 REST API。 + +## 安装 + +```yaml +dependencies: + dart_node_express: ^0.11.0-beta +``` + +通过 npm 安装 Express: + +```bash +npm install express +``` + +## 快速开始 + +```dart +import 'dart:js_interop'; +import 'package:dart_node_express/dart_node_express.dart'; + +void main() { + final app = express(); + + app.get('/', handler((req, res) { + res.send('Hello, Dart!'); + })); + + app.listen(3000, () { + print('Server running on port 3000'); + }.toJS); +} +``` + +## 路由 + +### 基本路由 + +```dart +app.get('/users', handler((req, res) { + res.jsonMap({'users': []}); +})); + +app.post('/users', handler((req, res) { + final body = req.body; + res.status(201); + res.jsonMap({'created': true}); +})); + +app.put('/users/:id', handler((req, res) { + final id = req.params['id']; + res.jsonMap({'updated': id}); +})); + +app.delete('/users/:id', handler((req, res) { + res.status(204); + res.end(); +})); +``` + +### 路由参数 + +```dart +app.get('/users/:userId/posts/:postId', handler((req, res) { + final userId = req.params['userId']; + final postId = req.params['postId']; + + res.jsonMap({ + 'userId': userId, + 'postId': postId, + }); +})); +``` + +### 查询参数 + +```dart +app.get('/search', handler((req, res) { + final query = req.query['q']; + final page = int.tryParse(req.query['page'] ?? '1') ?? 1; + + res.jsonMap({ + 'query': query, + 'page': page, + }); +})); +``` + +## 请求对象 + +`Request` 对象提供对传入请求数据的访问: + +```dart +app.post('/api/data', handler((req, res) { + // 请求体(需要 body-parsing 中间件) + final body = req.body; + + // 请求头 + final contentType = req.headers['content-type']; + + // URL 路径 + final path = req.path; + + // HTTP 方法 + final method = req.method; + + // 查询字符串参数 + final params = req.query; + + res.jsonMap({'received': body}); +})); +``` + +## 响应对象 + +`Response` 对象提供发送响应的方法: + +```dart +// 发送文本 +res.send('Hello!'); + +// 发送 JSON(对于 Dart Map,使用 jsonMap) +res.jsonMap({'message': 'Hello!'}); + +// 设置状态码(与响应分开调用) +res.status(201); +res.jsonMap({'created': true}); + +// 设置响应头 +res.set('X-Custom-Header', 'value'); + +// 重定向 +res.redirect('/new-location'); + +// 结束响应(无响应体) +res.status(204); +res.end(); +``` + +## 中间件 + +### 自定义中间件 + +```dart +app.use(middleware((req, res, next) { + print('${req.method} ${req.path}'); + next(); +})); +``` + +### 链式中间件 + +```dart +app.use(chain([ + middleware((req, res, next) { + print('First middleware'); + next(); + }), + middleware((req, res, next) { + print('Second middleware'); + next(); + }), +])); +``` + +### 请求上下文 + +在请求上下文中存储和检索值: + +```dart +// 在中间件中设置上下文 +app.use(middleware((req, res, next) { + setContext(req, 'userId', '123'); + next(); +})); + +// 在处理程序中获取上下文 +app.get('/profile', handler((req, res) { + final userId = getContext(req, 'userId'); + res.jsonMap({'userId': userId}); +})); +``` + +## 路由器 + +使用路由器组织路由: + +```dart +Router createUserRouter() { + final router = Router(); + + router.get('/', handler((req, res) { + res.jsonMap({'users': []}); + })); + + router.post('/', handler((req, res) { + res.status(201); + res.jsonMap({'created': true}); + })); + + router.get('/:id', handler((req, res) { + res.jsonMap({'user': req.params['id']}); + })); + + return router; +} + +void main() { + final app = express(); + + // 挂载路由器 + final router = createUserRouter(); + app.use('/api/users', router); + + app.listen(3000); +} +``` + +## 异步处理程序 + +使用异步处理程序进行数据库调用和其他异步操作: + +```dart +app.get('/users', asyncHandler((req, res) async { + final users = await database.fetchUsers(); + res.jsonMap({'users': users}); +})); +``` + +`asyncHandler` 包装器确保错误被正确捕获并传递给错误中间件。 + +## 验证 + +使用基于 Schema 的验证系统: + +```dart +// 定义验证数据类型 +typedef CreateUserData = ({String name, String email, int? age}); + +// 创建 Schema +final createUserSchema = schema( + { + 'name': string().minLength(2).maxLength(50), + 'email': string().email(), + 'age': optional(int_().positive()), + }, + (data) => ( + name: data['name'] as String, + email: data['email'] as String, + age: data['age'] as int?, + ), +); + +// 使用验证中间件 +app.post('/users', validateBody(createUserSchema)); +app.post('/users', handler((req, res) { + final result = getValidatedBody(req); + switch (result) { + case Success(:final value): + res.status(201); + res.jsonMap({'name': value.name, 'email': value.email}); + case Error(:final error): + res.status(400); + res.jsonMap({'error': error}); + } +})); +``` + +### 可用验证器 + +```dart +// 字符串验证器 +string().minLength(2).maxLength(100).notEmpty().email().alphanumeric() + +// 整数验证器 +int_().min(0).max(100).positive().range(1, 10) + +// 布尔验证器 +bool_() + +// 可选包装器 +optional(string()) +``` + +## 完整示例 + +```dart +import 'dart:js_interop'; +import 'package:dart_node_express/dart_node_express.dart'; + +void main() { + final app = express(); + + // 日志中间件 + app.use(middleware((req, res, next) { + print('[${DateTime.now()}] ${req.method} ${req.path}'); + next(); + })); + + // 路由 + app.get('/', handler((req, res) { + res.jsonMap({ + 'name': 'My API', + 'version': '1.0.0', + }); + })); + + app.get('/health', handler((req, res) { + res.jsonMap({'status': 'ok'}); + })); + + // 挂载路由器 + app.use('/api/users', createUserRouter()); + + // 启动服务器 + app.listen(3000, () { + print('Server running on port 3000'); + }.toJS); +} +``` + +## 源代码 + +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_express) 上获取。 diff --git a/packages/dart_node_express/pubspec.lock b/packages/dart_node_express/pubspec.lock index 75b8a68..4ee964c 100644 --- a/packages/dart_node_express/pubspec.lock +++ b/packages/dart_node_express/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_node_core" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" description: diff --git a/packages/dart_node_mcp/README.md b/packages/dart_node_mcp/README.md index 366d2e0..3363d2d 100644 --- a/packages/dart_node_mcp/README.md +++ b/packages/dart_node_mcp/README.md @@ -1,8 +1,21 @@ -# dart_node_mcp -MCP (Model Context Protocol) server bindings for Dart on Node.js. +MCP (Model Context Protocol) server bindings for Dart on Node.js. Build AI tool servers that can be used by Claude, GPT, and other AI assistants. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_mcp: ^0.11.0-beta + nadz: ^0.9.0 +``` + +Also install the npm package: + +```bash +npm install @modelcontextprotocol/sdk +``` + +## Quick Start ```dart import 'package:dart_node_mcp/dart_node_mcp.dart'; @@ -34,11 +47,78 @@ Future main() async { } ``` -## Run +## Core Concepts + +### Server Creation + +Create an MCP server with a name and version: + +```dart +final serverResult = McpServer.create((name: 'my-server', version: '1.0.0')); +``` + +### Registering Tools + +Tools are functions that AI assistants can call. Register them with a name, description, and handler: + +```dart +server.registerTool( + 'greet', + ( + description: 'Greet a user by name', + inputSchema: { + 'type': 'object', + 'properties': { + 'name': {'type': 'string', 'description': 'Name to greet'}, + }, + 'required': ['name'], + }, + ), + (args, meta) async { + final name = args['name'] as String; + return ( + content: [(type: 'text', text: 'Hello, $name!')], + isError: false, + ); + }, +); +``` + +### Transport + +Connect to clients using stdio transport (standard for MCP): + +```dart +final transport = switch (createStdioServerTransport()) { + Success(:final value) => value, + Error(:final error) => throw Exception(error), +}; + +await server.connect(transport); +``` + +## Compile and Run ```bash +# Compile Dart to JavaScript dart compile js -o server.js lib/main.dart + +# Run with Node.js node server.js ``` -## Part of [dart_node](https://github.com/MelbourneDeveloper/dart_node) +## Use with Claude Code + +Add your MCP server to Claude Code: + +```bash +claude mcp add --transport stdio my-server -- node /path/to/server.js +``` + +## Example: Too Many Cooks + +The [Too Many Cooks](/docs/too-many-cooks/) MCP server is built with dart_node_mcp. It provides multi-agent coordination for AI assistants editing the same codebase. + +## Source Code + +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_mcp). diff --git a/website/src/docs/mcp/index.md b/packages/dart_node_mcp/README_zh.md similarity index 59% rename from website/src/docs/mcp/index.md rename to packages/dart_node_mcp/README_zh.md index 1d551fe..3a9f4a3 100644 --- a/website/src/docs/mcp/index.md +++ b/packages/dart_node_mcp/README_zh.md @@ -1,30 +1,21 @@ ---- -layout: layouts/docs.njk -title: dart_node_mcp -description: MCP (Model Context Protocol) server bindings for Dart on Node.js. Build AI tool servers in Dart. -eleventyNavigation: - key: dart_node_mcp - parent: Packages - order: 7 ---- -MCP (Model Context Protocol) server bindings for Dart on Node.js. Build AI tool servers that can be used by Claude, GPT, and other AI assistants. +适用于 Node.js 上 Dart 的 MCP(模型上下文协议)服务器绑定。构建可供 Claude、GPT 和其他 AI 助手使用的 AI 工具服务器。 -## Installation +## 安装 ```yaml dependencies: - dart_node_mcp: ^0.2.0 + dart_node_mcp: ^0.11.0-beta nadz: ^0.9.0 ``` -Also install the npm package: +通过 npm 安装: ```bash npm install @modelcontextprotocol/sdk ``` -## Quick Start +## 快速开始 ```dart import 'package:dart_node_mcp/dart_node_mcp.dart'; @@ -56,19 +47,19 @@ Future main() async { } ``` -## Core Concepts +## 核心概念 -### Server Creation +### 创建服务器 -Create an MCP server with a name and version: +使用名称和版本创建 MCP 服务器: ```dart final serverResult = McpServer.create((name: 'my-server', version: '1.0.0')); ``` -### Registering Tools +### 注册工具 -Tools are functions that AI assistants can call. Register them with a name, description, and handler: +工具是 AI 助手可以调用的函数。使用名称、描述和处理程序注册它们: ```dart server.registerTool( @@ -93,9 +84,9 @@ server.registerTool( ); ``` -### Transport +### 传输 -Connect to clients using stdio transport (standard for MCP): +使用标准输入输出传输连接到客户端(MCP 标准方式): ```dart final transport = switch (createStdioServerTransport()) { @@ -106,28 +97,28 @@ final transport = switch (createStdioServerTransport()) { await server.connect(transport); ``` -## Compile and Run +## 编译和运行 ```bash -# Compile Dart to JavaScript +# 将 Dart 编译为 JavaScript dart compile js -o server.js lib/main.dart -# Run with Node.js +# 使用 Node.js 运行 node server.js ``` -## Use with Claude Code +## 与 Claude Code 一起使用 -Add your MCP server to Claude Code: +将您的 MCP 服务器添加到 Claude Code: ```bash claude mcp add --transport stdio my-server -- node /path/to/server.js ``` -## Example: Too Many Cooks +## 示例:Too Many Cooks -The [Too Many Cooks](/docs/too-many-cooks/) MCP server is built with dart_node_mcp. It provides multi-agent coordination for AI assistants editing the same codebase. +[Too Many Cooks](/zh/docs/too-many-cooks/) MCP 服务器是使用 dart_node_mcp 构建的。它为编辑同一代码库的 AI 助手提供多智能体协调功能。 -## Source Code +## 源代码 -The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_mcp). +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_mcp) 上获取。 diff --git a/packages/dart_node_mcp/pubspec.lock b/packages/dart_node_mcp/pubspec.lock index f14fa43..7d44754 100644 --- a/packages/dart_node_mcp/pubspec.lock +++ b/packages/dart_node_mcp/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_node_core" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" description: diff --git a/packages/dart_node_react/README.md b/packages/dart_node_react/README.md index 11ed784..4c3ca6e 100644 --- a/packages/dart_node_react/README.md +++ b/packages/dart_node_react/README.md @@ -1,35 +1,436 @@ # dart_node_react -React bindings for Dart. Build React web apps entirely in Dart. +Type-safe React bindings for building web applications in Dart. If you know React, you'll feel right at home. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_react: ^0.11.0-beta +``` + +Also install React via npm: + +```bash +npm install react react-dom +``` + +## Quick Start ```dart import 'package:dart_node_react/dart_node_react.dart'; +ReactElement app() { + return div( + className: 'app', + children: [ + h1(children: [text('Hello, Dart!')]), + p(children: [text('Welcome to React with Dart.')]), + ], + ); +} + void main() { - final app = div( - props: {'className': 'app'}, + final container = document.getElementById('root'); + final root = ReactDOM.createRoot(container); + root.render(app()); +} +``` + +## Components + +### Functional Components + +```dart +ReactElement greeting({required String name}) { + return div( + className: 'greeting', + children: [ + text('Hello, $name!'), + ], + ); +} + +// Usage +greeting(name: 'World'); +``` + +### Components with Props + +```dart +ReactElement userCard({ + required String name, + required String email, + String? avatarUrl, +}) { + return div( + className: 'user-card', + children: [ + avatarUrl != null + ? img(src: avatarUrl, alt: name) + : div(className: 'avatar-placeholder'), + h2(children: [text(name)]), + p(children: [text(email)]), + ], + ); +} +``` + +## Hooks + +### useState + +Returns a `StateHook` with `.value`, `.set()`, and `.setWithUpdater()`: + +```dart +ReactElement counter() { + final count = useState(0); + + return div(children: [ + p(children: [text('Count: ${count.value}')]), + button( + onClick: (_) => count.setWithUpdater((c) => c + 1), + children: [text('Increment')], + ), + button( + onClick: (_) => count.setWithUpdater((c) => c - 1), + children: [text('Decrement')], + ), + ]); +} +``` + +### useStateLazy + +For expensive initial state computation: + +```dart +final data = useStateLazy(() => expensiveComputation()); +``` + +### useEffect + +```dart +ReactElement timer() { + final seconds = useState(0); + + useEffect(() { + final timer = Timer.periodic(Duration(seconds: 1), (_) { + seconds.setWithUpdater((s) => s + 1); + }); + + // Cleanup function + return () => timer.cancel(); + }, []); // Empty deps = run once on mount + + return p(children: [text('Seconds: ${seconds.value}')]); +} +``` + +### useLayoutEffect + +Synchronous version of useEffect that runs before screen updates: + +```dart +useLayoutEffect(() { + // DOM measurements + return () { /* cleanup */ }; +}, [dependency]); +``` + +### useRef + +```dart +ReactElement focusInput() { + final inputRef = useRef(null); + + void handleClick() { + inputRef.current?.focus(); + } + + return div(children: [ + input(ref: inputRef, type: 'text'), + button( + onClick: (_) => handleClick(), + children: [text('Focus Input')], + ), + ]); +} +``` + +### useMemo + +```dart +ReactElement expensiveList({required List numbers}) { + final count = useState(0); + + // Only recalculate when count.value changes + final fib = useMemo( + () => fibonacci(count.value), + [count.value], + ); + + return div(children: [ + p(children: [text('Fibonacci of ${count.value} is $fib')]), + ]); +} +``` + +### useCallback + +```dart +ReactElement searchBox({required void Function(String) onSearch}) { + final query = useState(''); + + // Memoize the callback + final handleSubmit = useCallback( + () => onSearch(query.value), + [query.value, onSearch], + ); + + return form( + onSubmit: (_) => handleSubmit(), children: [ - h1(children: ['Hello from Dart!']), - button( - props: {'onClick': () => print('Clicked!')}, - children: ['Click me'], + input( + value: query.value, + onChange: (e) => query.set(e.target.value), ), + button(type: 'submit', children: [text('Search')]), ], ); +} +``` + +### useDebugValue + +Display custom labels in React DevTools: + +```dart +useDebugValue( + isOnline.value, + (isOnline) => isOnline ? 'Online' : 'Not Online', +); +``` + +## Elements + +### HTML Elements - render(app, querySelector('#root')); +```dart +// Divs and spans +div(className: 'container', children: [...]) +span(className: 'highlight', children: [...]) + +// Headings +h1(children: [text('Title')]) +h2(children: [text('Subtitle')]) + +// Paragraphs and text +p(children: [text('Some text')]) +text('Raw text content') + +// Links +a(href: 'https://example.com', children: [text('Click me')]) + +// Images +img(src: '/image.png', alt: 'Description') + +// Forms +form(onSubmit: handleSubmit, children: [...]) +input(type: 'text', value: value, onChange: handleChange) +button(type: 'submit', children: [text('Submit')]) +``` + +### Lists + +```dart +ReactElement todoList({required List todos}) { + return ul( + className: 'todo-list', + children: todos.map((todo) => + li( + key: todo.id, + children: [ + input( + type: 'checkbox', + checked: todo.completed, + ), + text(todo.title), + ], + ) + ).toList(), + ); } ``` -## Run +### Conditional Rendering -```bash -dart compile js -o app.js lib/main.dart -# Serve with your preferred static server +```dart +ReactElement userStatus({required User? user}) { + return div(children: [ + user != null + ? span(children: [text('Welcome, ${user.name}!')]) + : span(children: [text('Please log in')]), + ]); +} +``` + +## Event Handling + +```dart +ReactElement interactiveButton() { + void handleClick(MouseEvent e) { + print('Button clicked at (${e.clientX}, ${e.clientY})'); + } + + void handleMouseEnter(MouseEvent e) { + print('Mouse entered'); + } + + return button( + onClick: handleClick, + onMouseEnter: handleMouseEnter, + children: [text('Hover and Click Me')], + ); +} +``` + +### Form Events + +```dart +ReactElement loginForm() { + final email = useState(''); + final password = useState(''); + + void handleSubmit(Event e) { + e.preventDefault(); + print('Login: ${email.value} / ${password.value}'); + } + + return form( + onSubmit: handleSubmit, + children: [ + input( + type: 'email', + value: email.value, + onChange: (e) => email.set(e.target.value), + placeholder: 'Email', + ), + input( + type: 'password', + value: password.value, + onChange: (e) => password.set(e.target.value), + placeholder: 'Password', + ), + button(type: 'submit', children: [text('Log In')]), + ], + ); +} +``` + +## Styling + +### Inline Styles + +```dart +div( + style: { + 'backgroundColor': '#f0f0f0', + 'padding': '1rem', + 'borderRadius': '8px', + }, + children: [...], +) +``` + +### CSS Classes + +```dart +div( + className: 'card card-primary', + children: [...], +) +``` + +## Complete Example + +```dart +import 'package:dart_node_react/dart_node_react.dart'; + +ReactElement todoApp() { + final todos = useState>([]); + final input = useState(''); + + void addTodo() { + if (input.value.trim().isEmpty) return; + + todos.setWithUpdater((prev) => [ + ...prev, + Todo(id: DateTime.now().toString(), title: input.value, completed: false), + ]); + input.set(''); + } + + void toggleTodo(String id) { + todos.setWithUpdater((prev) => prev.map((todo) => + todo.id == id + ? Todo(id: todo.id, title: todo.title, completed: !todo.completed) + : todo + ).toList()); + } + + return div( + className: 'todo-app', + children: [ + h1(children: [text('Todo List')]), + + form( + onSubmit: (e) { + e.preventDefault(); + addTodo(); + }, + children: [ + input( + value: input.value, + onChange: (e) => input.set(e.target.value), + placeholder: 'What needs to be done?', + ), + button(type: 'submit', children: [text('Add')]), + ], + ), + + ul( + children: todos.value.map((todo) => + li( + key: todo.id, + className: todo.completed ? 'completed' : '', + onClick: (_) => toggleTodo(todo.id), + children: [text(todo.title)], + ) + ).toList(), + ), + + p(children: [ + text('${todos.value.where((t) => !t.completed).length} items left'), + ]), + ], + ); +} + +class Todo { + final String id; + final String title; + final bool completed; + + Todo({required this.id, required this.title, required this.completed}); +} + +void main() { + final root = ReactDOM.createRoot(document.getElementById('root')); + root.render(todoApp()); +} ``` -## Part of dart_node +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_react). diff --git a/website/src/docs/react/index.md b/packages/dart_node_react/README_zh.md similarity index 65% rename from website/src/docs/react/index.md rename to packages/dart_node_react/README_zh.md index 320f487..b5ec51a 100644 --- a/website/src/docs/react/index.md +++ b/packages/dart_node_react/README_zh.md @@ -1,29 +1,21 @@ ---- -layout: layouts/docs.njk -title: dart_node_react -description: React bindings for building web applications in Dart with hooks, components, and JSX-like syntax. -eleventyNavigation: - key: dart_node_react - parent: Packages - order: 3 ---- +# dart_node_react -`dart_node_react` provides type-safe React bindings for building web applications in Dart. If you know React, you'll feel right at home. +类型安全的 React 绑定,用于在 Dart 中构建 Web 应用程序。如果您熟悉 React,您会感到非常亲切。 -## Installation +## 安装 ```yaml dependencies: - dart_node_react: ^0.2.0 + dart_node_react: ^0.11.0-beta ``` -Also install React via npm: +通过 npm 安装 React: ```bash npm install react react-dom ``` -## Quick Start +## 快速开始 ```dart import 'package:dart_node_react/dart_node_react.dart'; @@ -45,9 +37,9 @@ void main() { } ``` -## Components +## 组件 -### Functional Components +### 函数组件 ```dart ReactElement greeting({required String name}) { @@ -59,11 +51,11 @@ ReactElement greeting({required String name}) { ); } -// Usage +// 使用方式 greeting(name: 'World'); ``` -### Components with Props +### 带 Props 的组件 ```dart ReactElement userCard({ @@ -88,43 +80,64 @@ ReactElement userCard({ ### useState +返回包含 `.value`、`.set()` 和 `.setWithUpdater()` 的 `StateHook`: + ```dart ReactElement counter() { - final (count, setCount) = useState(0); + final count = useState(0); return div(children: [ - p(children: [text('Count: $count')]), + p(children: [text('Count: ${count.value}')]), button( - onClick: (_) => setCount((c) => c + 1), + onClick: (_) => count.setWithUpdater((c) => c + 1), children: [text('Increment')], ), button( - onClick: (_) => setCount((c) => c - 1), + onClick: (_) => count.setWithUpdater((c) => c - 1), children: [text('Decrement')], ), ]); } ``` +### useStateLazy + +用于昂贵的初始状态计算: + +```dart +final data = useStateLazy(() => expensiveComputation()); +``` + ### useEffect ```dart ReactElement timer() { - final (seconds, setSeconds) = useState(0); + final seconds = useState(0); useEffect(() { final timer = Timer.periodic(Duration(seconds: 1), (_) { - setSeconds((s) => s + 1); + seconds.setWithUpdater((s) => s + 1); }); - // Cleanup function + // 清理函数 return () => timer.cancel(); - }, []); // Empty deps = run once on mount + }, []); // 空依赖数组 = 仅在挂载时运行一次 - return p(children: [text('Seconds: $seconds')]); + return p(children: [text('Seconds: ${seconds.value}')]); } ``` +### useLayoutEffect + +useEffect 的同步版本,在屏幕更新前运行: + +```dart +useLayoutEffect(() { + // DOM 测量 + return () { /* 清理 */ }; +}, [dependency]); +``` + ### useRef ```dart @@ -149,15 +162,17 @@ ReactElement focusInput() { ```dart ReactElement expensiveList({required List numbers}) { - // Only recalculate when numbers changes - final sorted = useMemo( - () => numbers.toList()..sort(), - [numbers], - ); + final count = useState(0); - return ul( - children: sorted.map((n) => li(children: [text('$n')])).toList(), + // 仅当 count.value 变化时重新计算 + final fib = useMemo( + () => fibonacci(count.value), + [count.value], ); + + return div(children: [ + p(children: [text('Fibonacci of ${count.value} is $fib')]), + ]); } ``` @@ -165,20 +180,20 @@ ReactElement expensiveList({required List numbers}) { ```dart ReactElement searchBox({required void Function(String) onSearch}) { - final (query, setQuery) = useState(''); + final query = useState(''); - // Memoize the callback + // 记忆化回调 final handleSubmit = useCallback( - () => onSearch(query), - [query, onSearch], + () => onSearch(query.value), + [query.value, onSearch], ); return form( onSubmit: (_) => handleSubmit(), children: [ input( - value: query, - onChange: (e) => setQuery(e.target.value), + value: query.value, + onChange: (e) => query.set(e.target.value), ), button(type: 'submit', children: [text('Search')]), ], @@ -186,36 +201,47 @@ ReactElement searchBox({required void Function(String) onSearch}) { } ``` -## Elements +### useDebugValue + +在 React DevTools 中显示自定义标签: + +```dart +useDebugValue( + isOnline.value, + (isOnline) => isOnline ? 'Online' : 'Not Online', +); +``` + +## 元素 -### HTML Elements +### HTML 元素 ```dart -// Divs and spans +// Div 和 span div(className: 'container', children: [...]) span(className: 'highlight', children: [...]) -// Headings +// 标题 h1(children: [text('Title')]) h2(children: [text('Subtitle')]) -// Paragraphs and text +// 段落和文本 p(children: [text('Some text')]) text('Raw text content') -// Links +// 链接 a(href: 'https://example.com', children: [text('Click me')]) -// Images +// 图片 img(src: '/image.png', alt: 'Description') -// Forms +// 表单 form(onSubmit: handleSubmit, children: [...]) input(type: 'text', value: value, onChange: handleChange) button(type: 'submit', children: [text('Submit')]) ``` -### Lists +### 列表 ```dart ReactElement todoList({required List todos}) { @@ -237,7 +263,7 @@ ReactElement todoList({required List todos}) { } ``` -### Conditional Rendering +### 条件渲染 ```dart ReactElement userStatus({required User? user}) { @@ -249,7 +275,7 @@ ReactElement userStatus({required User? user}) { } ``` -## Event Handling +## 事件处理 ```dart ReactElement interactiveButton() { @@ -269,16 +295,16 @@ ReactElement interactiveButton() { } ``` -### Form Events +### 表单事件 ```dart ReactElement loginForm() { - final (email, setEmail) = useState(''); - final (password, setPassword) = useState(''); + final email = useState(''); + final password = useState(''); void handleSubmit(Event e) { e.preventDefault(); - print('Login: $email / $password'); + print('Login: ${email.value} / ${password.value}'); } return form( @@ -286,14 +312,14 @@ ReactElement loginForm() { children: [ input( type: 'email', - value: email, - onChange: (e) => setEmail(e.target.value), + value: email.value, + onChange: (e) => email.set(e.target.value), placeholder: 'Email', ), input( type: 'password', - value: password, - onChange: (e) => setPassword(e.target.value), + value: password.value, + onChange: (e) => password.set(e.target.value), placeholder: 'Password', ), button(type: 'submit', children: [text('Log In')]), @@ -302,9 +328,9 @@ ReactElement loginForm() { } ``` -## Styling +## 样式 -### Inline Styles +### 内联样式 ```dart div( @@ -317,7 +343,7 @@ div( ) ``` -### CSS Classes +### CSS 类 ```dart div( @@ -326,27 +352,27 @@ div( ) ``` -## Complete Example +## 完整示例 ```dart import 'package:dart_node_react/dart_node_react.dart'; ReactElement todoApp() { - final (todos, setTodos) = useState>([]); - final (input, setInput) = useState(''); + final todos = useState>([]); + final input = useState(''); void addTodo() { - if (input.trim().isEmpty) return; + if (input.value.trim().isEmpty) return; - setTodos((prev) => [ + todos.setWithUpdater((prev) => [ ...prev, - Todo(id: DateTime.now().toString(), title: input, completed: false), + Todo(id: DateTime.now().toString(), title: input.value, completed: false), ]); - setInput(''); + input.set(''); } void toggleTodo(String id) { - setTodos((prev) => prev.map((todo) => + todos.setWithUpdater((prev) => prev.map((todo) => todo.id == id ? Todo(id: todo.id, title: todo.title, completed: !todo.completed) : todo @@ -365,8 +391,8 @@ ReactElement todoApp() { }, children: [ input( - value: input, - onChange: (e) => setInput(e.target.value), + value: input.value, + onChange: (e) => input.set(e.target.value), placeholder: 'What needs to be done?', ), button(type: 'submit', children: [text('Add')]), @@ -374,7 +400,7 @@ ReactElement todoApp() { ), ul( - children: todos.map((todo) => + children: todos.value.map((todo) => li( key: todo.id, className: todo.completed ? 'completed' : '', @@ -385,7 +411,7 @@ ReactElement todoApp() { ), p(children: [ - text('${todos.where((t) => !t.completed).length} items left'), + text('${todos.value.where((t) => !t.completed).length} items left'), ]), ], ); @@ -400,11 +426,11 @@ class Todo { } void main() { - final root = ReactDOM.createRoot(document.getElementById('root')!); + final root = ReactDOM.createRoot(document.getElementById('root')); root.render(todoApp()); } ``` -## API Reference +## 源代码 -See the [full API documentation](/api/dart_node_react/) for all available functions and types. +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_react) 上获取。 diff --git a/packages/dart_node_react/lib/src/children.dart b/packages/dart_node_react/lib/src/children.dart index e4766c9..13a38ef 100644 --- a/packages/dart_node_react/lib/src/children.dart +++ b/packages/dart_node_react/lib/src/children.dart @@ -47,7 +47,8 @@ external JSArray _childrenToArray(JSAny? children); /// }); /// ``` /// -/// See: https://react.dev/reference/react/Children +/// - [React.Children documentation](https://react.dev/reference/react/Children) +/// - [Children – React 中文文档](https://zh-hans.react.dev/reference/react/Children) // This class mirrors React's Children API which is a namespace with static // methods, so the lint is intentionally ignored. @@ -65,7 +66,8 @@ abstract final class Children { /// }); /// ``` /// - /// See: https://react.dev/reference/react/Children#children-map + /// - [Children.map documentation](https://react.dev/reference/react/Children#children-map) + /// - [Children.map – React 中文文档](https://zh-hans.react.dev/reference/react/Children#children-map) static List? map( JSAny? children, ReactElement Function(ReactElement child, int index) fn, @@ -95,7 +97,8 @@ abstract final class Children { /// }); /// ``` /// - /// See: https://react.dev/reference/react/Children#children-foreach + /// - [Children.forEach documentation](https://react.dev/reference/react/Children#children-foreach) + /// - [Children.forEach – React 中文文档](https://zh-hans.react.dev/reference/react/Children#children-foreach) static void forEach( JSAny? children, void Function(ReactElement child, int index) fn, @@ -117,7 +120,8 @@ abstract final class Children { /// final childCount = Children.count(props['children']); /// ``` /// - /// See: https://react.dev/reference/react/Children#children-count + /// - [Children.count documentation](https://react.dev/reference/react/Children#children-count) + /// - [Children.count – React 中文文档](https://zh-hans.react.dev/reference/react/Children#children-count) static int count(JSAny? children) => _childrenCount(children); /// Verifies that children has only one child and returns it. @@ -129,7 +133,8 @@ abstract final class Children { /// final onlyChild = Children.only(props['children']); /// ``` /// - /// See: https://react.dev/reference/react/Children#children-only + /// - [Children.only documentation](https://react.dev/reference/react/Children#children-only) + /// - [Children.only – React 中文文档](https://zh-hans.react.dev/reference/react/Children#children-only) static ReactElement only(JSAny? children) => ReactElement.fromJS(_childrenOnly(children)); @@ -144,7 +149,8 @@ abstract final class Children { /// final reversed = childArray.reversed.toList(); /// ``` /// - /// See: https://react.dev/reference/react/Children#children-toarray + /// - [Children.toArray documentation](https://react.dev/reference/react/Children#children-toarray) + /// - [Children.toArray – React 中文文档](https://zh-hans.react.dev/reference/react/Children#children-toarray) static List toArray(JSAny? children) => _childrenToArray(children).toDart .map( diff --git a/packages/dart_node_react/lib/src/context.dart b/packages/dart_node_react/lib/src/context.dart index 0b2bdb3..4990b35 100644 --- a/packages/dart_node_react/lib/src/context.dart +++ b/packages/dart_node_react/lib/src/context.dart @@ -29,7 +29,8 @@ extension type JsContext._(JSObject _) implements JSObject { /// Every Context object comes with a Provider React component that allows /// consuming components to subscribe to context changes. /// -/// See: https://reactjs.org/docs/context.html +/// - [createContext documentation](https://react.dev/reference/react/createContext) +/// - [使用 createContext 创建组件能够提供与读取的上下文(context)](https://zh-hans.react.dev/reference/react/createContext) final class Context { Context._(this._jsContext, this._defaultValue); @@ -97,7 +98,8 @@ final class Context { /// }); /// ``` /// -/// See: https://reactjs.org/docs/context.html#reactcreatecontext +/// - [createContext documentation](https://react.dev/reference/react/createContext) +/// - [使用 createContext 创建组件能够提供与读取的上下文(context)](https://zh-hans.react.dev/reference/react/createContext) Context createContext(T defaultValue) { final jsDefault = switch (defaultValue) { null => null, @@ -134,7 +136,8 @@ Context createContext(T defaultValue) { /// }); /// ``` /// -/// See: https://reactjs.org/docs/hooks-reference.html#usecontext +/// - [useContext documentation](https://react.dev/reference/react/useContext) +/// - [useContext 是一个 React Hook,可以让你读取和订阅组件中的 context](https://zh-hans.react.dev/reference/react/useContext) T useContext(Context context) { final jsValue = _reactUseContext(context.jsContext); return switch (jsValue) { diff --git a/packages/dart_node_react/lib/src/hooks.dart b/packages/dart_node_react/lib/src/hooks.dart index 8b4f206..b83c5db 100644 --- a/packages/dart_node_react/lib/src/hooks.dart +++ b/packages/dart_node_react/lib/src/hooks.dart @@ -71,7 +71,8 @@ JSAny? _toJsAny(Object? value) => (value == null) ? null : value.jsify(); /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-effect.html +/// - [useEffect documentation](https://react.dev/reference/react/useEffect) +/// - [useEffect 是一个 React Hook,它允许你将组件与外部系统同步](https://zh-hans.react.dev/reference/react/useEffect) void useEffect(Object? Function() sideEffect, [List? dependencies]) { JSAny? wrappedSideEffect() { final result = sideEffect(); @@ -110,7 +111,8 @@ void useEffect(Object? Function() sideEffect, [List? dependencies]) { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#uselayouteffect +/// - [useLayoutEffect documentation](https://react.dev/reference/react/useLayoutEffect) +/// - [useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发](https://zh-hans.react.dev/reference/react/useLayoutEffect) void useLayoutEffect( Object? Function() sideEffect, [ List? dependencies, @@ -155,7 +157,8 @@ void useLayoutEffect( /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#useref +/// - [useRef documentation](https://react.dev/reference/react/useRef) +/// - [useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值](https://zh-hans.react.dev/reference/react/useRef) Ref useRef([T? initialValue]) => useRefInit(initialValue); /// Returns a mutable [Ref] object with [Ref.current] property initialized to @@ -186,7 +189,8 @@ Ref useRef([T? initialValue]) => useRefInit(initialValue); /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#useref +/// - [useRef documentation](https://react.dev/reference/react/useRef) +/// - [useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值](https://zh-hans.react.dev/reference/react/useRef) Ref useRefInit(T initialValue) { final jsInitial = _toJsAny(initialValue); final jsRef = React.useRef(jsInitial); @@ -226,7 +230,8 @@ Ref useRefInit(T initialValue) { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#useimperativehandle +/// - [useImperativeHandle documentation](https://react.dev/reference/react/useImperativeHandle) +/// - [useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由 ref 暴露出来的句柄](https://zh-hans.react.dev/reference/react/useImperativeHandle) void useImperativeHandle( Object? ref, T Function() createHandle, [ @@ -280,7 +285,8 @@ void useImperativeHandle( /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#usememo +/// - [useMemo documentation](https://react.dev/reference/react/useMemo) +/// - [useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果](https://zh-hans.react.dev/reference/react/useMemo) T useMemo(T Function() createFunction, [List? dependencies]) { JSAny? jsCreateFunction() => _toJsAny(createFunction()); @@ -317,7 +323,8 @@ T useMemo(T Function() createFunction, [List? dependencies]) { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#usecallback +/// - [useCallback documentation](https://react.dev/reference/react/useCallback) +/// - [useCallback 是一个允许你在多次渲染中缓存函数的 React Hook](https://zh-hans.react.dev/reference/react/useCallback) JSFunction useCallback(Function callback, List dependencies) { final jsCallback = switch (callback) { final void Function() fn => fn.toJS, @@ -367,7 +374,8 @@ JSFunction useCallback(Function callback, List dependencies) { /// } /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#usedebugvalue +/// - [useDebugValue documentation](https://react.dev/reference/react/useDebugValue) +/// - [useDebugValue 是一个 React Hook,可以让你在 React 开发工具中为自定义 Hook 添加标签](https://zh-hans.react.dev/reference/react/useDebugValue) void useDebugValue(T value, [String Function(T)? format]) { final jsValue = _toJsAny(value); JSString jsFormatFn(JSAny? v) { diff --git a/packages/dart_node_react/lib/src/react.dart b/packages/dart_node_react/lib/src/react.dart index fe924fe..2a85fe9 100644 --- a/packages/dart_node_react/lib/src/react.dart +++ b/packages/dart_node_react/lib/src/react.dart @@ -12,7 +12,10 @@ import 'dart:js_interop_unsafe'; // Typed React Element Hierarchy // ============================================================================= -/// Base type for all React elements - provides type safety over raw JSObject +/// Base type for all React elements - provides type safety over raw JSObject. +/// +/// - [createElement documentation](https://react.dev/reference/react/createElement) +/// - [createElement 允许你创建一个 React 元素,它可以作为 JSX 的替代方案](https://zh-hans.react.dev/reference/react/createElement) extension type ReactElement._(JSObject _) implements JSObject, JSAny { /// Wrap a raw JSObject as a ReactElement factory ReactElement.fromJS(JSObject js) = ReactElement._; @@ -34,6 +37,9 @@ extension type ReactElement._(JSObject _) implements JSObject, JSAny { } /// React global object providing access to React's core APIs. +/// +/// - [React documentation](https://react.dev/reference/react) +/// - [本部分提供了使用 React 的详细参考文档](https://zh-hans.react.dev/reference/react) @JS('React') extension type React._(JSObject _) implements JSObject { /// Creates and returns a new React element of the given type. @@ -70,6 +76,9 @@ extension type React._(JSObject _) implements JSObject { } /// ReactDOM global object providing DOM-specific methods. +/// +/// - [ReactDOM documentation](https://react.dev/reference/react-dom/client) +/// - [react-dom/client API 允许你在客户端(浏览器)渲染 React 组件](https://zh-hans.react.dev/reference/react-dom/client) @JS('ReactDOM') extension type ReactDOM._(JSObject _) implements JSObject { /// Creates a root for displaying React components inside a DOM element. @@ -77,6 +86,9 @@ extension type ReactDOM._(JSObject _) implements JSObject { } /// A React root for rendering React elements into the DOM. +/// +/// - [createRoot documentation](https://react.dev/reference/react-dom/client/createRoot) +/// - [createRoot 允许在浏览器的 DOM 节点中创建根节点以显示 React 组件](https://zh-hans.react.dev/reference/react-dom/client/createRoot) extension type ReactRoot._(JSObject _) implements JSObject { /// Renders a React element into the root's DOM container. external void render(JSObject element); @@ -93,7 +105,10 @@ extension type Document._(JSObject _) implements JSObject { // Helper Functions // ============================================================================= -/// Create a React element (convenience wrapper) +/// Create a React element (convenience wrapper). +/// +/// - [createElement documentation](https://react.dev/reference/react/createElement) +/// - [createElement 允许你创建一个 React 元素,它可以作为 JSX 的替代方案](https://zh-hans.react.dev/reference/react/createElement) ReactElement createElement(JSAny type, [JSObject? props, JSAny? children]) => ReactElement._( (children != null) @@ -163,7 +178,8 @@ JSAny? _toJS(Object? value) => switch (value) { /// print(isValidElement('string')); // false /// ``` /// -/// See: https://react.dev/reference/react/isValidElement +/// - [isValidElement documentation](https://react.dev/reference/react/isValidElement) +/// - [isValidElement 检测参数值是否为 React 元素](https://zh-hans.react.dev/reference/react/isValidElement) bool isValidElement(JSAny? object) => React.isValidElement(object); /// Clones and returns a new React element using element as the starting point. @@ -177,7 +193,8 @@ bool isValidElement(JSAny? object) => React.isValidElement(object); /// final cloned = cloneElement(original, {'className': 'cloned'}); /// ``` /// -/// See: https://react.dev/reference/react/cloneElement +/// - [cloneElement documentation](https://react.dev/reference/react/cloneElement) +/// - [cloneElement 允许你使用一个元素作为初始值创建一个新的 React 元素](https://zh-hans.react.dev/reference/react/cloneElement) ReactElement cloneElement( ReactElement element, [ Map? props, diff --git a/packages/dart_node_react/lib/src/react_dom.dart b/packages/dart_node_react/lib/src/react_dom.dart index 2d24ac5..564c6aa 100644 --- a/packages/dart_node_react/lib/src/react_dom.dart +++ b/packages/dart_node_react/lib/src/react_dom.dart @@ -38,7 +38,8 @@ external ReactRoot _reactDomHydrateRoot( /// Portals provide a first-class way to render children into a DOM node that /// exists outside the DOM hierarchy of the parent component. /// -/// See: https://reactjs.org/docs/portals.html +/// - [Portal documentation](https://react.dev/reference/react-dom/createPortal) +/// - [Portal – React 中文文档](https://zh-hans.react.dev/reference/react-dom/createPortal) extension type ReactPortal._(JSObject _) implements JSObject { /// Creates a ReactPortal from a raw JSObject. factory ReactPortal.fromJs(JSObject jsObject) = ReactPortal._; @@ -67,7 +68,8 @@ extension type ReactPortal._(JSObject _) implements JSObject { /// root.render(myApp); /// ``` /// -/// See: https://react.dev/reference/react-dom/client/createRoot +/// - [createRoot documentation](https://react.dev/reference/react-dom/client/createRoot) +/// - [createRoot – React 中文文档](https://zh-hans.react.dev/reference/react-dom/client/createRoot) ReactRoot createRoot(JSObject container) => _reactDomCreateRoot(container); /// Creates a React root for hydrating server-rendered content (React 18+). @@ -81,7 +83,8 @@ ReactRoot createRoot(JSObject container) => _reactDomCreateRoot(container); /// final root = hydrateRoot(container, myApp); /// ``` /// -/// See: https://react.dev/reference/react-dom/client/hydrateRoot +/// - [hydrateRoot documentation](https://react.dev/reference/react-dom/client/hydrateRoot) +/// - [hydrateRoot – React 中文文档](https://zh-hans.react.dev/reference/react-dom/client/hydrateRoot) ReactRoot hydrateRoot(JSObject container, ReactElement initialChildren) => _reactDomHydrateRoot(container, initialChildren); @@ -100,7 +103,8 @@ ReactRoot hydrateRoot(JSObject container, ReactElement initialChildren) => /// ); /// ``` /// -/// See: https://react.dev/reference/react-dom/createPortal +/// - [createPortal documentation](https://react.dev/reference/react-dom/createPortal) +/// - [createPortal – React 中文文档](https://zh-hans.react.dev/reference/react-dom/createPortal) ReactPortal createPortal(ReactElement children, JSObject container) => ReactPortal.fromJs(_reactDomCreatePortal(children, container)); @@ -118,7 +122,8 @@ ReactPortal createPortal(ReactElement children, JSObject container) => /// // DOM has been updated /// ``` /// -/// See: https://react.dev/reference/react-dom/flushSync +/// - [flushSync documentation](https://react.dev/reference/react-dom/flushSync) +/// - [flushSync – React 中文文档](https://zh-hans.react.dev/reference/react-dom/flushSync) void flushSync(void Function() callback) { _reactDomFlushSync(callback.toJS); } diff --git a/packages/dart_node_react/lib/src/reducer_hook.dart b/packages/dart_node_react/lib/src/reducer_hook.dart index 5183a2c..f2181b0 100644 --- a/packages/dart_node_react/lib/src/reducer_hook.dart +++ b/packages/dart_node_react/lib/src/reducer_hook.dart @@ -17,7 +17,8 @@ external JSArray _reactUseReducer( /// - Only call Hooks at the top level. /// - Only call Hooks from inside a function component. /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#usereducer +/// - [useReducer documentation](https://react.dev/reference/react/useReducer) +/// - [useReducer – React 中文文档](https://zh-hans.react.dev/reference/react/useReducer) final class ReducerHook { ReducerHook._(this._state, this._dispatchFn); @@ -29,7 +30,8 @@ final class ReducerHook { /// The current state of the component. /// - /// See: https://reactjs.org/docs/hooks-reference.html#usereducer + /// - [useReducer documentation](https://react.dev/reference/react/useReducer) + /// - [useReducer – React 中文文档](https://zh-hans.react.dev/reference/react/useReducer) TState get state => _state; /// Dispatches [action] and triggers state changes. @@ -37,7 +39,8 @@ final class ReducerHook { /// Note: The dispatch function identity is stable and will not change on /// re-renders. /// - /// See: https://reactjs.org/docs/hooks-reference.html#usereducer + /// - [useReducer documentation](https://react.dev/reference/react/useReducer) + /// - [useReducer – React 中文文档](https://zh-hans.react.dev/reference/react/useReducer) void dispatch(TAction action) => _dispatchFn(action); } @@ -77,7 +80,8 @@ final class ReducerHook { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#usereducer +/// - [useReducer documentation](https://react.dev/reference/react/useReducer) +/// - [useReducer – React 中文文档](https://zh-hans.react.dev/reference/react/useReducer) ReducerHook useReducer( TState Function(TState state, TAction action) reducer, TState initialState, @@ -151,7 +155,8 @@ ReducerHook useReducer( /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#lazy-initialization +/// - [useReducer lazy initialization](https://react.dev/reference/react/useReducer#avoiding-recreating-the-initial-state) +/// - [useReducer – React 中文文档](https://zh-hans.react.dev/reference/react/useReducer#avoiding-recreating-the-initial-state) ReducerHook useReducerLazy( TState Function(TState state, TAction action) reducer, TInit initialArg, diff --git a/packages/dart_node_react/lib/src/ref.dart b/packages/dart_node_react/lib/src/ref.dart index 06542fa..1d813eb 100644 --- a/packages/dart_node_react/lib/src/ref.dart +++ b/packages/dart_node_react/lib/src/ref.dart @@ -22,6 +22,9 @@ final Expando _dartObjectStore = Expando('refDartValue'); /// component will be available via [current]. /// /// See [createRef] for usage examples and more info. +/// +/// - [createRef documentation](https://react.dev/reference/react/createRef) +/// - [createRef – React 中文文档](https://zh-hans.react.dev/reference/react/createRef) final class Ref { /// Creates a Ref from a JavaScript ref object. Ref._(this.jsRef); @@ -44,8 +47,8 @@ final class Ref { /// Sets the value of [current]. /// - /// See: - /// https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables + /// - [Referencing values with refs](https://react.dev/learn/referencing-values-with-refs) + /// - [使用 ref 引用值 – React 中文文档](https://zh-hans.react.dev/learn/referencing-values-with-refs) set current(T value) { if (value != null) _dartObjectStore[jsRef] = value; jsRef.current = (value == null) ? null : (value as Object).jsify(); @@ -70,5 +73,6 @@ external JsRef _reactCreateRef(); /// inputRef.current?.focus(); /// ``` /// -/// Learn more: https://reactjs.org/docs/refs-and-the-dom.html#creating-refs +/// - [createRef documentation](https://react.dev/reference/react/createRef) +/// - [createRef – React 中文文档](https://zh-hans.react.dev/reference/react/createRef) Ref createRef() => Ref.fromJs(_reactCreateRef()); diff --git a/packages/dart_node_react/lib/src/special_components.dart b/packages/dart_node_react/lib/src/special_components.dart index c60d738..cc8dd4d 100644 --- a/packages/dart_node_react/lib/src/special_components.dart +++ b/packages/dart_node_react/lib/src/special_components.dart @@ -42,7 +42,8 @@ external JSAny _reactLazy(JSFunction factory); /// createElement(Fragment, null, [child1, child2, child3]); /// ``` /// -/// See: https://reactjs.org/docs/fragments.html +/// - [Fragment documentation](https://react.dev/reference/react/Fragment) +/// - [Fragment – React 中文文档](https://zh-hans.react.dev/reference/react/Fragment) JSAny get Fragment => _reactFragment; /// Creates a Fragment element that groups children without adding extra @@ -56,7 +57,8 @@ JSAny get Fragment => _reactFragment; /// ]); /// ``` /// -/// See: https://reactjs.org/docs/fragments.html +/// - [Fragment documentation](https://react.dev/reference/react/Fragment) +/// - [Fragment – React 中文文档](https://zh-hans.react.dev/reference/react/Fragment) ReactElement fragment({List? children}) => (children != null && children.isNotEmpty) ? createElementWithChildren(_reactFragment, null, children) @@ -79,7 +81,8 @@ ReactElement fragment({List? children}) => /// ); /// ``` /// -/// See: https://reactjs.org/docs/concurrent-mode-suspense.html +/// - [Suspense documentation](https://react.dev/reference/react/Suspense) +/// - [Suspense – React 中文文档](https://zh-hans.react.dev/reference/react/Suspense) JSAny get Suspense => _reactSuspense; /// Creates a Suspense element that displays [fallback] while [child] is @@ -93,7 +96,8 @@ JSAny get Suspense => _reactSuspense; /// ); /// ``` /// -/// See: https://reactjs.org/docs/concurrent-mode-suspense.html +/// - [Suspense documentation](https://react.dev/reference/react/Suspense) +/// - [Suspense – React 中文文档](https://zh-hans.react.dev/reference/react/Suspense) ReactElement suspense({ required ReactElement fallback, ReactElement? child, @@ -124,7 +128,8 @@ ReactElement suspense({ /// createElement(StrictMode, null, appRoot); /// ``` /// -/// See: https://reactjs.org/docs/strict-mode.html +/// - [StrictMode documentation](https://react.dev/reference/react/StrictMode) +/// - [StrictMode – React 中文文档](https://zh-hans.react.dev/reference/react/StrictMode) JSAny get StrictMode => _reactStrictMode; /// Creates a StrictMode wrapper element. @@ -134,7 +139,8 @@ JSAny get StrictMode => _reactStrictMode; /// strictMode(child: myApp); /// ``` /// -/// See: https://reactjs.org/docs/strict-mode.html +/// - [StrictMode documentation](https://react.dev/reference/react/StrictMode) +/// - [StrictMode – React 中文文档](https://zh-hans.react.dev/reference/react/StrictMode) ReactElement strictMode({ReactElement? child, List? children}) => (children != null && children.isNotEmpty) ? createElementWithChildren(_reactStrictMode, null, children) @@ -172,7 +178,8 @@ typedef ForwardRefRenderFunction = /// })); /// ``` /// -/// See: https://reactjs.org/docs/forwarding-refs.html +/// - [forwardRef documentation](https://react.dev/reference/react/forwardRef) +/// - [forwardRef – React 中文文档](https://zh-hans.react.dev/reference/react/forwardRef) JSAny forwardRef2(ForwardRefRenderFunction render, {String? displayName}) { JSAny jsRender(JSObject jsProps, JSAny? jsRef) { final dartified = jsProps.dartify(); @@ -220,7 +227,8 @@ JSAny forwardRef2(ForwardRefRenderFunction render, {String? displayName}) { /// ); /// ``` /// -/// See: https://reactjs.org/docs/react-api.html#reactmemo +/// - [memo documentation](https://react.dev/reference/react/memo) +/// - [memo – React 中文文档](https://zh-hans.react.dev/reference/react/memo) JSAny memo2( JSAny component, { bool Function(Map prevProps, Map nextProps)? @@ -268,7 +276,8 @@ JSAny memo2( /// ); /// ``` /// -/// See: https://reactjs.org/docs/code-splitting.html#reactlazy +/// - [lazy documentation](https://react.dev/reference/react/lazy) +/// - [lazy – React 中文文档](https://zh-hans.react.dev/reference/react/lazy) JSAny lazy(Future Function() load) { Future jsLoad() async { final component = await load(); diff --git a/packages/dart_node_react/lib/src/state_hook.dart b/packages/dart_node_react/lib/src/state_hook.dart index a53cde1..659b99e 100644 --- a/packages/dart_node_react/lib/src/state_hook.dart +++ b/packages/dart_node_react/lib/src/state_hook.dart @@ -11,7 +11,8 @@ import 'package:dart_node_react/src/react.dart'; /// - Only call Hooks at the top level. /// - Only call Hooks from inside a function component. /// -/// Learn more: https://reactjs.org/docs/hooks-state.html +/// - [useState documentation](https://react.dev/reference/react/useState) +/// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState) final class StateHook { StateHook._(this._value, this._setValue); @@ -23,12 +24,14 @@ final class StateHook { /// The current value of the state. /// - /// See: https://reactjs.org/docs/hooks-reference.html#usestate + /// - [useState documentation](https://react.dev/reference/react/useState) + /// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState) T get value => _value; /// Updates [value] to [newValue]. /// - /// See: https://reactjs.org/docs/hooks-state.html#updating-state + /// - [setState documentation](https://react.dev/reference/react/useState#setstate) + /// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState#setstate) void set(T newValue) { final jsValue = switch (newValue) { null => null, @@ -39,7 +42,8 @@ final class StateHook { /// Updates [value] to the return value of [computeNewValue]. /// - /// See: https://reactjs.org/docs/hooks-reference.html#functional-updates + /// - [Updating state based on previous state](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state) + /// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState#updating-state-based-on-the-previous-state) void setWithUpdater(T Function(T oldValue) computeNewValue) { JSAny? updater(JSAny? oldValue) { final dartOld = switch (oldValue) { @@ -81,7 +85,8 @@ final class StateHook { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-state.html +/// - [useState documentation](https://react.dev/reference/react/useState) +/// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState) StateHook useState(T initialValue) { final jsInitial = switch (initialValue) { null => null, @@ -121,7 +126,8 @@ StateHook useState(T initialValue) { /// }); /// ``` /// -/// Learn more: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state +/// - [Avoiding recreating the initial state](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state) +/// - [useState 是一个 React Hook,它允许你向组件添加一个状态变量](https://zh-hans.react.dev/reference/react/useState#avoiding-recreating-the-initial-state) StateHook useStateLazy(T Function() init) { JSAny? jsInit() { final val = init(); diff --git a/packages/dart_node_react/lib/src/synthetic_event.dart b/packages/dart_node_react/lib/src/synthetic_event.dart index 6de299f..f57f80a 100644 --- a/packages/dart_node_react/lib/src/synthetic_event.dart +++ b/packages/dart_node_react/lib/src/synthetic_event.dart @@ -16,7 +16,8 @@ import 'dart:js_interop'; /// [stopPropagation] and [preventDefault], except the events work identically /// across all browsers. /// -/// See: https://reactjs.org/docs/events.html#syntheticevent +/// - [SyntheticEvent documentation](https://react.dev/reference/react-dom/components/common#react-event-object) +/// - [React 事件对象 – React 中文文档](https://zh-hans.react.dev/reference/react-dom/components/common#react-event-object) extension type SyntheticEvent._(JSObject _) implements JSObject { /// Creates a SyntheticEvent from a raw JSObject. factory SyntheticEvent.fromJs(JSObject jsObject) = SyntheticEvent._; @@ -71,7 +72,8 @@ extension type SyntheticEvent._(JSObject _) implements JSObject { /// A SyntheticEvent wrapper backed by a ClipboardEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent +/// - [ClipboardEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent) +/// - [ClipboardEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/ClipboardEvent) extension type SyntheticClipboardEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticClipboardEvent from a raw JSObject. factory SyntheticClipboardEvent.fromJs(JSObject jsObject) = @@ -88,7 +90,8 @@ extension type SyntheticClipboardEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a KeyboardEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent +/// - [KeyboardEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) +/// - [KeyboardEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent) extension type SyntheticKeyboardEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticKeyboardEvent from a raw JSObject. factory SyntheticKeyboardEvent.fromJs(JSObject jsObject) = @@ -139,7 +142,8 @@ extension type SyntheticKeyboardEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a CompositionEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent +/// - [CompositionEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent) +/// - [CompositionEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/CompositionEvent) extension type SyntheticCompositionEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticCompositionEvent from a raw JSObject. @@ -156,7 +160,8 @@ extension type SyntheticCompositionEvent._(JSObject _) /// A SyntheticEvent wrapper backed by a FocusEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent +/// - [FocusEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) +/// - [FocusEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/FocusEvent) extension type SyntheticFocusEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticFocusEvent from a raw JSObject. factory SyntheticFocusEvent.fromJs(JSObject jsObject) = SyntheticFocusEvent._; @@ -181,7 +186,8 @@ extension type SyntheticFormEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a MouseEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent +/// - [MouseEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +/// - [MouseEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent) extension type SyntheticMouseEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticMouseEvent from a raw JSObject. factory SyntheticMouseEvent.fromJs(JSObject jsObject) = SyntheticMouseEvent._; @@ -235,7 +241,8 @@ extension type SyntheticMouseEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a DragEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/DragEvent +/// - [DragEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent) +/// - [DragEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/DragEvent) extension type SyntheticDragEvent._(JSObject _) implements SyntheticMouseEvent { /// Creates a SyntheticDragEvent from a raw JSObject. factory SyntheticDragEvent.fromJs(JSObject jsObject) = SyntheticDragEvent._; @@ -250,7 +257,8 @@ extension type SyntheticDragEvent._(JSObject _) implements SyntheticMouseEvent { /// A SyntheticEvent wrapper backed by a PointerEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent +/// - [PointerEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) +/// - [PointerEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/PointerEvent) extension type SyntheticPointerEvent._(JSObject _) implements SyntheticMouseEvent { /// Creates a SyntheticPointerEvent from a raw JSObject. @@ -294,7 +302,8 @@ extension type SyntheticPointerEvent._(JSObject _) /// A SyntheticEvent wrapper backed by a TouchEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent +/// - [TouchEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent) +/// - [TouchEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent) extension type SyntheticTouchEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticTouchEvent from a raw JSObject. factory SyntheticTouchEvent.fromJs(JSObject jsObject) = SyntheticTouchEvent._; @@ -332,7 +341,8 @@ extension type SyntheticTouchEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a TransitionEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent +/// - [TransitionEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent) +/// - [TransitionEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/TransitionEvent) extension type SyntheticTransitionEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticTransitionEvent from a raw JSObject. @@ -355,7 +365,8 @@ extension type SyntheticTransitionEvent._(JSObject _) /// A SyntheticEvent wrapper backed by an AnimationEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent +/// - [AnimationEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent) +/// - [AnimationEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/AnimationEvent) extension type SyntheticAnimationEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticAnimationEvent from a raw JSObject. factory SyntheticAnimationEvent.fromJs(JSObject jsObject) = @@ -377,7 +388,8 @@ extension type SyntheticAnimationEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a UIEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent +/// - [UIEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) +/// - [UIEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/UIEvent) extension type SyntheticUIEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticUIEvent from a raw JSObject. factory SyntheticUIEvent.fromJs(JSObject jsObject) = SyntheticUIEvent._; @@ -395,7 +407,8 @@ extension type SyntheticUIEvent._(JSObject _) implements SyntheticEvent { /// A SyntheticEvent wrapper backed by a WheelEvent. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent +/// - [WheelEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent) +/// - [WheelEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/WheelEvent) extension type SyntheticWheelEvent._(JSObject _) implements SyntheticMouseEvent { /// Creates a SyntheticWheelEvent from a raw JSObject. @@ -420,7 +433,8 @@ extension type SyntheticWheelEvent._(JSObject _) /// A SyntheticEvent wrapper for input events. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/InputEvent +/// - [InputEvent (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent) +/// - [InputEvent (MDN 中文)](https://developer.mozilla.org/zh-CN/docs/Web/API/InputEvent) extension type SyntheticInputEvent._(JSObject _) implements SyntheticEvent { /// Creates a SyntheticInputEvent from a raw JSObject. factory SyntheticInputEvent.fromJs(JSObject jsObject) = SyntheticInputEvent._; diff --git a/packages/dart_node_react/lib/src/testing_library.dart b/packages/dart_node_react/lib/src/testing_library.dart index 2abbc61..6fcb11f 100644 --- a/packages/dart_node_react/lib/src/testing_library.dart +++ b/packages/dart_node_react/lib/src/testing_library.dart @@ -2,6 +2,8 @@ /// /// Provides idiomatic Dart wrappers around @testing-library/react for testing /// React components with user-centric queries and interactions. +/// +/// - [React Testing Library documentation](https://testing-library.com/docs/react-testing-library/intro) library; import 'dart:async'; @@ -122,6 +124,8 @@ external JSAny? _reactAct(JSFunction callback); // ============================================================================= /// Wrapper around a DOM node providing query and interaction methods. +/// +/// - [About Queries](https://testing-library.com/docs/queries/about) final class DomNode { DomNode._(this._node); @@ -190,6 +194,8 @@ final class DomNode { // ============================================================================= /// Query methods for finding elements in the rendered output. +/// +/// - [Queries documentation](https://testing-library.com/docs/queries/about) final class ScreenQuery { /// Creates a Screen with the given container. ScreenQuery._(this._container); @@ -441,6 +447,8 @@ final class ScreenQuery { // ============================================================================= /// Result of rendering a React component for testing. +/// +/// - [render documentation](https://testing-library.com/docs/react-testing-library/api#render) final class TestRenderResult extends ScreenQuery { TestRenderResult._(this._root, DomNode container, this._baseElement) : super._(container); @@ -475,6 +483,8 @@ final class TestRenderResult extends ScreenQuery { // ============================================================================= /// Renders a React element into a detached DOM container for testing. +/// +/// - [render documentation](https://testing-library.com/docs/react-testing-library/api#render) TestRenderResult render(ReactElement element, {JSObject? container}) { final baseElement = container ?? _createElement('div'); _appendChild(baseElement); @@ -492,11 +502,17 @@ TestRenderResult render(ReactElement element, {JSObject? container}) { // ============================================================================= /// Wraps code that causes React state updates in an act() block. +/// +/// - [act documentation](https://react.dev/reference/react/act) +/// - [act 允许你在断言之前等待所有挂起的更新完成](https://zh-hans.react.dev/reference/react/act) void act(void Function() callback) { _reactAct(callback.toJS); } /// Async version of act for operations that return a Future. +/// +/// - [act documentation](https://react.dev/reference/react/act) +/// - [act 允许你在断言之前等待所有挂起的更新完成](https://zh-hans.react.dev/reference/react/act) Future actAsync(Future Function() callback) async { await callback(); await Future.delayed(Duration.zero); @@ -507,6 +523,8 @@ Future actAsync(Future Function() callback) async { // ============================================================================= /// Fires a click event on the element. +/// +/// - [fireEvent documentation](https://testing-library.com/docs/dom-testing-library/api-events) void fireClick(DomNode element, [Map? eventInit]) { act(() { final event = _createMouseEvent( @@ -723,6 +741,8 @@ external void _objectDefineProperty( // ============================================================================= /// Simulates a user clicking on an element. +/// +/// - [user-event documentation](https://testing-library.com/docs/user-event/intro) Future userClick(DomNode element) async { fireMouseDown(element); fireFocus(element); @@ -738,6 +758,8 @@ Future userDblClick(DomNode element) async { } /// Simulates a user typing text into an input. +/// +/// - [type documentation](https://testing-library.com/docs/user-event/utility#type) Future userType(DomNode element, String text) async { fireFocus(element); final buffer = StringBuffer(element.value); @@ -780,6 +802,8 @@ Future userPaste(DomNode element, String text) async { // ============================================================================= /// Waits for a condition to be true. +/// +/// - [waitFor documentation](https://testing-library.com/docs/dom-testing-library/api-async#waitfor) Future waitFor( T Function() callback, { Duration timeout = const Duration(seconds: 1), @@ -801,6 +825,8 @@ Future waitFor( } /// Waits for an element to be removed from the DOM. +/// +/// - [waitForElementToBeRemoved documentation](https://testing-library.com/docs/dom-testing-library/api-async#waitforelementtoberemoved) Future waitForElementToBeRemoved( DomNode? Function() callback, { Duration timeout = const Duration(seconds: 1), diff --git a/packages/dart_node_react/pubspec.lock b/packages/dart_node_react/pubspec.lock index 508a127..c1e365e 100644 --- a/packages/dart_node_react/pubspec.lock +++ b/packages/dart_node_react/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_node_core" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" description: diff --git a/packages/dart_node_react_native/README.md b/packages/dart_node_react_native/README.md index b5a9ead..ab0ea05 100644 --- a/packages/dart_node_react_native/README.md +++ b/packages/dart_node_react_native/README.md @@ -1,36 +1,429 @@ -# dart_node_react_native -React Native bindings for Dart. Build mobile apps with Expo entirely in Dart. +`dart_node_react_native` provides type-safe React Native bindings for building iOS and Android apps in Dart. Combined with Expo, you get a complete mobile development experience. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_react_native: ^0.11.0-beta + dart_node_react: ^0.11.0-beta # Required peer dependency +``` + +Set up your Expo project: + +```bash +npx create-expo-app my-app +cd my-app +``` + +## Quick Start ```dart +import 'package:dart_node_react/dart_node_react.dart'; import 'package:dart_node_react_native/dart_node_react_native.dart'; -void main() { - final app = View( - props: {'style': {'flex': 1, 'justifyContent': 'center'}}, +ReactElement app() { + return safeAreaView( + style: {'flex': 1, 'backgroundColor': '#fff'}, children: [ - Text(children: ['Hello from Dart!']), - Button( - props: {'title': 'Press me', 'onPress': () => print('Pressed!')}, + view( + style: {'padding': 20}, + children: [ + text( + 'Hello, Dart!', + style: {'fontSize': 24, 'fontWeight': 'bold'}, + ), + text('Welcome to React Native with Dart.'), + ], ), ], ); +} +``` + +## Components + +### View + +The fundamental building block, similar to `div` in web: + +```dart +view( + style: { + 'flex': 1, + 'flexDirection': 'row', + 'justifyContent': 'center', + 'alignItems': 'center', + 'backgroundColor': '#f5f5f5', + }, + children: [...], +) +``` + +### Text + +For displaying text: + +```dart +text( + 'Hello, World!', + style: { + 'fontSize': 18, + 'fontWeight': '600', + 'color': '#333', + 'textAlign': 'center', + }, +) +``` - registerComponent('App', () => app); +### TextInput + +For user text input: + +```dart +ReactElement searchInput() { + final query = useState(''); + + return textInput( + value: query.value, + onChangeText: (value) => query.set(value), + placeholder: 'Search...', + style: { + 'height': 40, + 'borderWidth': 1, + 'borderColor': '#ccc', + 'borderRadius': 8, + 'paddingHorizontal': 12, + }, + ); } ``` -## Run +### TouchableOpacity -Use VSCode launch config `Mobile: Build & Run (Expo)` or: +For pressable elements with opacity feedback: -```bash -dart compile js -o App.js lib/main.dart -npx expo start +```dart +touchableOpacity( + onPress: () => print('Pressed!'), + style: { + 'backgroundColor': '#007AFF', + 'padding': 12, + 'borderRadius': 8, + }, + children: [ + text( + 'Press Me', + style: {'color': '#fff', 'textAlign': 'center'}, + ), + ], +) +``` + +### Button + +Simple button component: + +```dart +rnButton( + title: 'Submit', + onPress: () => print('Button pressed!'), + color: '#007AFF', +) +``` + +### ScrollView + +For scrollable content: + +```dart +scrollView( + style: {'flex': 1}, + contentContainerStyle: {'padding': 20}, + children: [ + // Many children that exceed screen height + ...items.map((item) => itemCard(item)), + ], +) +``` + +### FlatList + +For efficient list rendering: + +```dart +ReactElement userList({required List users}) { + return flatList( + data: users, + keyExtractor: (user, _) => user.id, + renderItem: (info) => userCard(user: info.item), + ItemSeparatorComponent: () => view( + style: {'height': 1, 'backgroundColor': '#eee'}, + ), + ); +} +``` + +### Image + +For displaying images: + +```dart +// Local image +image( + source: AssetSource('assets/logo.png'), + style: {'width': 100, 'height': 100}, +) + +// Remote image +image( + source: UriSource('https://example.com/image.jpg'), + style: {'width': 200, 'height': 150}, + resizeMode: 'cover', +) +``` + +### SafeAreaView + +For respecting device safe areas (notch, home indicator): + +```dart +safeAreaView( + style: {'flex': 1}, + children: [ + // Content here is safe from notches and system UI + ], +) +``` + +### ActivityIndicator + +Loading spinner: + +```dart +activityIndicator( + size: 'large', + color: '#007AFF', +) +``` + +## Styling + +React Native uses JavaScript objects for styles (like React inline styles but with different properties): + +```dart +view( + style: { + // Layout + 'flex': 1, + 'flexDirection': 'column', // or 'row' + 'justifyContent': 'center', // main axis + 'alignItems': 'center', // cross axis + + // Spacing + 'padding': 20, + 'paddingHorizontal': 16, + 'margin': 10, + 'marginTop': 20, + + // Appearance + 'backgroundColor': '#ffffff', + 'borderRadius': 8, + 'borderWidth': 1, + 'borderColor': '#ccc', + + // Shadows (iOS) + 'shadowColor': '#000', + 'shadowOffset': {'width': 0, 'height': 2}, + 'shadowOpacity': 0.25, + 'shadowRadius': 4, + + // Shadows (Android) + 'elevation': 5, + }, + children: [...], +) +``` + +## Navigation + +Use with React Navigation (via JS interop): + +```dart +// Define screens +ReactElement homeScreen({required NavigationProps nav}) { + return view(children: [ + text('Home Screen'), + touchableOpacity( + onPress: () => nav.navigate('Details', {'id': 123}), + children: [text('Go to Details')])], + ), + ]); +} + +ReactElement detailsScreen({required NavigationProps nav}) { + final id = nav.route.params['id']; + + return view(children: [ + text('Details for $id'), + touchableOpacity( + onPress: () => nav.goBack(), + children: [text('Go Back')])], + ), + ]); +} +``` + +## Complete Example + +```dart +import 'package:dart_node_react_native/dart_node_react_native.dart'; +import 'package:dart_node_react/dart_node_react.dart'; + +ReactElement todoApp() { + final todos = useState>([]); + final inputValue = useState(''); + + void addTodo() { + if (inputValue.value.trim().isEmpty) return; + + todos.setWithUpdater((prev) => [ + ...prev, + Todo(id: DateTime.now().toString(), title: inputValue.value, completed: false), + ]); + inputValue.set(''); + } + + void toggleTodo(String id) { + todos.setWithUpdater((prev) => prev.map((todo) => + todo.id == id + ? Todo(id: todo.id, title: todo.title, completed: !todo.completed) + : todo + ).toList()); + } + + return safeAreaView( + style: {'flex': 1, 'backgroundColor': '#f5f5f5'}, + children: [ + // Header + view( + style: { + 'padding': 20, + 'backgroundColor': '#007AFF', + }, + children: [ + text( + 'My Todos', + style: { + 'fontSize': 24, + 'fontWeight': 'bold', + 'color': '#fff', + }, + ), + ], + ), + + // Input + view( + style: { + 'flexDirection': 'row', + 'padding': 16, + 'backgroundColor': '#fff', + }, + children: [ + textInput( + style: { + 'flex': 1, + 'height': 44, + 'borderWidth': 1, + 'borderColor': '#ddd', + 'borderRadius': 8, + 'paddingHorizontal': 12, + }, + value: inputValue.value, + onChangeText: (value) => inputValue.set(value), + placeholder: 'Add a todo...', + ), + touchableOpacity( + onPress: addTodo, + style: { + 'marginLeft': 12, + 'backgroundColor': '#007AFF', + 'paddingHorizontal': 20, + 'justifyContent': 'center', + 'borderRadius': 8, + }, + children: [ + text( + 'Add', + style: {'color': '#fff', 'fontWeight': '600'}, + ), + ], + ), + ], + ), + + // List + scrollView( + style: {'flex': 1}, + children: todos.value.map((todo) => touchableOpacity( + onPress: () => toggleTodo(todo.id), + style: { + 'flexDirection': 'row', + 'alignItems': 'center', + 'padding': 16, + 'backgroundColor': '#fff', + 'borderBottomWidth': 1, + 'borderBottomColor': '#eee', + }, + children: [ + view( + style: { + 'width': 24, + 'height': 24, + 'borderRadius': 12, + 'borderWidth': 2, + 'borderColor': todo.completed ? '#4CAF50' : '#ccc', + 'backgroundColor': todo.completed ? '#4CAF50' : 'transparent', + 'marginRight': 12, + }, + ), + text( + todo.title, + style: { + 'flex': 1, + 'fontSize': 16, + 'textDecorationLine': todo.completed ? 'line-through' : 'none', + 'color': todo.completed ? '#999' : '#333', + }, + ), + ], + )).toList(), + ), + + // Footer + view( + style: {'padding': 16, 'backgroundColor': '#fff'}, + children: [ + text( + '${todos.value.where((t) => !t.completed).length} items remaining', + style: {'textAlign': 'center', 'color': '#666'}, + ), + ], + ), + ], + ); +} + +class Todo { + final String id; + final String title; + final bool completed; + + Todo({required this.id, required this.title, required this.completed}); +} ``` -## Part of dart_node +## API Reference -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +See the [full API documentation](/api/dart_node_react_native/) for all available components and types. diff --git a/website/src/docs/react-native/index.md b/packages/dart_node_react_native/README_zh.md similarity index 63% rename from website/src/docs/react-native/index.md rename to packages/dart_node_react_native/README_zh.md index 08c77bc..c7caa84 100644 --- a/website/src/docs/react-native/index.md +++ b/packages/dart_node_react_native/README_zh.md @@ -1,35 +1,26 @@ ---- -layout: layouts/docs.njk -title: dart_node_react_native -description: React Native bindings for building cross-platform mobile apps in Dart with Expo. -eleventyNavigation: - key: dart_node_react_native - parent: Packages - order: 4 ---- -`dart_node_react_native` provides type-safe React Native bindings for building iOS and Android apps in Dart. Combined with Expo, you get a complete mobile development experience. +`dart_node_react_native` 提供类型安全的 React Native 绑定,用于在 Dart 中构建 iOS 和 Android 应用程序。结合 Expo,您可以获得完整的移动开发体验。 -## Installation +## 安装 ```yaml dependencies: - dart_node_react_native: ^0.2.0 - dart_node_react: ^0.2.0 # Required peer dependency + dart_node_react_native: ^0.11.0-beta + dart_node_react: ^0.11.0-beta # 必需的对等依赖 ``` -Set up your Expo project: +设置您的 Expo 项目: ```bash npx create-expo-app my-app cd my-app ``` -## Quick Start +## 快速开始 ```dart -import 'package:dart_node_react_native/dart_node_react_native.dart'; import 'package:dart_node_react/dart_node_react.dart'; +import 'package:dart_node_react_native/dart_node_react_native.dart'; ReactElement app() { return safeAreaView( @@ -38,13 +29,11 @@ ReactElement app() { view( style: {'padding': 20}, children: [ - rnText( + text( + 'Hello, Dart!', style: {'fontSize': 24, 'fontWeight': 'bold'}, - children: [text('Hello, Dart!')], - ), - rnText( - children: [text('Welcome to React Native with Dart.')], ), + text('Welcome to React Native with Dart.'), ], ), ], @@ -52,11 +41,11 @@ ReactElement app() { } ``` -## Components +## 组件 ### View -The fundamental building block, similar to `div` in web: +基础构建块,类似于 Web 中的 `div`: ```dart view( @@ -73,31 +62,31 @@ view( ### Text -For displaying text (note: `rnText` to avoid conflict with React's `text()`): +用于显示文本: ```dart -rnText( +text( + 'Hello, World!', style: { 'fontSize': 18, 'fontWeight': '600', 'color': '#333', 'textAlign': 'center', }, - children: [text('Hello, World!')], ) ``` ### TextInput -For user text input: +用于用户文本输入: ```dart ReactElement searchInput() { - final (query, setQuery) = useState(''); + final query = useState(''); return textInput( - value: query, - onChangeText: setQuery, + value: query.value, + onChangeText: (value) => query.set(value), placeholder: 'Search...', style: { 'height': 40, @@ -112,7 +101,7 @@ ReactElement searchInput() { ### TouchableOpacity -For pressable elements with opacity feedback: +用于具有透明度反馈的可按压元素: ```dart touchableOpacity( @@ -123,9 +112,9 @@ touchableOpacity( 'borderRadius': 8, }, children: [ - rnText( + text( + 'Press Me', style: {'color': '#fff', 'textAlign': 'center'}, - children: [text('Press Me')], ), ], ) @@ -133,7 +122,7 @@ touchableOpacity( ### Button -Simple button component: +简单的按钮组件: ```dart rnButton( @@ -145,14 +134,14 @@ rnButton( ### ScrollView -For scrollable content: +用于可滚动内容: ```dart scrollView( style: {'flex': 1}, contentContainerStyle: {'padding': 20}, children: [ - // Many children that exceed screen height + // 超出屏幕高度的多个子元素 ...items.map((item) => itemCard(item)), ], ) @@ -160,7 +149,7 @@ scrollView( ### FlatList -For efficient list rendering: +用于高效的列表渲染: ```dart ReactElement userList({required List users}) { @@ -177,16 +166,16 @@ ReactElement userList({required List users}) { ### Image -For displaying images: +用于显示图片: ```dart -// Local image +// 本地图片 image( source: AssetSource('assets/logo.png'), style: {'width': 100, 'height': 100}, ) -// Remote image +// 远程图片 image( source: UriSource('https://example.com/image.jpg'), style: {'width': 200, 'height': 150}, @@ -196,20 +185,20 @@ image( ### SafeAreaView -For respecting device safe areas (notch, home indicator): +用于适应设备安全区域(刘海、Home 指示器): ```dart safeAreaView( style: {'flex': 1}, children: [ - // Content here is safe from notches and system UI + // 此处内容不会被刘海和系统 UI 遮挡 ], ) ``` ### ActivityIndicator -Loading spinner: +加载指示器: ```dart activityIndicator( @@ -218,56 +207,56 @@ activityIndicator( ) ``` -## Styling +## 样式 -React Native uses JavaScript objects for styles (like React inline styles but with different properties): +React Native 使用 JavaScript 对象来设置样式(类似于 React 内联样式但属性不同): ```dart view( style: { - // Layout + // 布局 'flex': 1, - 'flexDirection': 'column', // or 'row' - 'justifyContent': 'center', // main axis - 'alignItems': 'center', // cross axis + 'flexDirection': 'column', // 或 'row' + 'justifyContent': 'center', // 主轴 + 'alignItems': 'center', // 交叉轴 - // Spacing + // 间距 'padding': 20, 'paddingHorizontal': 16, 'margin': 10, 'marginTop': 20, - // Appearance + // 外观 'backgroundColor': '#ffffff', 'borderRadius': 8, 'borderWidth': 1, 'borderColor': '#ccc', - // Shadows (iOS) + // 阴影(iOS) 'shadowColor': '#000', 'shadowOffset': {'width': 0, 'height': 2}, 'shadowOpacity': 0.25, 'shadowRadius': 4, - // Shadows (Android) + // 阴影(Android) 'elevation': 5, }, children: [...], ) ``` -## Navigation +## 导航 -Use with React Navigation (via JS interop): +与 React Navigation 一起使用(通过 JS 互操作): ```dart -// Define screens +// 定义屏幕 ReactElement homeScreen({required NavigationProps nav}) { return view(children: [ - rnText(children: [text('Home Screen')]), + text('Home Screen'), touchableOpacity( onPress: () => nav.navigate('Details', {'id': 123}), - children: [rnText(children: [text('Go to Details')])], + children: [text('Go to Details')])], ), ]); } @@ -276,37 +265,37 @@ ReactElement detailsScreen({required NavigationProps nav}) { final id = nav.route.params['id']; return view(children: [ - rnText(children: [text('Details for $id')]), + text('Details for $id'), touchableOpacity( onPress: () => nav.goBack(), - children: [rnText(children: [text('Go Back')])], + children: [text('Go Back')])], ), ]); } ``` -## Complete Example +## 完整示例 ```dart import 'package:dart_node_react_native/dart_node_react_native.dart'; import 'package:dart_node_react/dart_node_react.dart'; ReactElement todoApp() { - final (todos, setTodos) = useState>([]); - final (input, setInput) = useState(''); + final todos = useState>([]); + final inputValue = useState(''); void addTodo() { - if (input.trim().isEmpty) return; + if (inputValue.value.trim().isEmpty) return; - setTodos((prev) => [ + todos.setWithUpdater((prev) => [ ...prev, - Todo(id: DateTime.now().toString(), title: input, completed: false), + Todo(id: DateTime.now().toString(), title: inputValue.value, completed: false), ]); - setInput(''); + inputValue.set(''); } void toggleTodo(String id) { - setTodos((prev) => prev.map((todo) => + todos.setWithUpdater((prev) => prev.map((todo) => todo.id == id ? Todo(id: todo.id, title: todo.title, completed: !todo.completed) : todo @@ -316,25 +305,25 @@ ReactElement todoApp() { return safeAreaView( style: {'flex': 1, 'backgroundColor': '#f5f5f5'}, children: [ - // Header + // 头部 view( style: { 'padding': 20, 'backgroundColor': '#007AFF', }, children: [ - rnText( + text( + 'My Todos', style: { 'fontSize': 24, 'fontWeight': 'bold', 'color': '#fff', }, - children: [text('My Todos')], ), ], ), - // Input + // 输入框 view( style: { 'flexDirection': 'row', @@ -351,8 +340,8 @@ ReactElement todoApp() { 'borderRadius': 8, 'paddingHorizontal': 12, }, - value: input, - onChangeText: setInput, + value: inputValue.value, + onChangeText: (value) => inputValue.set(value), placeholder: 'Add a todo...', ), touchableOpacity( @@ -365,21 +354,20 @@ ReactElement todoApp() { 'borderRadius': 8, }, children: [ - rnText( + text( + 'Add', style: {'color': '#fff', 'fontWeight': '600'}, - children: [text('Add')], ), ], ), ], ), - // List - flatList( - data: todos, - keyExtractor: (todo, _) => todo.id, - renderItem: (info) => touchableOpacity( - onPress: () => toggleTodo(info.item.id), + // 列表 + scrollView( + style: {'flex': 1}, + children: todos.value.map((todo) => touchableOpacity( + onPress: () => toggleTodo(todo.id), style: { 'flexDirection': 'row', 'alignItems': 'center', @@ -395,34 +383,31 @@ ReactElement todoApp() { 'height': 24, 'borderRadius': 12, 'borderWidth': 2, - 'borderColor': info.item.completed ? '#4CAF50' : '#ccc', - 'backgroundColor': info.item.completed ? '#4CAF50' : 'transparent', + 'borderColor': todo.completed ? '#4CAF50' : '#ccc', + 'backgroundColor': todo.completed ? '#4CAF50' : 'transparent', 'marginRight': 12, }, ), - rnText( + text( + todo.title, style: { 'flex': 1, 'fontSize': 16, - 'textDecorationLine': info.item.completed ? 'line-through' : 'none', - 'color': info.item.completed ? '#999' : '#333', + 'textDecorationLine': todo.completed ? 'line-through' : 'none', + 'color': todo.completed ? '#999' : '#333', }, - children: [text(info.item.title)], ), ], - ), - style: {'flex': 1}, + )).toList(), ), - // Footer + // 底部 view( style: {'padding': 16, 'backgroundColor': '#fff'}, children: [ - rnText( + text( + '${todos.value.where((t) => !t.completed).length} items remaining', style: {'textAlign': 'center', 'color': '#666'}, - children: [ - text('${todos.where((t) => !t.completed).length} items remaining'), - ], ), ], ), @@ -439,6 +424,6 @@ class Todo { } ``` -## API Reference +## API 参考 -See the [full API documentation](/api/dart_node_react_native/) for all available components and types. +请参阅[完整 API 文档](/api/dart_node_react_native/)了解所有可用组件和类型。 diff --git a/packages/dart_node_react_native/pubspec.lock b/packages/dart_node_react_native/pubspec.lock index ccd6042..82dc4d9 100644 --- a/packages/dart_node_react_native/pubspec.lock +++ b/packages/dart_node_react_native/pubspec.lock @@ -92,10 +92,9 @@ packages: dart_node_core: dependency: "direct main" description: - name: dart_node_core - sha256: "225e474698d27fa53f8e363b6f94ed87709fc9af57e97c0dfe7942594fd92aa3" - url: "https://pub.dev" - source: hosted + path: "../dart_node_core" + relative: true + source: path version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" @@ -107,10 +106,9 @@ packages: dart_node_react: dependency: "direct main" description: - name: dart_node_react - sha256: "23a0c53b1b378002d8569111841b9217becb7fbd0eb6a65998777924c25bd737" - url: "https://pub.dev" - source: hosted + path: "../dart_node_react" + relative: true + source: path version: "0.11.0-beta" file: dependency: transitive diff --git a/packages/dart_node_ws/README.md b/packages/dart_node_ws/README.md index 3fad813..4bd3533 100644 --- a/packages/dart_node_ws/README.md +++ b/packages/dart_node_ws/README.md @@ -1,36 +1,274 @@ # dart_node_ws -WebSocket bindings for Dart on Node.js. Build real-time servers entirely in Dart. +Type-safe WebSocket bindings for Node.js, enabling real-time bidirectional communication in your Dart applications. -## Getting Started +## Installation + +```yaml +dependencies: + dart_node_ws: ^0.11.0-beta +``` + +Also install the ws package via npm: + +```bash +npm install ws +``` + +## Quick Start + +### WebSocket Server ```dart import 'package:dart_node_ws/dart_node_ws.dart'; -import 'package:dart_node_express/dart_node_express.dart'; void main() { - final app = express(); - final server = app.listen(3000); + final server = createWebSocketServer(port: 8080); + + server.onConnection((client, url) { + print('Client connected from $url'); - final wss = WebSocketServer(server: server); + client.onMessage((message) { + print('Received: ${message.text}'); + // Echo back + client.send('You said: ${message.text}'); + }); - wss.on('connection', (ws) { - ws.on('message', (data) { - ws.send('Echo: $data'); + client.onClose((data) { + print('Client disconnected: ${data.code} ${data.reason}'); }); + + // Send welcome message + client.send('Welcome to the WebSocket server!'); }); - print('WebSocket server on ws://localhost:3000'); + print('WebSocket server running on port 8080'); } ``` -## Run +## WebSocket Server API -```bash -dart compile js -o server.js lib/main.dart -node server.js +### Creating a Server + +```dart +// Standalone server on a port +final server = createWebSocketServer(port: 8080); +``` + +### Server Events + +```dart +server.onConnection((WebSocketClient client, String? url) { + // New client connected + // url contains the request URL (e.g., '/ws?token=abc') + print('Connection from $url'); +}); +``` + +### Closing the Server + +```dart +server.close(() { + print('Server closed'); +}); +``` + +## WebSocket Client API + +### Client Events + +```dart +client.onMessage((WebSocketMessage message) { + // message.text - string content + // message.bytes - binary data (if applicable) + print('Received: ${message.text}'); +}); + +client.onClose((CloseEventData data) { + // data.code - close code (1000 = normal) + // data.reason - close reason + print('Closed with code ${data.code}: ${data.reason}'); +}); + +client.onError((WebSocketError error) { + print('Client error: ${error.message}'); +}); +``` + +### Sending Messages + +```dart +// Send text +client.send('Hello, client!'); + +// Send JSON (automatically serialized) +client.sendJson({'type': 'update', 'data': someData}); +``` + +### Client State + +```dart +// Check if connection is open +if (client.isOpen) { + client.send('Connected!'); +} + +// userId can be set for identification +client.userId = 'user123'; +``` + +### Closing Connection + +```dart +// Close with default code (1000 = normal) +client.close(); + +// Close with custom code and reason +client.close(1000, 'Normal closure'); +``` + +## Close Codes + +Standard WebSocket close codes: +- `1000`: Normal closure +- `1001`: Going away (server shutdown) +- `1002`: Protocol error +- `1006`: Abnormal closure (no close frame) +- `1011`: Internal error +- `3000-3999`: Library/framework codes +- `4000-4999`: Private use codes + +## Chat Server Example + +```dart +import 'dart:convert'; +import 'package:dart_node_ws/dart_node_ws.dart'; + +void main() { + final server = createWebSocketServer(port: 8080); + final clients = {}; + + server.onConnection((client, url) { + String? username; + + client.onMessage((message) { + final data = jsonDecode(message.text ?? '{}'); + + switch (data['type']) { + case 'join': + username = data['username']; + client.userId = username; + clients[username!] = client; + broadcast(clients, { + 'type': 'system', + 'text': '$username joined the chat', + }); + + case 'message': + if (username != null) { + broadcast(clients, { + 'type': 'message', + 'username': username, + 'text': data['text'], + 'timestamp': DateTime.now().toIso8601String(), + }); + } + } + }); + + client.onClose((data) { + if (username != null) { + clients.remove(username); + broadcast(clients, { + 'type': 'system', + 'text': '$username left the chat', + }); + } + }); + }); + + print('Chat server running on port 8080'); +} + +void broadcast(Map clients, Map message) { + final json = jsonEncode(message); + for (final client in clients.values) { + if (client.isOpen) { + client.send(json); + } + } +} +``` + +## Real-time Dashboard Example + +```dart +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:dart_node_ws/dart_node_ws.dart'; + +void main() { + final server = createWebSocketServer(port: 8080); + final subscribers = {}; + + // Simulate real-time data updates + Timer.periodic(Duration(seconds: 1), (_) { + final data = { + 'timestamp': DateTime.now().toIso8601String(), + 'cpu': Random().nextDouble() * 100, + 'memory': Random().nextDouble() * 100, + 'requests': Random().nextInt(1000), + }; + + final json = jsonEncode(data); + for (final client in subscribers) { + if (client.isOpen) { + client.send(json); + } + } + }); + + server.onConnection((client, url) { + print('Dashboard client connected'); + subscribers.add(client); + + // Send initial state + client.sendJson({ + 'type': 'init', + 'serverTime': DateTime.now().toIso8601String(), + }); + + client.onClose((data) { + subscribers.remove(client); + print('Dashboard client disconnected'); + }); + }); + + print('Dashboard WebSocket server on port 8080'); +} +``` + +## Error Handling + +```dart +server.onConnection((client, url) { + client.onMessage((message) { + try { + final data = jsonDecode(message.text ?? '{}'); + // Process message... + } catch (e) { + client.sendJson({'error': 'Invalid message format'}); + } + }); + + client.onError((error) { + print('Client error: ${error.message}'); + // Don't crash the server + }); +}); ``` -## Part of dart_node +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_ws). diff --git a/packages/dart_node_ws/README_zh.md b/packages/dart_node_ws/README_zh.md new file mode 100644 index 0000000..e1898f9 --- /dev/null +++ b/packages/dart_node_ws/README_zh.md @@ -0,0 +1,274 @@ +# dart_node_ws + +类型安全的 Node.js WebSocket 绑定,为您的 Dart 应用程序提供实时双向通信能力。 + +## 安装 + +```yaml +dependencies: + dart_node_ws: ^0.11.0-beta +``` + +通过 npm 安装 ws 包: + +```bash +npm install ws +``` + +## 快速开始 + +### WebSocket 服务器 + +```dart +import 'package:dart_node_ws/dart_node_ws.dart'; + +void main() { + final server = createWebSocketServer(port: 8080); + + server.onConnection((client, url) { + print('Client connected from $url'); + + client.onMessage((message) { + print('Received: ${message.text}'); + // 回显消息 + client.send('You said: ${message.text}'); + }); + + client.onClose((data) { + print('Client disconnected: ${data.code} ${data.reason}'); + }); + + // 发送欢迎消息 + client.send('Welcome to the WebSocket server!'); + }); + + print('WebSocket server running on port 8080'); +} +``` + +## WebSocket 服务器 API + +### 创建服务器 + +```dart +// 在指定端口创建独立服务器 +final server = createWebSocketServer(port: 8080); +``` + +### 服务器事件 + +```dart +server.onConnection((WebSocketClient client, String? url) { + // 新客户端已连接 + // url 包含请求 URL(例如 '/ws?token=abc') + print('Connection from $url'); +}); +``` + +### 关闭服务器 + +```dart +server.close(() { + print('Server closed'); +}); +``` + +## WebSocket 客户端 API + +### 客户端事件 + +```dart +client.onMessage((WebSocketMessage message) { + // message.text - 字符串内容 + // message.bytes - 二进制数据(如适用) + print('Received: ${message.text}'); +}); + +client.onClose((CloseEventData data) { + // data.code - 关闭代码(1000 = 正常关闭) + // data.reason - 关闭原因 + print('Closed with code ${data.code}: ${data.reason}'); +}); + +client.onError((WebSocketError error) { + print('Client error: ${error.message}'); +}); +``` + +### 发送消息 + +```dart +// 发送文本 +client.send('Hello, client!'); + +// 发送 JSON(自动序列化) +client.sendJson({'type': 'update', 'data': someData}); +``` + +### 客户端状态 + +```dart +// 检查连接是否打开 +if (client.isOpen) { + client.send('Connected!'); +} + +// 可以设置 userId 用于识别 +client.userId = 'user123'; +``` + +### 关闭连接 + +```dart +// 使用默认代码关闭(1000 = 正常关闭) +client.close(); + +// 使用自定义代码和原因关闭 +client.close(1000, 'Normal closure'); +``` + +## 关闭代码 + +标准 WebSocket 关闭代码: +- `1000`:正常关闭 +- `1001`:离开(服务器关闭) +- `1002`:协议错误 +- `1006`:异常关闭(无关闭帧) +- `1011`:内部错误 +- `3000-3999`:库/框架代码 +- `4000-4999`:私有使用代码 + +## 聊天服务器示例 + +```dart +import 'dart:convert'; +import 'package:dart_node_ws/dart_node_ws.dart'; + +void main() { + final server = createWebSocketServer(port: 8080); + final clients = {}; + + server.onConnection((client, url) { + String? username; + + client.onMessage((message) { + final data = jsonDecode(message.text ?? '{}'); + + switch (data['type']) { + case 'join': + username = data['username']; + client.userId = username; + clients[username!] = client; + broadcast(clients, { + 'type': 'system', + 'text': '$username joined the chat', + }); + + case 'message': + if (username != null) { + broadcast(clients, { + 'type': 'message', + 'username': username, + 'text': data['text'], + 'timestamp': DateTime.now().toIso8601String(), + }); + } + } + }); + + client.onClose((data) { + if (username != null) { + clients.remove(username); + broadcast(clients, { + 'type': 'system', + 'text': '$username left the chat', + }); + } + }); + }); + + print('Chat server running on port 8080'); +} + +void broadcast(Map clients, Map message) { + final json = jsonEncode(message); + for (final client in clients.values) { + if (client.isOpen) { + client.send(json); + } + } +} +``` + +## 实时仪表板示例 + +```dart +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:dart_node_ws/dart_node_ws.dart'; + +void main() { + final server = createWebSocketServer(port: 8080); + final subscribers = {}; + + // 模拟实时数据更新 + Timer.periodic(Duration(seconds: 1), (_) { + final data = { + 'timestamp': DateTime.now().toIso8601String(), + 'cpu': Random().nextDouble() * 100, + 'memory': Random().nextDouble() * 100, + 'requests': Random().nextInt(1000), + }; + + final json = jsonEncode(data); + for (final client in subscribers) { + if (client.isOpen) { + client.send(json); + } + } + }); + + server.onConnection((client, url) { + print('Dashboard client connected'); + subscribers.add(client); + + // 发送初始状态 + client.sendJson({ + 'type': 'init', + 'serverTime': DateTime.now().toIso8601String(), + }); + + client.onClose((data) { + subscribers.remove(client); + print('Dashboard client disconnected'); + }); + }); + + print('Dashboard WebSocket server on port 8080'); +} +``` + +## 错误处理 + +```dart +server.onConnection((client, url) { + client.onMessage((message) { + try { + final data = jsonDecode(message.text ?? '{}'); + // 处理消息... + } catch (e) { + client.sendJson({'error': 'Invalid message format'}); + } + }); + + client.onError((error) { + print('Client error: ${error.message}'); + // 不要让服务器崩溃 + }); +}); +``` + +## 源代码 + +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/dart_node_ws) 上获取。 diff --git a/packages/dart_node_ws/lib/src/websocket_types.dart b/packages/dart_node_ws/lib/src/websocket_types.dart index 59af33a..ec40512 100644 --- a/packages/dart_node_ws/lib/src/websocket_types.dart +++ b/packages/dart_node_ws/lib/src/websocket_types.dart @@ -11,7 +11,8 @@ external JSString _jsStringify(JSAny? value); /// WebSocket connection ready states as defined by the WebSocket API. /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState +/// - [WebSocket.readyState (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) +/// - [WebSocket:readyState 属性 (MDN)](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/readyState) enum WebSocketReadyState { /// Socket has been created. The connection is not yet open. connecting(0), @@ -45,7 +46,8 @@ enum WebSocketReadyState { /// - 3000-3999: Library/framework codes (IANA registered) /// - 4000-4999: Private use codes /// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code +/// - [CloseEvent.code (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code) +/// - [CloseEvent (MDN)](https://developer.mozilla.org/zh-CN/docs/Web/API/CloseEvent) typedef CloseEventData = ({ /// The close code (1000-4999) indicating why the connection closed. int code, diff --git a/packages/dart_node_ws/pubspec.lock b/packages/dart_node_ws/pubspec.lock index 0385221..85f6f00 100644 --- a/packages/dart_node_ws/pubspec.lock +++ b/packages/dart_node_ws/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_node_core" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" dart_node_coverage: dependency: "direct dev" description: diff --git a/packages/reflux/README.md b/packages/reflux/README.md index f1d0c5b..a442c8b 100644 --- a/packages/reflux/README.md +++ b/packages/reflux/README.md @@ -1,10 +1,58 @@ -# Reflux +Reflux is a state management library for **React with Dart** and **Flutter**. It provides a predictable state container with full type safety using Dart's sealed classes for exhaustive pattern matching. -Redux-inspired state management for **React with Dart ([dart_node](https://dartnode.dev))** and **Flutter**. +> [**Reflux**](https://en.wikipedia.org/wiki/Reflux) is a technique where a liquid is heated in a flask with a condenser on top. The vapor rises, cools, condenses, and flows back down into the flask - a continuous cycle of transformation and return. Unidirectional State Management works the same way: the heat and energy are your dispatched actions, the rising vapor represents actions flowing through the system, the condenser is the reducer that transforms the input, the condensed liquid flowing back is the new state returning to your components, and the continuous cycle is the unidirectional data flow loop. Same substance, continuously refined. -Predictable state container with full type safety using Dart's sealed classes for exhaustive pattern matching. +## Installation -## Getting Started +```yaml +dependencies: + reflux: ^0.11.0-beta +``` + +## Core Concepts + +### Store + +The store holds the complete state tree of your application. There should be a single store for the entire app. + +```dart +import 'package:reflux/reflux.dart'; + +final store = createStore(counterReducer, (count: 0)); +``` + +### Actions + +Actions are sealed classes that describe what happened. Use Dart's pattern matching on the actual TYPE, not strings. + +```dart +sealed class CounterAction extends Action {} + +final class Increment extends CounterAction {} +final class Decrement extends CounterAction {} +final class SetValue extends CounterAction { + const SetValue(this.value); + final int value; +} +``` + +### Reducers + +Reducers are pure functions that specify how state changes in response to actions. + +```dart +typedef CounterState = ({int count}); + +CounterState counterReducer(CounterState state, Action action) => + switch (action) { + Increment() => (count: state.count + 1), + Decrement() => (count: state.count - 1), + SetValue(:final value) => (count: value), + _ => state, + }; +``` + +## Quick Start ```dart import 'package:reflux/reflux.dart'; @@ -36,6 +84,62 @@ void main() { } ``` -## Part of dart_node +## Middleware + +Middleware provides a third-party extension point between dispatching an action and the reducer. + +```dart +Middleware loggerMiddleware() => + (api) => (next) => (action) { + print('Dispatching: ${action.runtimeType}'); + next(action); + print('State: ${api.getState()}'); + }; + +final store = createStore( + counterReducer, + (count: 0), + enhancer: applyMiddleware([loggerMiddleware()]), +); +``` + +## Selectors + +Selectors extract and memoize derived data from the state. + +```dart +final getCount = createSelector1( + (CounterState s) => s.count, + (count) => count * 2, +); + +final doubledCount = getCount(store.getState()); +``` + +## Time Travel + +The TimeTravelEnhancer allows you to undo/redo state changes. + +```dart +final timeTravel = TimeTravelEnhancer(); + +final store = createStore( + counterReducer, + (count: 0), + enhancer: timeTravel.enhancer, +); + +store.dispatch(Increment()); +store.dispatch(Increment()); + +timeTravel.undo(); // Go back one step +timeTravel.redo(); // Go forward one step +``` + +## API Reference + +See the [full API documentation](/api/reflux/) for all available functions and types. + +## Source Code -[GitHub](https://github.com/MelbourneDeveloper/dart_node) +The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/reflux). diff --git a/website/src/docs/reflux/index.md b/packages/reflux/README_zh.md similarity index 59% rename from website/src/docs/reflux/index.md rename to packages/reflux/README_zh.md index 855459c..b4ec479 100644 --- a/website/src/docs/reflux/index.md +++ b/packages/reflux/README_zh.md @@ -1,27 +1,18 @@ ---- -layout: layouts/docs.njk -title: reflux -description: State management for React with Dart and Flutter. Predictable state container with type-safe actions. -eleventyNavigation: - key: reflux - parent: Packages - order: 9 ---- -Reflux is a state management library for **React with Dart** and **Flutter**. It provides a predictable state container with full type safety using Dart's sealed classes for exhaustive pattern matching. +Reflux 是一个用于 **React with Dart** 和 **Flutter** 的状态管理库。它使用 Dart 的密封类提供完全类型安全的可预测状态容器,支持穷尽模式匹配。 -## Installation +## 安装 ```yaml dependencies: - reflux: ^0.9.0 + reflux: ^0.11.0-beta ``` -## Core Concepts +## 核心概念 ### Store -The store holds the complete state tree of your application. There should be a single store for the entire app. +Store 保存应用程序的完整状态树。整个应用应该只有一个 store。 ```dart import 'package:reflux/reflux.dart'; @@ -31,7 +22,7 @@ final store = createStore(counterReducer, (count: 0)); ### Actions -Actions are sealed classes that describe what happened. Use Dart's pattern matching on the actual TYPE, not strings. +Actions 是描述发生了什么的密封类。使用 Dart 的模式匹配来匹配实际的类型,而不是字符串。 ```dart sealed class CounterAction extends Action {} @@ -46,7 +37,7 @@ final class SetValue extends CounterAction { ### Reducers -Reducers are pure functions that specify how state changes in response to actions. +Reducers 是纯函数,指定状态如何响应 actions 而改变。 ```dart typedef CounterState = ({int count}); @@ -60,20 +51,20 @@ CounterState counterReducer(CounterState state, Action action) => }; ``` -## Quick Start +## 快速开始 ```dart import 'package:reflux/reflux.dart'; -// State as a record +// 使用记录类型定义状态 typedef CounterState = ({int count}); -// Actions as sealed classes +// 使用密封类定义 Actions sealed class CounterAction extends Action {} final class Increment extends CounterAction {} final class Decrement extends CounterAction {} -// Reducer with pattern matching +// 使用模式匹配的 Reducer CounterState counterReducer(CounterState state, Action action) => switch (action) { Increment() => (count: state.count + 1), @@ -92,9 +83,9 @@ void main() { } ``` -## Middleware +## 中间件 -Middleware provides a third-party extension point between dispatching an action and the reducer. +中间件提供了在分发 action 和 reducer 之间的第三方扩展点。 ```dart Middleware loggerMiddleware() => @@ -111,9 +102,9 @@ final store = createStore( ); ``` -## Selectors +## 选择器 -Selectors extract and memoize derived data from the state. +选择器从状态中提取和记忆化派生数据。 ```dart final getCount = createSelector1( @@ -124,9 +115,9 @@ final getCount = createSelector1( final doubledCount = getCount(store.getState()); ``` -## Time Travel +## 时间旅行 -The TimeTravelEnhancer allows you to undo/redo state changes. +TimeTravelEnhancer 允许您撤销/重做状态更改。 ```dart final timeTravel = TimeTravelEnhancer(); @@ -140,14 +131,14 @@ final store = createStore( store.dispatch(Increment()); store.dispatch(Increment()); -timeTravel.undo(); // Go back one step -timeTravel.redo(); // Go forward one step +timeTravel.undo(); // 后退一步 +timeTravel.redo(); // 前进一步 ``` -## API Reference +## API 参考 -See the [full API documentation](/api/reflux/) for all available functions and types. +请参阅[完整 API 文档](/api/reflux/)了解所有可用函数和类型。 -## Source Code +## 源代码 -The source code is available on [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/reflux). +源代码可在 [GitHub](https://github.com/melbournedeveloper/dart_node/tree/main/packages/reflux) 上获取。 diff --git a/packages/reflux/pubspec.lock b/packages/reflux/pubspec.lock index 63014cf..56c9007 100644 --- a/packages/reflux/pubspec.lock +++ b/packages/reflux/pubspec.lock @@ -95,7 +95,7 @@ packages: path: "../dart_logging" relative: true source: path - version: "0.9.0-beta" + version: "0.11.0-beta" file: dependency: transitive description: diff --git a/website/.eleventyignore b/website/.eleventyignore new file mode 100644 index 0000000..ff29d90 --- /dev/null +++ b/website/.eleventyignore @@ -0,0 +1 @@ +# Empty - we want Eleventy to process all files including gitignored ones diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..73fe75f --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,21 @@ +# Generated docs (copied from READMEs at build time) +src/docs/core/index.md +src/docs/express/index.md +src/docs/react/index.md +src/docs/react-native/index.md +src/docs/websockets/index.md +src/docs/sqlite/index.md +src/docs/mcp/index.md +src/docs/logging/index.md +src/docs/reflux/index.md +src/docs/jsx/index.md +src/zh/docs/core/index.md +src/zh/docs/express/index.md +src/zh/docs/react/index.md +src/zh/docs/react-native/index.md +src/zh/docs/websockets/index.md +src/zh/docs/sqlite/index.md +src/zh/docs/mcp/index.md +src/zh/docs/logging/index.md +src/zh/docs/reflux/index.md +src/zh/docs/jsx/index.md diff --git a/website/eleventy.config.js b/website/eleventy.config.js index 68ce2a5..84d2f35 100644 --- a/website/eleventy.config.js +++ b/website/eleventy.config.js @@ -3,8 +3,20 @@ import pluginRss from "@11ty/eleventy-plugin-rss"; import eleventyNavigationPlugin from "@11ty/eleventy-navigation"; import markdownIt from "markdown-it"; import markdownItAnchor from "markdown-it-anchor"; +import { execSync } from "child_process"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const packagesDir = resolve(__dirname, "..", "packages"); + +const supportedLanguages = ['en', 'zh']; +const defaultLanguage = 'en'; export default function(eleventyConfig) { + // Don't use .gitignore to ignore files (we want to process generated docs) + eleventyConfig.setUseGitIgnore(false); + // Configure markdown-it with anchor plugin for header IDs const mdOptions = { html: true, @@ -30,17 +42,34 @@ export default function(eleventyConfig) { eleventyConfig.addPassthroughCopy("src/assets"); eleventyConfig.addPassthroughCopy("src/api"); eleventyConfig.addPassthroughCopy("src/robots.txt"); + eleventyConfig.addPassthroughCopy("src/llms.txt"); // Watch targets eleventyConfig.addWatchTarget("src/assets/"); + // Watch READMEs and copy when they change + eleventyConfig.addWatchTarget(packagesDir); + eleventyConfig.on("eleventy.beforeWatch", (changedFiles) => { + if (changedFiles.some(f => f.endsWith("README.md"))) { + execSync("node scripts/copy-readmes.js", { stdio: "inherit" }); + } + }); + // Collections + // English posts only (from src/blog/) eleventyConfig.addCollection("posts", function(collectionApi) { return collectionApi.getFilteredByGlob("src/blog/*.md").sort((a, b) => { return b.date - a.date; }); }); + // Chinese posts only (from src/zh/blog/) + eleventyConfig.addCollection("zhPosts", function(collectionApi) { + return collectionApi.getFilteredByGlob("src/zh/blog/*.md").sort((a, b) => { + return b.date - a.date; + }); + }); + eleventyConfig.addCollection("docs", function(collectionApi) { return collectionApi.getFilteredByGlob("src/docs/**/*.md"); }); @@ -91,6 +120,52 @@ export default function(eleventyConfig) { return postsByCategory; }); + // Chinese tag collection + eleventyConfig.addCollection("zhTagList", function(collectionApi) { + const tagSet = new Set(); + collectionApi.getFilteredByGlob("src/zh/blog/*.md").forEach(post => { + (post.data.tags || []).forEach(tag => { + tag !== 'post' && tag !== 'posts' && tagSet.add(tag); + }); + }); + return [...tagSet].sort(); + }); + + // Chinese category collection + eleventyConfig.addCollection("zhCategoryList", function(collectionApi) { + const categorySet = new Set(); + collectionApi.getFilteredByGlob("src/zh/blog/*.md").forEach(post => { + post.data.category && categorySet.add(post.data.category); + }); + return [...categorySet].sort(); + }); + + // Chinese posts by tag + eleventyConfig.addCollection("zhPostsByTag", function(collectionApi) { + const postsByTag = {}; + collectionApi.getFilteredByGlob("src/zh/blog/*.md").forEach(post => { + (post.data.tags || []).forEach(tag => { + tag !== 'post' && tag !== 'posts' && (postsByTag[tag] = postsByTag[tag] || []).push(post); + }); + }); + Object.keys(postsByTag).forEach(tag => { + postsByTag[tag].sort((a, b) => b.date - a.date); + }); + return postsByTag; + }); + + // Chinese posts by category + eleventyConfig.addCollection("zhPostsByCategory", function(collectionApi) { + const postsByCategory = {}; + collectionApi.getFilteredByGlob("src/zh/blog/*.md").forEach(post => { + post.data.category && (postsByCategory[post.data.category] = postsByCategory[post.data.category] || []).push(post); + }); + Object.keys(postsByCategory).forEach(cat => { + postsByCategory[cat].sort((a, b) => b.date - a.date); + }); + return postsByCategory; + }); + // Filters eleventyConfig.addFilter("dateFormat", (dateObj) => { return new Date(dateObj).toLocaleDateString('en-US', { @@ -116,6 +191,35 @@ export default function(eleventyConfig) { return str ? str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') : ''; }); + // i18n filter - get translation by key path + eleventyConfig.addFilter("t", (key, lang = defaultLanguage) => { + const i18n = eleventyConfig.globalData?.i18n; + if (!i18n) return key; + const langData = i18n[lang] || i18n[defaultLanguage]; + const keys = key.split('.'); + let value = langData; + for (const k of keys) { + value = value?.[k]; + } + return value || key; + }); + + // Get alternate language URL + eleventyConfig.addFilter("altLangUrl", (url, currentLang, targetLang) => { + if (currentLang === 'en' && targetLang !== 'en') { + return `/${targetLang}${url}`; + } else if (currentLang !== 'en' && targetLang === 'en') { + return url.replace(`/${currentLang}`, '') || '/'; + } else if (currentLang !== 'en' && targetLang !== 'en') { + return url.replace(`/${currentLang}`, `/${targetLang}`); + } + return url; + }); + + // Add global data for languages + eleventyConfig.addGlobalData("supportedLanguages", supportedLanguages); + eleventyConfig.addGlobalData("defaultLanguage", defaultLanguage); + // Shortcodes eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`); diff --git a/website/package-lock.json b/website/package-lock.json index ddf02dd..9ff97e1 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -12,8 +12,14 @@ "@11ty/eleventy-navigation": "^0.3.5", "@11ty/eleventy-plugin-rss": "^2.0.2", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "@babel/core": "^7.28.6", + "@playwright/test": "^1.57.0", + "babel-plugin-istanbul": "^7.0.1", + "istanbul-lib-instrument": "^6.0.3", "jsdom": "^24.1.3", - "markdown-it-anchor": "^9.2.0" + "markdown-it-anchor": "^9.2.0", + "nyc": "^17.1.0", + "v8-to-istanbul": "^9.3.0" } }, "node_modules/@11ty/dependency-tree": { @@ -270,6 +276,293 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -385,6 +678,123 @@ "node": ">=18" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/slugify": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -418,6 +828,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -489,6 +906,46 @@ "node": ">= 14" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -516,6 +973,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -580,6 +1057,26 @@ "dev": true, "license": "MIT" }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -587,6 +1084,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcp-47": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", @@ -666,6 +1173,56 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -680,6 +1237,37 @@ "node": ">= 0.4" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -705,6 +1293,48 @@ "fsevents": "~2.3.2" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -728,6 +1358,13 @@ "node": ">=14" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -735,6 +1372,28 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -788,6 +1447,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -795,6 +1464,22 @@ "dev": true, "license": "MIT" }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -916,6 +1601,20 @@ "dev": true, "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1001,6 +1700,23 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1155,6 +1871,68 @@ "dev": true, "license": "MIT" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -1205,6 +1983,34 @@ "node": ">= 0.8" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1230,6 +2036,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1255,6 +2081,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -1269,6 +2105,28 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1295,6 +2153,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -1335,6 +2200,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1364,6 +2239,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1390,6 +2282,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", @@ -1495,6 +2394,38 @@ "node": ">=0.10.0" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1552,73 +2483,245 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iso-639-1": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", + "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "semver": "^7.5.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-json": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", - "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "license": "ISC" + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=8" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, - "node_modules/iso-639-1": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", - "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -1673,6 +2776,32 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -1741,6 +2870,26 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1758,6 +2907,32 @@ "node": ">=12" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -1927,6 +3102,26 @@ "dev": true, "license": "MIT" }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/node-retrieve-globals": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", @@ -1992,6 +3187,48 @@ "dev": true, "license": "MIT" }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2005,6 +3242,84 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -2032,20 +3347,117 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/please-upgrade-node": { @@ -2121,6 +3533,19 @@ "node": ">=6" } }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -2204,6 +3629,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -2211,6 +3666,33 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -2295,6 +3777,13 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2302,6 +3791,36 @@ "dev": true, "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2322,6 +3841,48 @@ "node": ">=8.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2352,6 +3913,44 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -2362,6 +3961,19 @@ "node": ">=0.10.0" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -2369,6 +3981,21 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2438,6 +4065,26 @@ "node": ">=18" } }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -2465,6 +4112,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -2483,6 +4161,38 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -2543,6 +4253,64 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -2581,6 +4349,57 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } } } diff --git a/website/package.json b/website/package.json index a659d85..e2a6139 100644 --- a/website/package.json +++ b/website/package.json @@ -5,16 +5,29 @@ "type": "module", "scripts": { "dev": "eleventy --serve", - "build": "bash scripts/generate-api-docs.sh && eleventy", + "build": "bash scripts/build.sh", + "clean": "bash scripts/clean.sh", + "rebuild": "bash scripts/rebuild.sh", "build:docs": "bash scripts/generate-api-docs.sh", - "build:site": "eleventy" + "build:site": "eleventy", + "copy:readmes": "node scripts/copy-readmes.js", + "watch:readmes": "fswatch -o ../packages/*/README.md | xargs -n1 -I{} node scripts/copy-readmes.js", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:coverage": "playwright test && node scripts/merge-coverage.js" }, "devDependencies": { "@11ty/eleventy": "^3.1.2", "@11ty/eleventy-navigation": "^0.3.5", "@11ty/eleventy-plugin-rss": "^2.0.2", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "@babel/core": "^7.28.6", + "@playwright/test": "^1.57.0", + "babel-plugin-istanbul": "^7.0.1", + "istanbul-lib-instrument": "^6.0.3", "jsdom": "^24.1.3", - "markdown-it-anchor": "^9.2.0" + "markdown-it-anchor": "^9.2.0", + "nyc": "^17.1.0", + "v8-to-istanbul": "^9.3.0" } } diff --git a/website/playwright.config.js b/website/playwright.config.js new file mode 100644 index 0000000..edd2037 --- /dev/null +++ b/website/playwright.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html'], + ['json', { outputFile: 'test-results/results.json' }], + ], + use: { + baseURL: 'http://localhost:8080', + trace: 'on-first-retry', + }, + webServer: { + command: 'npm run dev', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/website/scripts/build.sh b/website/scripts/build.sh new file mode 100755 index 0000000..9d5af19 --- /dev/null +++ b/website/scripts/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +node scripts/copy-readmes.js +bash scripts/generate-api-docs.sh +npx eleventy diff --git a/website/scripts/clean.sh b/website/scripts/clean.sh new file mode 100755 index 0000000..16984e0 --- /dev/null +++ b/website/scripts/clean.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +rm -rf _site +rm -rf node_modules +rm -rf .dart-doc-temp +rm -rf src/docs/*/index.md +rm -rf src/api diff --git a/website/scripts/copy-readmes.js b/website/scripts/copy-readmes.js new file mode 100644 index 0000000..9e8015e --- /dev/null +++ b/website/scripts/copy-readmes.js @@ -0,0 +1,158 @@ +#!/usr/bin/env node + +/** + * Copies package README.md files to docs directories at build time. + * + * Maps each package README to its corresponding docs folder, adding + * the necessary Eleventy frontmatter for the docs layout. + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, '..', '..'); +const docsDir = join(__dirname, '..', 'src', 'docs'); +const zhDocsDir = join(__dirname, '..', 'src', 'zh', 'docs'); + +// Mapping from package directory name to docs slug +const packageToDocsMap = { + 'dart_node_core': { slug: 'core', title: 'dart_node_core', order: 1 }, + 'dart_node_express': { slug: 'express', title: 'dart_node_express', order: 2 }, + 'dart_node_react': { slug: 'react', title: 'dart_node_react', order: 3 }, + 'dart_node_react_native': { slug: 'react-native', title: 'dart_node_react_native', order: 4 }, + 'dart_node_ws': { slug: 'websockets', title: 'dart_node_ws', order: 5 }, + 'dart_node_better_sqlite3': { slug: 'sqlite', title: 'dart_node_better_sqlite3', order: 6 }, + 'dart_node_mcp': { slug: 'mcp', title: 'dart_node_mcp', order: 7 }, + 'dart_logging': { slug: 'logging', title: 'dart_logging', order: 8 }, + 'reflux': { slug: 'reflux', title: 'reflux', order: 9 }, + 'dart_jsx': { slug: 'jsx', title: 'dart_jsx', order: 10 }, +}; + +function generateFrontmatter(config, lang = 'en') { + if (lang === 'zh') { + return `--- +layout: layouts/docs.njk +title: ${config.title} +lang: zh +permalink: /zh/docs/${config.slug}/ +eleventyNavigation: + key: ${config.title} + parent: Packages + order: ${config.order} +--- + +`; + } + return `--- +layout: layouts/docs.njk +title: ${config.title} +eleventyNavigation: + key: ${config.title} + parent: Packages + order: ${config.order} +--- + +`; +} + +function processReadme(content, packageName) { + // Remove the first heading (# package_name) as it will be in the frontmatter title + const lines = content.split('\n'); + let startIndex = 0; + let inCodeBlock = false; + + // Find and skip the first H1 heading (but not inside code blocks) + for (let i = 0; i < lines.length; i++) { + // Track code block state + if (lines[i].startsWith('```')) { + inCodeBlock = !inCodeBlock; + continue; + } + + // Only match H1 headings outside of code blocks + if (!inCodeBlock && lines[i].startsWith('# ')) { + startIndex = i + 1; + // Skip any blank lines immediately after the heading + while (startIndex < lines.length && lines[startIndex].trim() === '') { + startIndex++; + } + break; + } + } + + return lines.slice(startIndex).join('\n').trim(); +} + +function copyEnglishReadmes() { + console.log('Copying English package READMEs to docs...\n'); + + for (const [packageDir, config] of Object.entries(packageToDocsMap)) { + const readmePath = join(rootDir, 'packages', packageDir, 'README.md'); + const docsPath = join(docsDir, config.slug); + const outputPath = join(docsPath, 'index.md'); + + if (!existsSync(readmePath)) { + console.log(` SKIP: ${packageDir} (no README.md)`); + continue; + } + + // Ensure docs directory exists + if (!existsSync(docsPath)) { + mkdirSync(docsPath, { recursive: true }); + console.log(` CREATE: ${config.slug}/`); + } + + // Read README content + const readmeContent = readFileSync(readmePath, 'utf-8'); + + // Process and write to docs + const frontmatter = generateFrontmatter(config); + const processedContent = processReadme(readmeContent, packageDir); + const finalContent = frontmatter + processedContent + '\n'; + + writeFileSync(outputPath, finalContent); + console.log(` COPY: ${packageDir}/README.md -> docs/${config.slug}/index.md`); + } +} + +function copyChineseReadmes() { + console.log('\nCopying Chinese package READMEs to zh/docs...\n'); + + for (const [packageDir, config] of Object.entries(packageToDocsMap)) { + const readmePath = join(rootDir, 'packages', packageDir, 'README_zh.md'); + const docsPath = join(zhDocsDir, config.slug); + const outputPath = join(docsPath, 'index.md'); + + if (!existsSync(readmePath)) { + console.log(` SKIP: ${packageDir} (no README_zh.md)`); + continue; + } + + // Ensure docs directory exists + if (!existsSync(docsPath)) { + mkdirSync(docsPath, { recursive: true }); + console.log(` CREATE: zh/docs/${config.slug}/`); + } + + // Read README content + const readmeContent = readFileSync(readmePath, 'utf-8'); + + // Process and write to docs + const frontmatter = generateFrontmatter(config, 'zh'); + const processedContent = processReadme(readmeContent, packageDir); + const finalContent = frontmatter + processedContent + '\n'; + + writeFileSync(outputPath, finalContent); + console.log(` COPY: ${packageDir}/README_zh.md -> zh/docs/${config.slug}/index.md`); + } +} + +function main() { + copyEnglishReadmes(); + copyChineseReadmes(); + console.log('\nDone!'); +} + +main(); diff --git a/website/scripts/generate-api-docs.js b/website/scripts/generate-api-docs.js index b4f3622..b32e06d 100644 --- a/website/scripts/generate-api-docs.js +++ b/website/scripts/generate-api-docs.js @@ -24,8 +24,15 @@ const WEBSITE_DIR = path.dirname(__dirname); const PROJECT_ROOT = path.dirname(WEBSITE_DIR); const PACKAGES_DIR = path.join(PROJECT_ROOT, 'packages'); const API_OUTPUT_DIR = path.join(WEBSITE_DIR, 'src', 'api'); +const API_OUTPUT_DIR_ZH = path.join(WEBSITE_DIR, 'src', 'zh', 'api'); const TEMP_DIR = path.join(WEBSITE_DIR, '.dart-doc-temp'); +// Supported languages for API docs +const LANGUAGES = [ + { code: '', outputDir: API_OUTPUT_DIR, langAttr: null }, + { code: 'zh', outputDir: API_OUTPUT_DIR_ZH, langAttr: 'zh' }, +]; + const PACKAGES = [ 'dart_node_core', 'dart_node_express', @@ -227,7 +234,7 @@ const runDartDoc = (packageDir, outputDir) => { const escapeYaml = (str) => str.replace(/"/g, '\\"').replace(/\n/g, ' '); -const extractContent = (htmlPath, packageName) => { +const extractContent = (htmlPath, packageName, langPrefix = '') => { const html = fs.readFileSync(htmlPath, 'utf-8'); const dom = new JSDOM(html); const doc = dom.window.document; @@ -242,11 +249,11 @@ const extractContent = (htmlPath, packageName) => { const mainContent = doc.querySelector('#dartdoc-main-content'); return mainContent - ? { title, description, content: processContent(mainContent, packageName, dom) } + ? { title, description, content: processContent(mainContent, packageName, dom, langPrefix) } : null; }; -const processContent = (element, packageName, dom) => { +const processContent = (element, packageName, dom, langPrefix = '') => { const h1 = element.querySelector('h1'); h1?.remove(); @@ -273,7 +280,8 @@ const processContent = (element, packageName, dom) => { // Links like ../dart_node_ws/Foo.html -> /api/dart_node_ws/Foo/ // (removing the duplicate package/package structure) - newHref.startsWith('../') && (newHref = newHref.replace(/^\.\.\//, `/api/`)); + // Apply language prefix for non-English versions + newHref.startsWith('../') && (newHref = newHref.replace(/^\.\.\//, `${langPrefix}/api/`)); // Links like Foo.html -> Foo/ (relative, stays same level) // Links like Foo/bar.html -> Foo/bar/ @@ -285,7 +293,7 @@ const processContent = (element, packageName, dom) => { return element.innerHTML; }; -const createMdFile = (outputPath, title, description, packageName, content, elementName = null) => { +const createMdFile = (outputPath, title, description, packageName, content, elementName = null, langOptions = {}) => { // Get element-specific docs only - no fallbacks const externalLinks = elementName ? (getExternalDocs(elementName, packageName) || []) @@ -302,11 +310,15 @@ ${externalLinks.map(link => `
  • { ensureDir(tempDocDir); runDartDoc(packageDir, tempDocDir); - const outputDir = path.join(API_OUTPUT_DIR, packageName); - ensureDir(outputDir); - - // Use LIBRARY index.html as the package index - // dart doc creates: tempDocDir/packageName/index.html (the library page) - const indexHtml = path.join(tempDocDir, packageName, 'index.html'); - - fs.existsSync(indexHtml) && (() => { - const data = extractContent(indexHtml, packageName); - data && createMdFile( - path.join(outputDir, 'index.md'), - `${packageName} library`, - data.description, - packageName, - data.content - ); - })(); - - // Process all other files in library directory - put them DIRECTLY under package - const libDir = path.join(tempDocDir, packageName); - fs.existsSync(libDir) && processLibraryDir(libDir, outputDir, packageName); + // Process for each language + LANGUAGES.forEach(lang => { + const langPrefix = lang.code ? `/${lang.code}` : ''; + const outputDir = path.join(lang.outputDir, packageName); + ensureDir(outputDir); + + // Use LIBRARY index.html as the package index + // dart doc creates: tempDocDir/packageName/index.html (the library page) + const indexHtml = path.join(tempDocDir, packageName, 'index.html'); + + fs.existsSync(indexHtml) && (() => { + const data = extractContent(indexHtml, packageName, langPrefix); + const langOptions = lang.langAttr + ? { langAttr: lang.langAttr, permalink: `${langPrefix}/api/${packageName}/` } + : {}; + data && createMdFile( + path.join(outputDir, 'index.md'), + `${packageName} library`, + data.description, + packageName, + data.content, + null, + langOptions + ); + })(); + + // Process all other files in library directory - put them DIRECTLY under package + const libDir = path.join(tempDocDir, packageName); + fs.existsSync(libDir) && processLibraryDir(libDir, outputDir, packageName, lang); + }); console.log(`Documentation processed for ${packageName}`); })(); }; -const processLibraryDir = (libDir, outputDir, packageName) => { +const processLibraryDir = (libDir, outputDir, packageName, lang = LANGUAGES[0]) => { const entries = fs.readdirSync(libDir, { withFileTypes: true }); + const langPrefix = lang.code ? `/${lang.code}` : ''; + + // Helper to check if a file should be skipped (sidebar files, library index) + const shouldSkipFile = (fileName) => + fileName.endsWith('-sidebar.html') || + fileName === `${packageName}-library.html` || + fileName === 'index.html'; entries.forEach(entry => { const fullPath = path.join(libDir, entry.name); - // Skip the library index files - already processed as package index - const skipFiles = [`${packageName}-library.html`, `${packageName}-library-sidebar.html`, 'index.html']; - - entry.isFile() && entry.name.endsWith('.html') && !skipFiles.includes(entry.name) && (() => { - const data = extractContent(fullPath, packageName); + entry.isFile() && entry.name.endsWith('.html') && !shouldSkipFile(entry.name) && (() => { + const data = extractContent(fullPath, packageName, langPrefix); const baseName = path.basename(entry.name, '.html'); // Put class files directly under package: /api/package/ClassName/ const classDir = path.join(outputDir, baseName); ensureDir(classDir); + const langOptions = lang.langAttr + ? { langAttr: lang.langAttr, permalink: `${langPrefix}/api/${packageName}/${baseName}/` } + : {}; data && createMdFile( path.join(classDir, 'index.md'), data.title, data.description, packageName, data.content, - baseName // Pass element name for specific external docs + baseName, // Pass element name for specific external docs + langOptions ); })(); @@ -384,19 +413,23 @@ const processLibraryDir = (libDir, outputDir, packageName) => { ensureDir(subOutputDir); fs.readdirSync(fullPath) - .filter(f => f.endsWith('.html')) + .filter(f => f.endsWith('.html') && !shouldSkipFile(f)) .forEach(file => { - const data = extractContent(path.join(fullPath, file), packageName); + const data = extractContent(path.join(fullPath, file), packageName, langPrefix); const baseName = path.basename(file, '.html'); const methodDir = path.join(subOutputDir, baseName); ensureDir(methodDir); + const langOptions = lang.langAttr + ? { langAttr: lang.langAttr, permalink: `${langPrefix}/api/${packageName}/${entry.name}/${baseName}/` } + : {}; data && createMdFile( path.join(methodDir, 'index.md'), data.title, data.description, packageName, data.content, - parentElementName // Use parent class name for external docs + parentElementName, // Use parent class name for external docs + langOptions ); }); })(); @@ -467,10 +500,16 @@ description: Complete API documentation for all dart_node packages const main = async () => { console.log('Generating API documentation for dart_node packages...'); console.log(`Packages directory: ${PACKAGES_DIR}`); - console.log(`Output directory: ${API_OUTPUT_DIR}`); + console.log(`Output directories: ${API_OUTPUT_DIR}, ${API_OUTPUT_DIR_ZH}`); cleanDir(TEMP_DIR); cleanDir(API_OUTPUT_DIR); + // Clean zh/api but preserve zh/api/index.md (hand-written Chinese API index) + // Only clean package subdirectories, not the index + PACKAGES.forEach(pkg => { + const zhPkgDir = path.join(API_OUTPUT_DIR_ZH, pkg); + fs.existsSync(zhPkgDir) && fs.rmSync(zhPkgDir, { recursive: true }); + }); for (const pkg of PACKAGES) { await processPackage(pkg); @@ -480,7 +519,7 @@ const main = async () => { fs.rmSync(TEMP_DIR, { recursive: true }); console.log('\n=== API documentation generation complete ==='); - console.log(`Output: ${API_OUTPUT_DIR}`); + console.log(`Output: ${API_OUTPUT_DIR}, ${API_OUTPUT_DIR_ZH}`); }; main().catch(err => { diff --git a/website/scripts/instrument-js.js b/website/scripts/instrument-js.js new file mode 100644 index 0000000..78435f7 --- /dev/null +++ b/website/scripts/instrument-js.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +/** + * Instruments JavaScript files with Istanbul for coverage tracking. + * This approach tracks all code execution including event handlers. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { createInstrumenter } from 'istanbul-lib-instrument'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const websiteDir = path.join(__dirname, '..'); +const srcDir = path.join(websiteDir, 'src', 'assets', 'js'); +const distDir = path.join(websiteDir, '_site', 'assets', 'js'); + +// Create instrumenter +const instrumenter = createInstrumenter({ + esModules: false, + compact: false, + produceSourceMap: true, + autoWrap: true, + coverageVariable: '__coverage__', + coverageGlobalScope: 'window', + coverageGlobalScopeFunc: false, +}); + +// Get all JS files in source directory +const jsFiles = fs.readdirSync(srcDir).filter(f => f.endsWith('.js')); + +for (const file of jsFiles) { + const srcPath = path.join(srcDir, file); + const distPath = path.join(distDir, file); + + // Read source + const code = fs.readFileSync(srcPath, 'utf-8'); + + // Instrument + const instrumented = instrumenter.instrumentSync(code, srcPath); + + // Write to dist (overwrite the built file) + fs.writeFileSync(distPath, instrumented); + + console.log(`Instrumented: ${file}`); +} + +console.log('\nInstrumentation complete. Run tests now.'); diff --git a/website/scripts/merge-coverage.js b/website/scripts/merge-coverage.js new file mode 100644 index 0000000..033d93d --- /dev/null +++ b/website/scripts/merge-coverage.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; +import v8toIstanbul from 'v8-to-istanbul'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const websiteDir = path.join(__dirname, '..'); +const coverageDir = path.join(websiteDir, 'coverage'); +const srcDir = path.join(websiteDir, 'src', 'assets', 'js'); +const nycOutputDir = path.join(coverageDir, '.nyc_output'); + +// Ensure directories exist +if (!fs.existsSync(nycOutputDir)) fs.mkdirSync(nycOutputDir, { recursive: true }); + +// Read all coverage files +const files = fs.readdirSync(coverageDir) + .filter(f => f.startsWith('coverage-') && f.endsWith('.json')); + +if (files.length === 0) { + console.log('No coverage files found'); + process.exit(0); +} + +// Merge V8 coverage data +const mergedV8 = {}; + +for (const file of files) { + const content = fs.readFileSync(path.join(coverageDir, file), 'utf-8'); + if (content.trim() === '[]' || content.trim() === '') continue; + + const data = JSON.parse(content); + + for (const entry of data) { + if (!entry.url || !entry.source) continue; + + const key = entry.url; + if (!mergedV8[key]) { + mergedV8[key] = { + url: entry.url, + scriptId: entry.scriptId || '0', + source: entry.source, + functions: new Map(), // Use Map to properly merge function coverage + }; + } + + // Merge functions by their offset ranges + if (entry.functions) { + for (const func of entry.functions) { + const rangeKey = `${func.ranges[0].startOffset}-${func.ranges[0].endOffset}`; + const existing = mergedV8[key].functions.get(rangeKey); + + if (!existing) { + // Clone the function data + mergedV8[key].functions.set(rangeKey, JSON.parse(JSON.stringify(func))); + } else { + // Merge counts for each range + for (let i = 0; i < func.ranges.length; i++) { + if (existing.ranges[i]) { + existing.ranges[i].count = Math.max( + existing.ranges[i].count, + func.ranges[i].count + ); + } + } + } + } + } + } +} + +// Convert Map back to array for v8-to-istanbul +for (const key of Object.keys(mergedV8)) { + mergedV8[key].functions = Array.from(mergedV8[key].functions.values()); +} + +// Convert to Istanbul format and generate reports +const istanbulCoverage = {}; + +// Use a temp directory for v8-to-istanbul source files +const tempDir = path.join(coverageDir, '.temp-sources'); +if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); + +for (const [url, v8Data] of Object.entries(mergedV8)) { + const fileName = url.split('/').pop() || 'unknown.js'; + // Use a temp file for v8-to-istanbul, but map to real source path + const tempFile = path.join(tempDir, fileName); + const realSourceFile = path.join(srcDir, fileName); + + // Write source to temp file for v8-to-istanbul to read + fs.writeFileSync(tempFile, v8Data.source); + + try { + const converter = v8toIstanbul(tempFile, 0, { source: v8Data.source }); + await converter.load(); + + // Apply V8 coverage + converter.applyCoverage(v8Data.functions); + + // Get Istanbul format + const istanbul = converter.toIstanbul(); + + // Remap the path to the real source file + for (const [tempPath, data] of Object.entries(istanbul)) { + data.path = realSourceFile; + istanbulCoverage[realSourceFile] = data; + } + } catch (err) { + console.error(`Error converting ${fileName}:`, err.message); + } +} + +// Clean up temp directory +fs.rmSync(tempDir, { recursive: true, force: true }); + +// Write Istanbul coverage +const istanbulFile = path.join(nycOutputDir, 'coverage.json'); +fs.writeFileSync(istanbulFile, JSON.stringify(istanbulCoverage, null, 2)); + +// Generate HTML and LCOV reports using nyc +console.log('\nGenerating coverage reports...\n'); + +try { + execSync(`npx nyc report --reporter=html --reporter=lcov --reporter=text --temp-dir="${nycOutputDir}" --report-dir="${coverageDir}" --include="src/assets/js/**/*.js"`, { + cwd: websiteDir, + stdio: 'inherit', + }); +} catch (err) { + console.error('Failed to generate reports:', err.message); +} + +// Clean up individual coverage files +for (const file of files) { + fs.unlinkSync(path.join(coverageDir, file)); +} + +console.log(`\nHTML report: ${path.join(coverageDir, 'index.html')}`); +console.log(`LCOV report: ${path.join(coverageDir, 'lcov.info')}`); +console.log(''); diff --git a/website/scripts/rebuild.sh b/website/scripts/rebuild.sh new file mode 100755 index 0000000..e156947 --- /dev/null +++ b/website/scripts/rebuild.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +bash scripts/clean.sh +npm install +bash scripts/build.sh diff --git a/website/scripts/test.sh b/website/scripts/test.sh new file mode 100755 index 0000000..87812bc --- /dev/null +++ b/website/scripts/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +# Install Playwright browsers if needed +npx playwright install --with-deps chromium + +# Run tests +npm test diff --git a/website/src/_data/i18n.json b/website/src/_data/i18n.json new file mode 100644 index 0000000..bc45269 --- /dev/null +++ b/website/src/_data/i18n.json @@ -0,0 +1,128 @@ +{ + "en": { + "nav": { + "docs": "Docs", + "api": "API", + "blog": "Blog", + "github": "GitHub" + }, + "hero": { + "title": "Full-Stack Dart for the
    JavaScript Ecosystem", + "subtitle": "Write React, React Native, and Express applications entirely in Dart. One language. Runtime type safety. Sound null safety. No compromises.", + "getStarted": "Get Started", + "whyDart": "Why Dart?" + }, + "audience": { + "title": "Built for You", + "subtitle": "Whether you're coming from React or Flutter, dart_node speaks your language.", + "reactTab": "React / React Native Developers", + "flutterTab": "Flutter Developers", + "sameParadigms": "Same Paradigms", + "sameParadigmsDesc": "Hooks, components, props, state — everything you know from React works the same way in Dart.", + "runtimeTypeSafety": "Runtime Type Safety", + "runtimeTypeSafetyDesc": "Unlike TypeScript, Dart preserves types at runtime. No more any escapes or erased generics.", + "simplerTooling": "Simpler Tooling", + "simplerToolingDesc": "No webpack, no babel, no tsconfig. Just dart compile js and you're done.", + "sameLanguage": "Same Language", + "sameLanguageDesc": "Use your existing Dart skills. Share models, utilities, and business logic across platforms.", + "webEcosystem": "Web Ecosystem Access", + "webEcosystemDesc": "Leverage the massive React and npm ecosystems while writing pure Dart code.", + "fullStackDart": "Full-Stack Dart", + "fullStackDartDesc": "Backend (Express), web (React), mobile (React Native) — all in one language." + }, + "stack": { + "title": "The dart_node Stack", + "subtitle": "Packages and tools that give you full-stack superpowers.", + "learnMore": "Learn more" + }, + "types": { + "title": "Why Types Matter at Runtime", + "subtitle": "TypeScript erases types when it compiles to JavaScript. Dart doesn't.", + "learnMore": "Learn More About Dart's Type System" + }, + "getStarted": { + "title": "Get Started in Minutes", + "readGuide": "Read the Full Guide" + }, + "footer": { + "documentation": "Documentation", + "gettingStarted": "Getting Started", + "apiReference": "API Reference", + "examples": "Examples", + "community": "Community", + "discord": "Discord", + "twitter": "Twitter", + "more": "More", + "dartOfficial": "Dart Official", + "flutter": "Flutter", + "copyright": "dart_node. Built with Dart.", + "tagline": "Made for React developers. Made for Flutter developers. Made for everyone." + }, + "language": { + "switchLabel": "Language" + } + }, + "zh": { + "nav": { + "docs": "文档", + "api": "API", + "blog": "博客", + "github": "GitHub" + }, + "hero": { + "title": "面向 JavaScript 生态系统的
    全栈 Dart", + "subtitle": "完全使用 Dart 编写 React、React Native 和 Express 应用程序。一种语言。运行时类型安全。健全的空安全。无妥协。", + "getStarted": "快速开始", + "whyDart": "为什么选择 Dart?" + }, + "audience": { + "title": "为您打造", + "subtitle": "无论您来自 React 还是 Flutter,dart_node 都能说您的语言。", + "reactTab": "React / React Native 开发者", + "flutterTab": "Flutter 开发者", + "sameParadigms": "相同的范式", + "sameParadigmsDesc": "Hooks、组件、props、状态 — 您在 React 中了解的一切在 Dart 中以相同的方式工作。", + "runtimeTypeSafety": "运行时类型安全", + "runtimeTypeSafetyDesc": "与 TypeScript 不同,Dart 在运行时保留类型。不再有 any 转义或被擦除的泛型。", + "simplerTooling": "更简单的工具链", + "simplerToolingDesc": "无需 webpack、babel、tsconfig。只需 dart compile js 即可完成。", + "sameLanguage": "相同的语言", + "sameLanguageDesc": "使用您现有的 Dart 技能。跨平台共享模型、工具和业务逻辑。", + "webEcosystem": "Web 生态系统访问", + "webEcosystemDesc": "在编写纯 Dart 代码的同时,利用庞大的 React 和 npm 生态系统。", + "fullStackDart": "全栈 Dart", + "fullStackDartDesc": "后端(Express)、Web(React)、移动端(React Native)— 全部使用一种语言。" + }, + "stack": { + "title": "dart_node 技术栈", + "subtitle": "为您提供全栈超能力的包和工具。", + "learnMore": "了解更多" + }, + "types": { + "title": "为什么运行时类型很重要", + "subtitle": "TypeScript 在编译为 JavaScript 时会擦除类型。Dart 不会。", + "learnMore": "了解更多关于 Dart 类型系统" + }, + "getStarted": { + "title": "几分钟内开始使用", + "readGuide": "阅读完整指南" + }, + "footer": { + "documentation": "文档", + "gettingStarted": "快速开始", + "apiReference": "API 参考", + "examples": "示例", + "community": "社区", + "discord": "Discord", + "twitter": "Twitter", + "more": "更多", + "dartOfficial": "Dart 官网", + "flutter": "Flutter", + "copyright": "dart_node。使用 Dart 构建。", + "tagline": "为 React 开发者打造。为 Flutter 开发者打造。为每个人打造。" + }, + "language": { + "switchLabel": "语言" + } + } +} diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json new file mode 100644 index 0000000..e0fa6e4 --- /dev/null +++ b/website/src/_data/languages.json @@ -0,0 +1,14 @@ +{ + "en": { + "code": "en", + "name": "English", + "nativeName": "English", + "dir": "ltr" + }, + "zh": { + "code": "zh", + "name": "Chinese", + "nativeName": "中文", + "dir": "ltr" + } +} diff --git a/website/src/_data/navigation.json b/website/src/_data/navigation.json index 21f3e38..61d703b 100644 --- a/website/src/_data/navigation.json +++ b/website/src/_data/navigation.json @@ -78,15 +78,10 @@ { "text": "reflux", "url": "/docs/reflux/" - } - ] - }, - { - "title": "Tools", - "items": [ + }, { - "text": "Too Many Cooks", - "url": "/docs/too-many-cooks/" + "text": "dart_jsx", + "url": "/docs/jsx/" } ] } diff --git a/website/src/_data/navigation_zh.json b/website/src/_data/navigation_zh.json new file mode 100644 index 0000000..51f6791 --- /dev/null +++ b/website/src/_data/navigation_zh.json @@ -0,0 +1,142 @@ +{ + "main": [ + { + "text": "文档", + "url": "/zh/docs/getting-started/" + }, + { + "text": "API", + "url": "/zh/api/" + }, + { + "text": "博客", + "url": "/zh/blog/" + }, + { + "text": "GitHub", + "url": "https://github.com/melbournedeveloper/dart_node", + "external": true + } + ], + "docs": [ + { + "title": "简介", + "items": [ + { + "text": "快速开始", + "url": "/zh/docs/getting-started/" + }, + { + "text": "为什么选择 Dart?", + "url": "/zh/docs/why-dart/" + }, + { + "text": "Dart 到 JavaScript", + "url": "/zh/docs/dart-to-js/" + }, + { + "text": "JS 互操作", + "url": "/zh/docs/js-interop/" + } + ] + }, + { + "title": "包", + "items": [ + { + "text": "dart_node_core", + "url": "/zh/docs/core/" + }, + { + "text": "dart_node_express", + "url": "/zh/docs/express/" + }, + { + "text": "dart_node_react", + "url": "/zh/docs/react/" + }, + { + "text": "dart_node_react_native", + "url": "/zh/docs/react-native/" + }, + { + "text": "dart_node_ws", + "url": "/zh/docs/websockets/" + }, + { + "text": "dart_node_better_sqlite3", + "url": "/zh/docs/sqlite/" + }, + { + "text": "dart_node_mcp", + "url": "/zh/docs/mcp/" + }, + { + "text": "dart_logging", + "url": "/zh/docs/logging/" + }, + { + "text": "reflux", + "url": "/zh/docs/reflux/" + }, + { + "text": "dart_jsx", + "url": "/zh/docs/jsx/" + } + ] + } + ], + "footer": [ + { + "title": "文档", + "items": [ + { + "text": "快速开始", + "url": "/zh/docs/getting-started/" + }, + { + "text": "API 参考", + "url": "/zh/api/" + }, + { + "text": "示例", + "url": "/zh/docs/examples/" + } + ] + }, + { + "title": "社区", + "items": [ + { + "text": "GitHub", + "url": "https://github.com/melbournedeveloper/dart_node" + }, + { + "text": "Discord", + "url": "#" + }, + { + "text": "Twitter", + "url": "https://twitter.com/dart_node" + } + ] + }, + { + "title": "更多", + "items": [ + { + "text": "博客", + "url": "/zh/blog/" + }, + { + "text": "Dart 官网", + "url": "https://dart.dev" + }, + { + "text": "Flutter", + "url": "https://flutter.dev" + } + ] + } + ] +} diff --git a/website/src/_includes/layouts/api.njk b/website/src/_includes/layouts/api.njk index c141501..1330655 100644 --- a/website/src/_includes/layouts/api.njk +++ b/website/src/_includes/layouts/api.njk @@ -2,28 +2,30 @@ layout: layouts/base.njk --- +{% set langPrefix = '/zh' if lang == 'zh' else '' %} +
    diff --git a/website/src/_includes/layouts/base.njk b/website/src/_includes/layouts/base.njk index ebc6fa5..63542d0 100644 --- a/website/src/_includes/layouts/base.njk +++ b/website/src/_includes/layouts/base.njk @@ -1,6 +1,14 @@ - + + @@ -9,26 +17,40 @@ + + + + + + + + - + - + + + + + - - - - - + + + + + + + @@ -43,8 +65,29 @@ + + + + + + + - + + + + + {% if "/docs/" in page.url %} + + + {% endif %} + + + + + + + + {% block head %}{% endblock %} @@ -67,12 +238,13 @@