diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 9f5e289..2b3aa55 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -17,11 +17,15 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/setup-node@v5 - - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - uses: actions/checkout@v6 with: repository: Next2D/player ref: main + - run: npm install -g npm@latest - run: npm install - run: npm run test @@ -31,11 +35,14 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/setup-node@v5 - - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - uses: actions/checkout@v6 with: repository: Next2D/player ref: main + - run: npm install -g npm@latest - run: npm install - - run: npm run test - + - run: npm run test \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d4f9e53..3e81192 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,8 +17,12 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -g npm@latest - run: npm install - run: npx eslint ./src/**/*.ts @@ -28,7 +32,11 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -g npm@latest - run: npm install - run: npx eslint ./src/**/*.ts \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 892042c..ba3b6cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,21 +5,22 @@ on: branches: - main +permissions: + id-token: write + contents: read + jobs: - build: + publish: runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: "22.x" + node-version: 24 registry-url: "https://registry.npmjs.org" + - run: npm install -g npm@latest - run: npm run create:package - run: npm install - run: npm run release - - run: cd ~/work/framework/framework/dist && npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} \ No newline at end of file + - run: npm publish + working-directory: ./dist \ No newline at end of file diff --git a/.gitignore b/.gitignore index 265f50c..93c859f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ dist-ssr *.sln *.sw? -package-lock.json \ No newline at end of file +coverage \ No newline at end of file diff --git a/Framework_Flowchart.svg b/Framework_Flowchart.svg deleted file mode 100644 index b713d50..0000000 --- a/Framework_Flowchart.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Request
User
[Source] src/App.js
[Function] gotoView({Path})
YES
NO
use loading
(Default value is true)
[Source] config.loading.{ClassName}
[Function] start
JSON
Get external JSON data
CONTENT
Get JSON data exported by Animation Tool
CUSTOM
Requests to external APIs
YES
NO
use cache
(Default value is false)
cache
Global
NO
Cached
[Source] src/view/{Path}View.js
[Function] initialize
[Source] src/view/{Path}ViewModel.js
[Function] bind
[Source] src/view/{PrevPath}ViewModel.js
[Function] unbind
YES
NO
use callback
(Default value is empty)
[Source] config.loading.{ClassName}
[Function] start
YES
NO
use loading
(Default value is true)
[Source] config.loading.{ClassName}
[Function] end
Response
Start drawing
YES
Remove Response Data

{ app } from "@next2d/framework"
responce = app.getResponce()
Response Data Registration

{ app } from "@next2d/framework"
responce = app.getResponce()
\ No newline at end of file diff --git a/README.md b/README.md index b3c6442..60e6c74 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Next2D Framework [![UnitTest](https://github.com/Next2D/Framework/actions/workflows/integration.yml/badge.svg?branch=main)](https://github.com/Next2D/Framework/actions/workflows/integration.yml) -[![CodeQL](https://github.com/Next2D/Framework/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/Next2D/Framework/actions/workflows/codeql-analysis.yml) +[![CodeQL](https://github.com/Next2D/framework/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/Next2D/framework/actions/workflows/github-code-scanning/codeql) [![Lint](https://github.com/Next2D/Framework/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/Next2D/Framework/actions/workflows/lint.yml) [![release](https://img.shields.io/github/v/release/Next2D/Framework)](https://github.com/Next2D/Framework/releases) @@ -16,23 +16,78 @@ Next2D Framework [日本語] Next2D Frameworkは、クリーンアーキテクチャー、ドメイン駆動開発、テスト駆動開発、MVVMの原則に従って設計されおり、柔軟性、拡張性、保守性に重点を置いたアーキテクチャーとデザイン手法で各レイヤーを疎結合に保つ事が可能です。 -従来のCanvas/WebGLアプリケーションでは困難だったURLによるシーン管理(SPA)を可能にし、シーン毎のUI開発・画面確認が可能になりました。UI構築にはアトミックデザインを推奨しており、コンポーネントの細分化、再利用可能なモジュール設計など、効率的なUI構築と保守が可能となっています。 +従来のCanvasアプリケーションでは困難だったURLによるシーン管理(SPA)を可能にし、シーン毎のUI開発・画面確認が可能になりました。UI構築にはアトミックデザインを推奨しており、コンポーネントの細分化、再利用可能なモジュール設計など、効率的なUI構築と保守が可能となっています。 また、テスト駆動開発を重視しているため、ユニットテスト、統合テスト、UIテストなど、さまざまなレベルでテストを行いながら、高品質なコードの開発をサポートします。 [English] -Next2D Framework is designed according to the principles of clean architecture, domain-driven development, test-driven development, and MVVM, with an emphasis on flexibility, scalability, and maintainability, and a design methodology that keeps each layer loosely coupled. +The Next2D Framework is designed according to principles of clean architecture, domain-driven development, test-driven development, and MVVM. Its architecture and design methodology prioritize flexibility, scalability, and maintainability, enabling loose coupling between layers. -It is designed according to the principles of MVVM, with an architecture and design methodology that focuses on flexibility, scalability, and maintainability, and keeps each layer loosely coupled. The UI can be efficiently built and maintained by subdividing components and designing modules that can be reused. - -In addition, the emphasis on test-driven development supports the development of high-quality code while testing at various levels, including unit tests, integration tests, and UI tests. +It enables scene management via URLs (SPA), which was difficult with traditional Canvas applications, allowing UI development and screen verification per scene. We recommend Atomic Design for UI construction, enabling efficient UI development and maintenance through component granularity and reusable module design. + +Furthermore, with a strong emphasis on test-driven development, it supports the creation of high-quality code by facilitating testing at various levels, including unit tests, integration tests, and UI tests. [简体中文] Next2D框架是根据简洁架构、领域驱动开发、测试驱动开发和MVVM的原则设计的,其架构和设计方法注重灵活性、可扩展性和可维护性,使每一层都能保持松散耦合。 -它可以通过URL(SPA)实现场景管理,这在传统的Canvas/WebGL应用程序中是很难实现的,并且可以为每个场景进行UI开发和屏幕检查。 该系统能够实现高效的UI构建和维护。 +它可以通过URL(SPA)实现场景管理,这在传统的Canvas应用程序中是很难实现的,并且可以为每个场景进行UI开发和屏幕检查。 该系统能够实现高效的UI构建和维护。 此外,对测试驱动开发的强调支持高质量代码的开发,同时在各个层面进行测试,包括单元测试、集成测试和UI测试。 + +## Architecture + +``` +src/ +├── application/ # Application Layer +│ ├── Application.ts # Main application class +│ ├── Context.ts # View/ViewModel context management +│ ├── content/ # Content classes (MovieClip, Shape, TextField, Video) +│ ├── service/ # Application services (pure functions) +│ │ ├── QueryStringParserService.ts +│ │ └── RoutingRequestsParserService.ts +│ ├── usecase/ # Application use cases (with side effects) +│ │ ├── ApplicationGotoViewUseCase.ts +│ │ ├── ApplicationInitializeUseCase.ts +│ │ ├── ContextRunUseCase.ts +│ │ └── ExecuteCallbackUseCase.ts +│ └── variable/ # Application state (Config, Context, Cache, Packages, Query) +├── domain/ # Domain Layer +│ ├── entity/ # Domain entities +│ │ └── DefaultLoader.ts +│ ├── service/ # Domain services +│ │ ├── LoadingService.ts +│ │ ├── ScreenOverlayService.ts +│ │ └── ViewBinderService.ts +│ └── variable/ # Domain state +├── infrastructure/ # Infrastructure Layer +│ ├── dto/ # Data Transfer Objects +│ │ └── ResponseDTO.ts +│ ├── repository/ # External data access +│ │ ├── ContentRepository.ts +│ │ ├── CustomRepository.ts +│ │ └── JsonRepository.ts +│ ├── service/ # Infrastructure services +│ │ ├── RequestCacheCheckService.ts +│ │ └── RequestResponseProcessService.ts +│ ├── usecase/ # Infrastructure use cases +│ │ ├── RequestUseCase.ts +│ │ └── ResponseRemoveVariableUseCase.ts +│ └── variable/ # Infrastructure state +├── interface/ # TypeScript interfaces +│ ├── IConfig.ts # Configuration interface +│ ├── IRequest.ts # Request interface +│ ├── IRouting.ts # Routing interface +│ └── ... +├── shared/ # Shared utilities +│ └── util/ # Pure utility functions +│ ├── NormalizeHttpMethod.ts +│ ├── ParseQueryString.ts +│ └── ToCamelCase.ts +└── view/ # View Layer + ├── View.ts # Base View class (abstract) + └── ViewModel.ts # Base ViewModel class (abstract) +``` + ## Support [日本語] @@ -69,8 +124,180 @@ cd app-name npm start ``` -## Flowchart -![Flowchart](./Framework_Flowchart.svg) +## API Reference + +### Application + +| Method | Description | +|--------|-------------| +| `gotoView(name?)` | Navigate to a View. If no argument, parses URL | +| `getContext()` | Get the current Context | +| `getResponse()` | Get the response data Map | +| `getCache()` | Get the cache data Map | + +### View Lifecycle + +| Method | Description | +|--------|-------------| +| `initialize()` | Called after constructor | +| `onEnter()` | Called when View is displayed | +| `onExit()` | Called when View is hidden | + +### ViewModel Lifecycle + +| Method | Description | +|--------|-------------| +| `initialize()` | Called after constructor | + +### Context + +| Property/Method | Description | +|-----------------|-------------| +| `view` | Current View instance | +| `viewModel` | Current ViewModel instance | +| `root` | Root Sprite on Stage | + +### Exported Classes + +```typescript +import { + app, // Application instance + View, // Base View class + ViewModel, // Base ViewModel class + MovieClipContent, // MovieClip content from Animation Tool + ShapeContent, // Shape content from Animation Tool + TextFieldContent, // TextField content from Animation Tool + VideoContent // Video content from Animation Tool +} from "@next2d/framework"; +``` + +## Configuration + +### IConfig + +```typescript +interface IConfig { + platform: string; // "web" | "app" + spa: boolean; // Enable SPA mode + defaultTop?: string; // Default top page name (default: "top") + stage: { + width: number; // Stage width + height: number; // Stage height + fps: number; // Frame rate + options?: { + base?: string; + fullScreen?: boolean; + tagId?: string; + bgColor?: string; + }; + }; + loading?: { + callback: string; // Loading class name + }; + gotoView?: { + callback: string | string[]; // Callback after view transition + }; + routing?: { + [key: string]: { + private?: boolean; + redirect?: string; + requests?: IRequest[]; + }; + }; +} +``` + +### IRequest + +```typescript +interface IRequest { + type: "json" | "content" | "custom" | "cluster"; + path?: string; // URL path + name?: string; // Response key name + cache?: boolean; // Enable caching + callback?: string | string[]; + // For custom type + class?: string; + access?: "static" | "instance"; + method?: string; + // For HTTP requests + headers?: HeadersInit; + body?: any; +} +``` + +## Flowchart + +```mermaid +graph TD + User([User]) -->|Request| GotoView[gotoView Path] + + GotoView --> LoadingCheck{use loading?
Default: true} + + LoadingCheck -->|YES| ScreenOverlay[Screen Overlay] + LoadingCheck -->|NO| RemoveResponse + ScreenOverlay --> LoadingStart[Start Loading] + LoadingStart --> RemoveResponse + + RemoveResponse[Remove Previous Response Data] --> ParseQuery[Parse Query String] + ParseQuery --> UpdateHistory{SPA mode?} + + UpdateHistory -->|YES| PushState[Push History State] + UpdateHistory -->|NO| RequestType + PushState --> RequestType + + RequestType[Request Type] + + RequestType --> JSON[JSON: Get external JSON data] + RequestType --> CONTENT[CONTENT: Get Animation Tool JSON] + RequestType --> CUSTOM[CUSTOM: Request to external API] + + JSON --> CacheCheck{use cache?
Default: false} + CONTENT --> CacheCheck + CUSTOM --> CacheCheck + + CacheCheck -->|YES| CacheData[(Cache)] + CacheCheck -->|NO| GlobalData{{Global Network}} + + CacheData --> Cached{Cached?} + + Cached -->|NO| GlobalData + Cached -->|YES| RegisterResponse + GlobalData --> RegisterResponse + + RegisterResponse[Register Response Data] --> RequestCallback{request callback?} + + RequestCallback -->|YES| ExecRequestCallback[Execute Request Callback] + RequestCallback -->|NO| UnbindView + ExecRequestCallback --> UnbindView + + UnbindView[Previous View: onExit & Unbind] --> BindView[New View/ViewModel: Bind] + BindView --> ViewModelInit[ViewModel: initialize] + + ViewModelInit --> ViewInit[View: initialize] + ViewInit --> AddToStage[Add View to Stage] + AddToStage --> GotoViewCallback{gotoView callback?} + + GotoViewCallback -->|YES| ExecGotoViewCallback[Execute gotoView Callback] + GotoViewCallback -->|NO| LoadingEndCheck + ExecGotoViewCallback --> LoadingEndCheck + + LoadingEndCheck{use loading?
Default: true} + + LoadingEndCheck -->|YES| LoadingEnd[End Loading] + LoadingEndCheck -->|NO| OnEnter + LoadingEnd --> DisposeOverlay[Dispose Screen Overlay] + DisposeOverlay --> OnEnter + + OnEnter[View: onEnter] --> StartDrawing + + StartDrawing[Start Drawing] -->|Response| User + + style User fill:#d5e8d4,stroke:#82b366 + style StartDrawing fill:#dae8fc,stroke:#6c8ebf + style CacheData fill:#fff2cc,stroke:#d6b656 + style GlobalData fill:#f5f5f5,stroke:#666666 +``` ## License This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [LICENSE](LICENSE) file for details. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7f8b476 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3972 @@ +{ + "name": "@next2d/framework", + "version": "4.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@next2d/framework", + "version": "4.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/web-worker": "^4.0.18", + "eslint": "^9.39.2", + "eslint-plugin-unused-imports": "^4.3.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18", + "vitest-webgl-canvas-mock": "^1.1.0" + }, + "peerDependencies": { + "@next2d/cache": "file:../player/packages/cache", + "@next2d/core": "file:../player/packages/core", + "@next2d/display": "file:../player/packages/display", + "@next2d/events": "file:../player/packages/events", + "@next2d/filters": "file:../player/packages/filters", + "@next2d/geom": "file:../player/packages/geom", + "@next2d/media": "file:../player/packages/media", + "@next2d/net": "file:../player/packages/net", + "@next2d/player": "file:../player", + "@next2d/render-queue": "file:../player/packages/render-queue", + "@next2d/renderer": "file:../player/packages/renderer", + "@next2d/text": "file:../player/packages/text", + "@next2d/texture-packer": "file:../player/packages/texture-packer", + "@next2d/ui": "file:../player/packages/ui", + "@next2d/webgl": "file:../player/packages/webgl" + } + }, + "../player": { + "name": "@next2d/player", + "version": "3.0.0", + "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "fflate": "^0.8.2", + "htmlparser2": "^10.1.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@playwright/test": "^1.58.1", + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.3.0", + "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "@vitest/web-worker": "^4.0.18", + "@webgpu/types": "^0.1.69", + "eslint": "^9.39.2", + "eslint-plugin-unused-imports": "^4.3.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", + "rollup": "^4.57.1", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18", + "vitest-webgl-canvas-mock": "^1.1.0", + "xml2js": "^0.6.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Next2D" + }, + "peerDependencies": { + "@next2d/cache": "file:packages/cache", + "@next2d/core": "file:packages/core", + "@next2d/display": "file:packages/display", + "@next2d/events": "file:packages/events", + "@next2d/filters": "file:packages/filters", + "@next2d/geom": "file:packages/geom", + "@next2d/media": "file:packages/media", + "@next2d/net": "file:packages/net", + "@next2d/render-queue": "file:packages/render-queue", + "@next2d/renderer": "file:packages/renderer", + "@next2d/text": "file:packages/text", + "@next2d/texture-packer": "file:packages/texture-packer", + "@next2d/ui": "file:packages/ui", + "@next2d/webgl": "file:packages/webgl", + "@next2d/webgpu": "file:packages/webgpu" + } + }, + "../player/packages/cache": { + "name": "@next2d/cache", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/core": { + "name": "@next2d/core", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/core": "file:../core", + "@next2d/display": "file:../display", + "@next2d/events": "file:../events", + "@next2d/filters": "file:../filters", + "@next2d/geom": "file:../geom", + "@next2d/media": "file:../media", + "@next2d/net": "file:../net", + "@next2d/render-queue": "file:../render-queue", + "@next2d/renderer": "file:../renderer", + "@next2d/text": "file:../text", + "@next2d/ui": "file:../ui" + } + }, + "../player/packages/display": { + "name": "@next2d/display", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/events": "file:../events", + "@next2d/filters": "file:../filters", + "@next2d/geom": "file:../geom", + "@next2d/media": "file:../media", + "@next2d/net": "file:../net", + "@next2d/render-queue": "file:../render-queue", + "@next2d/text": "file:../text", + "@next2d/ui": "file:../ui" + } + }, + "../player/packages/events": { + "name": "@next2d/events", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/filters": { + "name": "@next2d/filters", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/geom": { + "name": "@next2d/geom", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/media": { + "name": "@next2d/media", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/display": "file:../display", + "@next2d/events": "file:../events", + "@next2d/geom": "file:../geom", + "@next2d/net": "file:../net" + } + }, + "../player/packages/net": { + "name": "@next2d/net", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/render-queue": { + "name": "@next2d/render-queue", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/renderer": { + "name": "@next2d/renderer", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/cache": "file:../cache", + "@next2d/texture-packer": "file:../texture-packer", + "@next2d/webgl": "file:../webgl", + "@next2d/webgpu": "file:../webgpu" + } + }, + "../player/packages/text": { + "name": "@next2d/text", + "version": "*", + "license": "MIT", + "peer": true, + "dependencies": { + "htmlparser2": "^10.0.0" + }, + "peerDependencies": { + "@next2d/cache": "file:../cache", + "@next2d/display": "file:../display", + "@next2d/events": "file:../events", + "@next2d/geom": "file:../geom", + "@next2d/ui": "file:../ui" + } + }, + "../player/packages/texture-packer": { + "name": "@next2d/texture-packer", + "version": "*", + "license": "MIT", + "peer": true + }, + "../player/packages/ui": { + "name": "@next2d/ui", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/events": "file:../events" + } + }, + "../player/packages/webgl": { + "name": "@next2d/webgl", + "version": "*", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@next2d/render-queue": "file:../render-queue", + "@next2d/texture-packer": "file:../texture-packer" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.7.tgz", + "integrity": "sha512-8CO/UQ4tzDd7ula+/CVimJIVWez99UJlbMyIgk8xOnhAVPKLnBZmUFYVgugS441v2ZqUq5EnSh6B0Ua0liSFAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "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/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.11.0.tgz", + "integrity": "sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/@next2d/cache": { + "resolved": "../player/packages/cache", + "link": true + }, + "node_modules/@next2d/core": { + "resolved": "../player/packages/core", + "link": true + }, + "node_modules/@next2d/display": { + "resolved": "../player/packages/display", + "link": true + }, + "node_modules/@next2d/events": { + "resolved": "../player/packages/events", + "link": true + }, + "node_modules/@next2d/filters": { + "resolved": "../player/packages/filters", + "link": true + }, + "node_modules/@next2d/geom": { + "resolved": "../player/packages/geom", + "link": true + }, + "node_modules/@next2d/media": { + "resolved": "../player/packages/media", + "link": true + }, + "node_modules/@next2d/net": { + "resolved": "../player/packages/net", + "link": true + }, + "node_modules/@next2d/player": { + "resolved": "../player", + "link": true + }, + "node_modules/@next2d/render-queue": { + "resolved": "../player/packages/render-queue", + "link": true + }, + "node_modules/@next2d/renderer": { + "resolved": "../player/packages/renderer", + "link": true + }, + "node_modules/@next2d/text": { + "resolved": "../player/packages/text", + "link": true + }, + "node_modules/@next2d/texture-packer": { + "resolved": "../player/packages/texture-packer", + "link": true + }, + "node_modules/@next2d/ui": { + "resolved": "../player/packages/ui", + "link": true + }, + "node_modules/@next2d/webgl": { + "resolved": "../player/packages/webgl", + "link": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/web-worker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.0.18.tgz", + "integrity": "sha512-h9MiAI3nQNVeEH8Tn1p9CwJGmXPJPUTGhzcuQalIk+6fqIazqUDVzDi+NUKrpK6sQKgSa4MonhyDThhtZqH+cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "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/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssfontparser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", + "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz", + "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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-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-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/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/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-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": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz", + "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^5.3.7", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.20.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "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": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", + "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "~0.5.0" + } + }, + "node_modules/parse-color/node_modules/color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==", + "dev": true + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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-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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.22", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.22.tgz", + "integrity": "sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.22" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.22", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.22.tgz", + "integrity": "sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", + "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-webgl-canvas-mock": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vitest-webgl-canvas-mock/-/vitest-webgl-canvas-mock-1.1.0.tgz", + "integrity": "sha512-F/5+XvBs7cSZPe41IGQTbSjNimB4NntPnRqv4eWb42voFKQINH8y2xZkibNUxYJCGIuDFsYp1lDQgTvWLahSzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssfontparser": "^1.2.1", + "parse-color": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", + "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index b5d4752..8a8f43c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@next2d/framework", "description": "Next2D Framework is designed according to the principles of clean architecture, domain-driven development, test-driven development, and MVVM, with an emphasis on flexibility, scalability, and maintainability, and a design methodology that keeps each layer loosely coupled.", - "version": "3.0.13", + "version": "4.0.0", "homepage": "https://next2d.app", "bugs": "https://github.com/Next2D/Framework/issues/new", "author": "Toshiyuki Ienaga (https://github.com/ienaga/)", @@ -31,19 +31,20 @@ "url": "git+https://github.com/Next2D/Framework.git" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.0", - "@types/node": "^24.9.2", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/parser": "^8.46.2", - "@vitest/web-worker": "^4.0.6", - "eslint": "^9.39.0", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/web-worker": "^4.0.18", + "eslint": "^9.39.2", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.4.0", - "jsdom": "^27.1.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", "typescript": "^5.9.3", - "vite": "^7.1.12", - "vitest": "^4.0.6", + "vite": "^7.3.1", + "vitest": "^4.0.18", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { diff --git a/specs/cn/animation-tool.md b/specs/cn/animation-tool.md new file mode 100644 index 0000000..9efdfca --- /dev/null +++ b/specs/cn/animation-tool.md @@ -0,0 +1,140 @@ +# Animation Tool 集成 + +Next2D Framework 与使用 Animation Tool 创建的资源无缝集成。 + +## 概述 + +Animation Tool 是用于为 Next2D Player 创建动画和 UI 组件的工具。输出的 JSON 文件可以在框架中加载并用作 MovieClip。 + +## 目录结构 + +``` +src/ +├── ui/ +│ ├── content/ # Animation Tool 生成的内容 +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── component/ # 原子设计组件 +│ │ ├── atom/ # 最小单元组件 +│ │ │ ├── ButtonAtom.ts +│ │ │ └── TextAtom.ts +│ │ ├── molecule/ # 组合的 Atom 组件 +│ │ │ ├── TopBtnMolecule.ts +│ │ │ └── HomeBtnMolecule.ts +│ │ ├── organism/ # 多个 Molecule 组合 +│ │ ├── template/ # 页面模板 +│ │ └── page/ # 页面组件 +│ │ ├── top/ +│ │ │ └── TopPage.ts +│ │ └── home/ +│ │ └── HomePage.ts +│ │ +│ └── animation/ # 代码动画定义 +│ └── top/ +│ └── TopBtnShowAnimation.ts +│ +└── file/ # Animation Tool 输出文件 + └── sample.n2d +``` + +## MovieClipContent + +包装使用 Animation Tool 创建的内容的类。 + +### 基本结构 + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @see file/sample.n2d + */ +export class TopContent extends MovieClipContent +{ + /** + * 返回在 Animation Tool 中设置的符号名称 + */ + get namespace(): string + { + return "TopContent"; + } +} +``` + +### namespace 的作用 + +`namespace` 属性应与在 Animation Tool 中创建的符号名称匹配。此名称用于从加载的 JSON 数据生成相应的 MovieClip。 + +## 加载内容 + +### routing.json 中的配置 + +Animation Tool JSON 文件通过 `routing.json` 中的 `requests` 加载。 + +```json +{ + "@sample": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}content/sample.json", + "name": "MainContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { + "type": "cluster", + "path": "@sample" + } + ] + } +} +``` + +#### 请求配置 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `type` | string | 指定 `"content"` | +| `path` | string | JSON 文件的路径 | +| `name` | string | 在响应中注册的键名 | +| `cache` | boolean | 是否缓存 | + +#### 集群功能 + +以 `@` 开头的键被定义为集群,可以在多个路由之间共享。使用 `type: "cluster"` 引用它们。 + +```json +{ + "@common": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}common.json", + "name": "CommonContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + }, + "home": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + } +} +``` + +## 相关 + +- [View/ViewModel](/cn/reference/framework/view) +- [路由](/cn/reference/framework/routing) +- [配置](/cn/reference/framework/config) diff --git a/specs/cn/config.md b/specs/cn/config.md new file mode 100644 index 0000000..dd067f5 --- /dev/null +++ b/specs/cn/config.md @@ -0,0 +1,331 @@ +# 配置文件 + +Next2D Framework 配置使用三个 JSON 文件管理。 + +## 文件结构 + +``` +src/config/ +├── stage.json # 显示区域设置 +├── config.json # 环境设置 +└── routing.json # 路由设置 +``` + +## stage.json + +用于设置显示区域(Stage)的 JSON 文件。 + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "transparent" + } +} +``` + +### 属性 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `width` | number | 240 | 显示区域宽度 | +| `height` | number | 240 | 显示区域高度 | +| `fps` | number | 60 | 每秒绘制次数(1-60) | +| `options` | object | null | 选项设置 | + +### options 设置 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `fullScreen` | boolean | false | 超出舞台宽高在整个屏幕上绘制 | +| `tagId` | string | null | 指定后,绘制发生在具有该 ID 的元素内 | +| `bgColor` | string | "transparent" | 十六进制背景颜色。默认为透明 | + +## config.json + +用于管理特定环境设置的文件。分为 `local`、`dev`、`stg`、`prd` 和 `all`,其中除 `all` 以外的任何环境名称都是任意的。 + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +### all 设置 + +`all` 是在任何环境中导出的公共变量。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `spa` | boolean | true | 作为单页应用程序通过 URL 控制场景 | +| `defaultTop` | string | "top" | 页面顶部的 View。如果未设置,将启动 TopView 类 | +| `loading.callback` | string | Loading | 加载画面类名。调用 start 和 end 函数 | +| `gotoView.callback` | string \| array | ["callback.Background"] | gotoView 完成后的回调类 | + +### platform 设置 + +构建时使用 `--platform` 指定的值会被设置。 + +支持的值:`macos`、`windows`、`linux`、`ios`、`android`、`web` + +```typescript +import { config } from "@/config/Config"; + +if (config.platform === "ios") { + // iOS 特定处理 +} +``` + +## routing.json + +路由配置文件。详情请参阅[路由](/cn/reference/framework/routing)。 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [] + } +} +``` + +## 获取配置值 + +在代码中使用 `config` 对象获取配置值。 + +### Config.ts 示例 + +```typescript +import stageJson from "./stage.json"; +import configJson from "./config.json"; + +interface IStageConfig { + width: number; + height: number; + fps: number; + options: { + fullScreen: boolean; + tagId: string | null; + bgColor: string; + }; +} + +interface IConfig { + stage: IStageConfig; + api: { + endPoint: string; + }; + content: { + endPoint: string; + }; + spa: boolean; + defaultTop: string; + platform: string; +} + +export const config: IConfig = { + stage: stageJson, + ...configJson +}; +``` + +### 使用示例 + +```typescript +import { config } from "@/config/Config"; + +// 舞台设置 +const stageWidth = config.stage.width; +const stageHeight = config.stage.height; + +// API 设置 +const apiEndPoint = config.api.endPoint; + +// SPA 设置 +const isSpa = config.spa; +``` + +## 加载画面 + +调用 `loading.callback` 中设置的类的 `start` 和 `end` 函数。 + +```typescript +export class Loading +{ + private shape: Shape; + + constructor() + { + this.shape = new Shape(); + // 初始化加载显示 + } + + start(): void + { + // 加载开始时的处理 + stage.addChild(this.shape); + } + + end(): void + { + // 加载结束时的处理 + this.shape.remove(); + } +} +``` + +## gotoView 回调 + +调用 `gotoView.callback` 中设置的类的 `execute` 函数。可以设置多个类作为数组,并使用 async/await 顺序执行。 + +```typescript +import { app } from "@next2d/framework"; +import { Shape, stage } from "@next2d/display"; + +export class Background +{ + public readonly shape: Shape; + + constructor() + { + this.shape = new Shape(); + } + + execute(): void + { + const context = app.getContext(); + const view = context.view; + if (!view) return; + + // 将背景放在后面 + view.addChildAt(this.shape, 0); + } +} +``` + +## 构建命令 + +带环境指定的构建: + +```bash +# 本地环境 +npm run build -- --env=local + +# 开发环境 +npm run build -- --env=dev + +# 生产环境 +npm run build -- --env=prd +``` + +指定平台: + +```bash +npm run build -- --platform=web +npm run build -- --platform=ios +npm run build -- --platform=android +``` + +## 配置示例 + +### 完整配置文件示例 + +#### stage.json + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "#1461A0" + } +} +``` + +#### config.json + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/mock/content/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + }, + "content": { + "endPoint": "https://dev-cdn.example.com/content/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + }, + "content": { + "endPoint": "https://cdn.example.com/content/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +## 相关 + +- [路由](/cn/reference/framework/routing) +- [View/ViewModel](/cn/reference/framework/view) diff --git a/specs/cn/index.md b/specs/cn/index.md new file mode 100644 index 0000000..620fed7 --- /dev/null +++ b/specs/cn/index.md @@ -0,0 +1,345 @@ +# Next2D Framework + +Next2D Framework 是一个用于构建 Next2D Player 应用程序的 MVVM 框架。它提供单页应用程序(SPA)的路由、View/ViewModel 管理和配置管理。 + +## 主要特性 + +- **MVVM 模式**:通过 Model-View-ViewModel 分离关注点 +- **Clean Architecture**:依赖倒置和松耦合设计 +- **单页应用程序**:基于 URL 的场景管理 +- **Animation Tool 集成**:与 Animation Tool 资源无缝集成 +- **TypeScript 支持**:类型安全的开发 +- **原子设计**:推荐的可重用组件设计 + +## 架构概述 + +本项目实现了 Clean Architecture 和 MVVM 模式的组合。 + +```mermaid +graph TB + subgraph ViewLayer["视图层"] + View["View"] + ViewModel["ViewModel"] + UI["UI 组件"] + end + + subgraph InterfaceLayer["接口层"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IResponse"] + end + + subgraph ApplicationLayer["应用层"] + UseCase["UseCase"] + end + + subgraph DomainLayer["领域层"] + DomainLogic["领域逻辑"] + DomainService["Service"] + end + + subgraph InfraLayer["基础设施层"] + Repository["Repository"] + ExternalAPI["外部 API"] + end + + ViewLayer -.->|通过接口| InterfaceLayer + ViewLayer -.->|调用| ApplicationLayer + ApplicationLayer -.->|通过接口| InterfaceLayer + ApplicationLayer -.->|使用| DomainLayer + ApplicationLayer -.->|调用| InfraLayer + InfraLayer -.->|访问| ExternalAPI +``` + +### 层职责 + +| 层 | 路径 | 角色 | +|---|------|------| +| **View** | `view/*`, `ui/*` | 处理画面结构和显示 | +| **ViewModel** | `view/*` | View 和 Model 之间的桥梁,事件处理 | +| **Interface** | `interface/*` | 抽象层,类型定义 | +| **Application** | `model/application/*/usecase/*` | 业务逻辑实现(UseCase) | +| **Domain** | `model/domain/*` | 核心业务规则 | +| **Infrastructure** | `model/infrastructure/repository/*` | 数据访问,外部 API 集成 | + +### 依赖方向 + +遵循 Clean Architecture 原则,依赖始终指向内部(领域层)。 + +- **视图层**:通过接口使用应用层 +- **应用层**:通过接口使用领域层和基础设施层 +- **领域层**:不依赖任何东西(纯业务逻辑) +- **基础设施层**:实现领域层接口 + +## 目录结构 + +``` +my-app/ +├── src/ +│ ├── config/ # 配置文件 +│ │ ├── stage.json # 舞台设置 +│ │ ├── config.json # 环境设置 +│ │ ├── routing.json # 路由设置 +│ │ └── Config.ts # 配置类型定义和导出 +│ │ +│ ├── interface/ # 接口定义 +│ │ ├── IDraggable.ts # 可拖动对象 +│ │ ├── ITextField.ts # 文本字段 +│ │ ├── IHomeTextResponse.ts # API 响应类型 +│ │ └── IViewName.ts # 视图名称类型定义 +│ │ +│ ├── view/ # View 和 ViewModel +│ │ ├── top/ +│ │ │ ├── TopView.ts # 画面结构定义 +│ │ │ └── TopViewModel.ts # 业务逻辑桥梁 +│ │ └── home/ +│ │ ├── HomeView.ts +│ │ └── HomeViewModel.ts +│ │ +│ ├── model/ +│ │ ├── application/ # 应用层 +│ │ │ ├── top/ +│ │ │ │ └── usecase/ +│ │ │ │ └── NavigateToViewUseCase.ts +│ │ │ └── home/ +│ │ │ └── usecase/ +│ │ │ ├── StartDragUseCase.ts +│ │ │ ├── StopDragUseCase.ts +│ │ │ └── CenterTextFieldUseCase.ts +│ │ │ +│ │ ├── domain/ # 领域层 +│ │ │ └── callback/ +│ │ │ ├── Background.ts +│ │ │ └── Background/ +│ │ │ └── service/ +│ │ │ ├── BackgroundDrawService.ts +│ │ │ └── BackgroundChangeScaleService.ts +│ │ │ +│ │ └── infrastructure/ # 基础设施层 +│ │ └── repository/ +│ │ └── HomeTextRepository.ts +│ │ +│ ├── ui/ # UI 组件 +│ │ ├── animation/ # 动画定义 +│ │ │ └── top/ +│ │ │ └── TopBtnShowAnimation.ts +│ │ │ +│ │ ├── component/ # 原子设计 +│ │ │ ├── atom/ # 最小单元组件 +│ │ │ │ ├── ButtonAtom.ts +│ │ │ │ └── TextAtom.ts +│ │ │ ├── molecule/ # 组合的 Atom 组件 +│ │ │ │ ├── HomeBtnMolecule.ts +│ │ │ │ └── TopBtnMolecule.ts +│ │ │ ├── organism/ # 多个 Molecule 组合 +│ │ │ ├── template/ # 页面模板 +│ │ │ └── page/ # 页面组件 +│ │ │ ├── top/ +│ │ │ │ └── TopPage.ts +│ │ │ └── home/ +│ │ │ └── HomePage.ts +│ │ │ +│ │ └── content/ # Animation Tool 生成的内容 +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── assets/ # 静态资源 +│ │ +│ ├── Packages.ts # 包导出 +│ └── index.ts # 入口点 +│ +├── file/ # Animation Tool 输出文件 +│ └── sample.n2d +│ +├── mock/ # 模拟数据 +│ ├── api/ # API 模拟 +│ ├── content/ # 内容模拟 +│ └── img/ # 图像模拟 +│ +└── package.json +``` + +## 框架流程图 + +使用 gotoView 函数的画面转换详细流程。 + +```mermaid +graph TD + User([用户]) -->|请求| GotoView[gotoView Path] + + GotoView --> LoadingCheck{使用加载?
默认: true} + + LoadingCheck -->|是| ScreenOverlay[画面遮罩] + LoadingCheck -->|否| RemoveResponse + ScreenOverlay --> LoadingStart[开始加载] + LoadingStart --> RemoveResponse + + RemoveResponse[移除前一个响应数据] --> ParseQuery[解析查询字符串] + ParseQuery --> UpdateHistory{SPA 模式?} + + UpdateHistory -->|是| PushState[推送历史状态] + UpdateHistory -->|否| RequestType + PushState --> RequestType + + RequestType[请求类型] + + RequestType --> JSON[JSON: 获取外部 JSON 数据] + RequestType --> CONTENT[CONTENT: 获取 Animation Tool JSON] + RequestType --> CUSTOM[CUSTOM: 请求外部 API] + + JSON --> CacheCheck{使用缓存?
默认: false} + CONTENT --> CacheCheck + CUSTOM --> CacheCheck + + CacheCheck -->|是| CacheData[(缓存)] + CacheCheck -->|否| GlobalData{{全球网络}} + + CacheData --> Cached{已缓存?} + + Cached -->|否| GlobalData + Cached -->|是| RegisterResponse + GlobalData --> RegisterResponse + + RegisterResponse[注册响应数据] --> RequestCallback{请求回调?} + + RequestCallback -->|是| ExecRequestCallback[执行请求回调] + RequestCallback -->|否| UnbindView + ExecRequestCallback --> UnbindView + + UnbindView[前一个 View: onExit 和解绑] --> BindView[新 View/ViewModel: 绑定] + BindView --> ViewModelInit[ViewModel: initialize] + + ViewModelInit --> ViewInit[View: initialize] + ViewInit --> AddToStage[将 View 添加到舞台] + AddToStage --> GotoViewCallback{gotoView 回调?} + + GotoViewCallback -->|是| ExecGotoViewCallback[执行 gotoView 回调] + GotoViewCallback -->|否| LoadingEndCheck + ExecGotoViewCallback --> LoadingEndCheck + + LoadingEndCheck{使用加载?
默认: true} + + LoadingEndCheck -->|是| LoadingEnd[结束加载] + LoadingEndCheck -->|否| OnEnter + LoadingEnd --> DisposeOverlay[释放画面遮罩] + DisposeOverlay --> OnEnter + + OnEnter[View: onEnter] --> StartDrawing + + StartDrawing[开始绘制] -->|响应| User + + style User fill:#d5e8d4,stroke:#82b366 + style StartDrawing fill:#dae8fc,stroke:#6c8ebf + style CacheData fill:#fff2cc,stroke:#d6b656 + style GlobalData fill:#f5f5f5,stroke:#666666 +``` + +### 关键流程步骤 + +| 步骤 | 说明 | +|------|------| +| **gotoView** | 画面转换的入口点 | +| **Loading** | 加载画面显示/隐藏控制 | +| **Request Type** | 三种请求类型:JSON、CONTENT、CUSTOM | +| **Cache** | 响应数据缓存控制 | +| **View/ViewModel Bind** | 新 View/ViewModel 的绑定过程 | +| **onEnter** | 画面显示完成后的回调 | + +## 关键设计模式 + +### 1. MVVM(Model-View-ViewModel) + +- **View**:处理画面结构和显示。没有业务逻辑 +- **ViewModel**:View 和 Model 之间的桥梁。持有 UseCase 并处理事件 +- **Model**:处理业务逻辑和数据访问 + +### 2. UseCase 模式 + +为每个用户操作创建专用的 UseCase 类: + +```typescript +export class StartDragUseCase +{ + execute(target: IDraggable): void + { + target.startDrag(); + } +} +``` + +### 3. 依赖倒置 + +依赖接口,而不是具体类: + +```typescript +// 好:依赖接口 +import type { IDraggable } from "@/interface/IDraggable"; + +function startDrag(target: IDraggable): void +{ + target.startDrag(); +} +``` + +### 4. Repository 模式 + +抽象数据访问并实现错误处理: + +```typescript +export class HomeTextRepository +{ + static async get(): Promise + { + try { + const response = await fetch(`${config.api.endPoint}api/home.json`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Failed to fetch:", error); + throw error; + } + } +} +``` + +## 快速开始 + +### 创建项目 + +```bash +npx create-next2d-app my-app +cd my-app +npm install +npm start +``` + +### 自动生成 View/ViewModel + +```bash +npm run generate +``` + +此命令解析 `routing.json` 中的顶级属性并生成相应的 View 和 ViewModel 类。 + +## 最佳实践 + +1. **接口优先**:始终依赖接口,而不是具体类型 +2. **单一职责原则**:每个类只有一个职责 +3. **依赖注入**:通过构造函数注入依赖 +4. **错误处理**:在 Repository 层适当处理错误 +5. **类型安全**:避免 `any` 类型,使用显式类型定义 + +## 相关文档 + +### 基础 +- [View/ViewModel](/cn/reference/framework/view) - 画面显示和数据绑定 +- [路由](/cn/reference/framework/routing) - 基于 URL 的画面转换 +- [配置](/cn/reference/framework/config) - 环境和舞台设置 +- [Animation Tool 集成](/cn/reference/framework/animation-tool) - 使用 Animation Tool 资源 + +### Next2D Player 集成 +- [Next2D Player](/cn/reference/player) - 渲染引擎 +- [MovieClip](/cn/reference/player/movie-clip) - 时间轴动画 +- [事件系统](/cn/reference/player/events) - 用户交互 diff --git a/specs/cn/routing.md b/specs/cn/routing.md new file mode 100644 index 0000000..d58ee5f --- /dev/null +++ b/specs/cn/routing.md @@ -0,0 +1,388 @@ +# 路由 + +Next2D Framework 可以作为单页应用程序通过 URL 控制场景。路由在 `routing.json` 中配置。 + +## 基本配置 + +路由的顶级属性可以使用字母数字字符和斜杠。斜杠用作以驼峰命名法访问 View 类的键。 + +```json +{ + "top": { + "requests": [] + }, + "home": { + "requests": [] + }, + "quest/list": { + "requests": [] + } +} +``` + +在上面的示例中: +- `top` → `TopView` 类 +- `home` → `HomeView` 类 +- `quest/list` → `QuestListView` 类 + +## 路由定义 + +### 基本路由 + +```json +{ + "top": { + "requests": [] + } +} +``` + +访问:`https://example.com/` 或 `https://example.com/top` + +### 二级属性 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `private` | boolean | false | 控制直接 URL 访问。如果为 true,URL 访问将加载 TopView | +| `requests` | array | null | 在 View 绑定之前发送请求 | + +### 私有路由 + +要限制直接 URL 访问: + +```json +{ + "quest/detail": { + "private": true, + "requests": [] + } +} +``` + +当 `private: true` 时,直接 URL 访问会重定向到 `TopView`。只能通过 `app.gotoView()` 访问。 + +## requests 配置 + +可以在 View 绑定之前获取数据。检索的数据可通过 `app.getResponse()` 获取。 + +### requests 数组设置 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `type` | string | content | 固定值:`json`、`content`、`custom` | +| `path` | string | empty | 请求目标路径 | +| `name` | string | empty | 在 `response` 中设置的键名 | +| `cache` | boolean | false | 是否缓存数据 | +| `callback` | string \| array | null | 请求完成后的回调类 | +| `class` | string | empty | 执行请求的类(仅 custom 类型) | +| `access` | string | public | 函数访问修饰符(`public` 或 `static`) | +| `method` | string | empty | 要执行的函数名(仅 custom 类型) | + +### 类型变体 + +#### json + +获取外部 JSON 数据: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + } + ] + } +} +``` + +#### content + +获取 Animation Tool JSON: + +```json +{ + "top": { + "requests": [ + { + "type": "content", + "path": "{{content.endPoint}}top.json", + "name": "TopContent" + } + ] + } +} +``` + +#### custom + +使用自定义类执行请求: + +```json +{ + "user/profile": { + "requests": [ + { + "type": "custom", + "class": "repository.UserRepository", + "access": "static", + "method": "getProfile", + "name": "UserProfile" + } + ] + } +} +``` + +### 变量展开 + +用 `{{***}}` 包围以从 `config.json` 获取变量: + +```json +{ + "path": "{{api.endPoint}}path/to/api" +} +``` + +### 使用缓存 + +设置 `cache: true` 会缓存数据。缓存的数据在画面转换中持久存在。 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/master.json", + "name": "MasterData", + "cache": true + } + ] + } +} +``` + +获取缓存的数据: + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +### 回调 + +请求完成后执行回调: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData", + "callback": "callback.HomeDataCallback" + } + ] + } +} +``` + +回调类: + +```typescript +export class HomeDataCallback +{ + constructor(data: any) + { + // 传递检索到的数据 + } + + execute(): void + { + // 回调处理 + } +} +``` + +## 画面转换 + +### app.gotoView() + +使用 `app.gotoView()` 进行画面转换: + +```typescript +import { app } from "@next2d/framework"; + +// 基本转换 +await app.gotoView("home"); + +// 按路径转换 +await app.gotoView("quest/list"); + +// 带查询参数 +await app.gotoView("quest/detail?id=123"); +``` + +### UseCase 中的画面转换 + +建议在 UseCase 中处理画面转换: + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +在 ViewModel 中使用: + +```typescript +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +## 获取响应数据 + +`requests` 的数据可以通过 `app.getResponse()` 获取: + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("TopText")) { + const topText = response.get("TopText") as { word: string }; + this.text = topText.word; + } +} +``` + +**注意:** `response` 数据在画面转换时会重置。对于需要跨画面持久存在的数据,请使用 `cache: true`。 + +## SPA 模式 + +在 `config.json` 的 `all.spa` 中配置: + +```json +{ + "all": { + "spa": true + } +} +``` + +- `true`:通过 URL 控制场景(使用 History API) +- `false`:禁用基于 URL 的场景控制 + +## 默认首页 + +在 `config.json` 中配置: + +```json +{ + "all": { + "defaultTop": "top" + } +} +``` + +如果未设置,将启动 `TopView` 类。 + +## 自动生成 View/ViewModel + +从 `routing.json` 设置自动生成: + +```bash +npm run generate +``` + +此命令解析 `routing.json` 中的顶级属性并生成相应的 View 和 ViewModel 类。 + +## 配置示例 + +### 完整 routing.json 示例 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + }, + { + "type": "content", + "path": "{{content.endPoint}}home.json", + "name": "HomeContent", + "cache": true + } + ] + }, + "quest/list": { + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getList", + "name": "QuestList" + } + ] + }, + "quest/detail": { + "private": true, + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getDetail", + "name": "QuestDetail" + } + ] + } +} +``` + +## 相关 + +- [View/ViewModel](/cn/reference/framework/view) +- [配置](/cn/reference/framework/config) diff --git a/specs/cn/view.md b/specs/cn/view.md new file mode 100644 index 0000000..94d8c34 --- /dev/null +++ b/specs/cn/view.md @@ -0,0 +1,459 @@ +# View 和 ViewModel + +Next2D Framework 采用 MVVM(Model-View-ViewModel)模式。基本风格是每个画面创建一组 View 和 ViewModel。 + +## 架构 + +```mermaid +graph TB + subgraph ViewLayer["视图层"] + ViewRole["处理画面结构和显示"] + ViewRule["没有业务逻辑"] + end + + subgraph ViewModelLayer["ViewModel 层"] + VMRole1["View 和 Model 之间的桥梁"] + VMRole2["持有 UseCase"] + VMRole3["事件处理"] + end + + subgraph ModelLayer["Model 层"] + ModelRole1["业务逻辑(UseCase)"] + ModelRole2["数据访问(Repository)"] + end + + ViewLayer <-->|双向| ViewModelLayer + ViewModelLayer <--> ModelLayer +``` + +## 目录结构 + +``` +src/ +└── view/ + ├── top/ + │ ├── TopView.ts + │ └── TopViewModel.ts + └── home/ + ├── HomeView.ts + └── HomeViewModel.ts +``` + +## View + +View 是附加到主上下文的容器。View 只处理显示结构,将业务逻辑委托给 ViewModel。 + +### View 职责 + +- **画面结构定义** - UI 组件放置和坐标设置 +- **事件侦听器注册** - 与 ViewModel 方法的连接 +- **生命周期管理** - `initialize`、`onEnter`、`onExit` + +### 基本结构 + +```typescript +import type { TopViewModel } from "./TopViewModel"; +import { View } from "@next2d/framework"; +import { TopPage } from "@/ui/component/page/top/TopPage"; + +export class TopView extends View +{ + private readonly _topPage: TopPage; + + constructor(vm: TopViewModel) + { + super(vm); + this._topPage = new TopPage(); + this.addChild(this._topPage); + } + + async initialize(): Promise + { + this._topPage.initialize(this.vm); + } + + async onEnter(): Promise + { + await this._topPage.onEnter(); + } + + async onExit(): Promise + { + return void 0; + } +} +``` + +### 生命周期 + +```mermaid +sequenceDiagram + participant Framework as Framework + participant VM as ViewModel + participant View as View + participant UI as UI 组件 + + Note over Framework,UI: 画面转换开始 + + Framework->>VM: new ViewModel() + Framework->>VM: initialize() + Note over VM: ViewModel 首先初始化 + + Framework->>View: new View(vm) + Framework->>View: initialize() + View->>UI: 创建组件 + View->>VM: 注册事件侦听器 + + Framework->>View: onEnter() + View->>UI: 开始动画 + + Note over Framework,UI: 用户与画面交互 + + Framework->>View: onExit() + View->>UI: 清理 +``` + +#### initialize() - 初始化 + +**何时调用:** +- View 实例创建后立即 +- 画面转换期间只调用一次 +- 在 ViewModel 的 `initialize()` **之后**执行 + +**主要用途:** +- 创建和排列 UI 组件 +- 注册事件侦听器 +- 添加子元素(`addChild`) + +```typescript +async initialize(): Promise +{ + const { HomeBtnMolecule } = await import("@/ui/component/molecule/HomeBtnMolecule"); + const { PointerEvent } = next2d.events; + + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; + + // 将事件委托给 ViewModel + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); +} +``` + +#### onEnter() - 画面显示时 + +**何时调用:** +- `initialize()` 完成后 +- 画面显示前 + +**主要用途:** +- 开始入场动画 +- 启动计时器和间隔 +- 设置焦点 + +```typescript +async onEnter(): Promise +{ + const topBtn = this.getChildByName("topBtn") as TopBtnMolecule; + topBtn.playEntrance(() => { + console.log("动画完成"); + }); +} +``` + +#### onExit() - 画面隐藏时 + +**何时调用:** +- 转换到另一个画面前 +- View 销毁前 + +**主要用途:** +- 停止动画 +- 清除计时器和间隔 +- 释放资源 + +```typescript +async onExit(): Promise +{ + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } +} +``` + +## ViewModel + +ViewModel 充当 View 和 Model 之间的桥梁。它持有 UseCase 并处理来自 View 的事件以执行业务逻辑。 + +### ViewModel 职责 + +- **事件处理** - 从 View 接收事件 +- **UseCase 执行** - 调用业务逻辑 +- **依赖管理** - 持有 UseCase 实例 +- **状态管理** - 管理画面特定状态 + +### 基本结构 + +```typescript +import { ViewModel, app } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; + +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async initialize(): Promise + { + // 从 routing.json 的 requests 接收数据 + const response = app.getResponse(); + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; + } + + getTopText(): string + { + return this.topText; + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +### ViewModel 初始化时机 + +**重要:ViewModel 的 `initialize()` 在 View 的 `initialize()` 之前调用。** + +``` +1. ViewModel 实例创建 + ↓ +2. ViewModel.initialize() ← ViewModel 先 + ↓ +3. View 实例创建(ViewModel 注入) + ↓ +4. View.initialize() + ↓ +5. View.onEnter() +``` + +这确保了 View 初始化时 ViewModel 数据已准备就绪。 + +```typescript +// HomeViewModel.ts +export class HomeViewModel extends ViewModel +{ + private homeText: string = ""; + + async initialize(): Promise + { + // 在 ViewModel 的 initialize 中获取数据 + const data = await HomeTextRepository.get(); + this.homeText = data.word; + } + + getHomeText(): string + { + return this.homeText; + } +} + +// HomeView.ts +export class HomeView extends View +{ + constructor(private readonly vm: HomeViewModel) + { + super(); + } + + async initialize(): Promise + { + // 此时,vm.initialize() 已经完成 + const text = this.vm.getHomeText(); + + // 使用获取的数据构建 UI + const textField = new TextAtom(text); + this.addChild(textField); + } +} +``` + +## 画面转换 + +使用 `app.gotoView()` 进行画面转换。 + +```typescript +import { app } from "@next2d/framework"; + +// 导航到指定的 View +await app.gotoView("home"); + +// 带参数导航 +await app.gotoView("user/detail?id=123"); +``` + +### UseCase 中的画面转换 + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +## 获取响应数据 + +`routing.json` 中 `requests` 的数据可以通过 `app.getResponse()` 获取。 + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("UserData")) { + const userData = response.get("UserData"); + this.userName = userData.name; + } +} +``` + +## 获取缓存数据 + +`cache: true` 的数据可以通过 `app.getCache()` 获取。 + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +## 设计原则 + +### 1. 关注点分离 + +```typescript +// 好:View 只处理显示,ViewModel 处理逻辑 +class HomeView extends View +{ + async initialize(): Promise + { + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick); + } +} + +class HomeViewModel extends ViewModel +{ + onClick(event: PointerEvent): void + { + this.someUseCase.execute(); + } +} +``` + +### 2. 依赖倒置 + +ViewModel 依赖接口,而不是具体类。 + +```typescript +// 好:依赖接口 +homeContentPointerDownEvent(event: PointerEvent): void +{ + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); +} +``` + +### 3. 始终将事件委托给 ViewModel + +永远不要在 View 内部完全处理事件;始终委托给 ViewModel。 + +## View/ViewModel 模板 + +### View + +```typescript +import type { YourViewModel } from "./YourViewModel"; +import { View } from "@next2d/framework"; + +export class YourView extends View +{ + constructor(vm: YourViewModel) + { + super(vm); + } + + async initialize(): Promise + { + // 创建和排列 UI 组件 + } + + async onEnter(): Promise + { + // 画面显示时 + } + + async onExit(): Promise + { + // 画面隐藏时 + } +} +``` + +### ViewModel + +```typescript +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/your/usecase/YourUseCase"; + +export class YourViewModel extends ViewModel +{ + private readonly yourUseCase: YourUseCase; + + constructor() + { + super(); + this.yourUseCase = new YourUseCase(); + } + + async initialize(): Promise + { + return void 0; + } + + yourEventHandler(event: Event): void + { + this.yourUseCase.execute(); + } +} +``` + +## 相关 + +- [路由](/cn/reference/framework/routing) +- [配置](/cn/reference/framework/config) diff --git a/specs/en/animation-tool.md b/specs/en/animation-tool.md new file mode 100644 index 0000000..41111f1 --- /dev/null +++ b/specs/en/animation-tool.md @@ -0,0 +1,140 @@ +# AnimationTool Integration + +Next2D Framework seamlessly integrates with assets created in AnimationTool. + +## Overview + +AnimationTool is a tool for creating animations and UI components for Next2D Player. The output JSON files can be loaded in the framework and used as MovieClips. + +## Directory Structure + +``` +src/ +├── ui/ +│ ├── content/ # Animation Tool generated content +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── component/ # Atomic Design components +│ │ ├── atom/ # Smallest unit components +│ │ │ ├── ButtonAtom.ts +│ │ │ └── TextAtom.ts +│ │ ├── molecule/ # Combined Atom components +│ │ │ ├── TopBtnMolecule.ts +│ │ │ └── HomeBtnMolecule.ts +│ │ ├── organism/ # Multiple Molecule combinations +│ │ ├── template/ # Page templates +│ │ └── page/ # Page components +│ │ ├── top/ +│ │ │ └── TopPage.ts +│ │ └── home/ +│ │ └── HomePage.ts +│ │ +│ └── animation/ # Code animation definitions +│ └── top/ +│ └── TopBtnShowAnimation.ts +│ +└── file/ # Animation Tool output files + └── sample.n2d +``` + +## MovieClipContent + +A class that wraps content created with Animation Tool. + +### Basic Structure + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @see file/sample.n2d + */ +export class TopContent extends MovieClipContent +{ + /** + * Returns the symbol name set in Animation Tool + */ + get namespace(): string + { + return "TopContent"; + } +} +``` + +### Role of namespace + +The `namespace` property should match the symbol name created in Animation Tool. This name is used to generate the corresponding MovieClip from the loaded JSON data. + +## Loading Content + +### Configuration in routing.json + +Animation Tool JSON files are loaded via `requests` in `routing.json`. + +```json +{ + "@sample": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}content/sample.json", + "name": "MainContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { + "type": "cluster", + "path": "@sample" + } + ] + } +} +``` + +#### Request Configuration + +| Property | Type | Description | +|----------|------|-------------| +| `type` | string | Specify `"content"` | +| `path` | string | Path to the JSON file | +| `name` | string | Key name registered in response | +| `cache` | boolean | Whether to cache | + +#### Cluster Feature + +Keys starting with `@` are defined as clusters and can be shared across multiple routes. Reference them with `type: "cluster"`. + +```json +{ + "@common": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}common.json", + "name": "CommonContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + }, + "home": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + } +} +``` + +## Related + +- [View/ViewModel](/en/reference/framework/view) +- [Routing](/en/reference/framework/routing) +- [Configuration](/en/reference/framework/config) diff --git a/specs/en/config.md b/specs/en/config.md new file mode 100644 index 0000000..e9202e5 --- /dev/null +++ b/specs/en/config.md @@ -0,0 +1,331 @@ +# Configuration Files + +Next2D Framework configuration is managed with three JSON files. + +## File Structure + +``` +src/config/ +├── stage.json # Display area settings +├── config.json # Environment settings +└── routing.json # Routing settings +``` + +## stage.json + +JSON file for setting the display area (Stage). + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "transparent" + } +} +``` + +### Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `width` | number | 240 | Display area width | +| `height` | number | 240 | Display area height | +| `fps` | number | 60 | Drawings per second (1-60) | +| `options` | object | null | Option settings | + +### options Settings + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `fullScreen` | boolean | false | Draw on entire screen beyond Stage width/height | +| `tagId` | string | null | When specified, drawing occurs within the element with that ID | +| `bgColor` | string | "transparent" | Background color in hexadecimal. Default is transparent | + +## config.json + +File for managing environment-specific settings. Divided into `local`, `dev`, `stg`, `prd`, and `all`, where any environment name except `all` is arbitrary. + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +### all Settings + +`all` is a common variable exported in any environment. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `spa` | boolean | true | Control scenes via URL as Single Page Application | +| `defaultTop` | string | "top" | View for page top. TopView class launches if not set | +| `loading.callback` | string | Loading | Loading screen class name. Calls start and end functions | +| `gotoView.callback` | string \| array | ["callback.Background"] | Callback class after gotoView completion | + +### platform Settings + +The value specified with `--platform` at build time is set. + +Supported values: `macos`, `windows`, `linux`, `ios`, `android`, `web` + +```typescript +import { config } from "@/config/Config"; + +if (config.platform === "ios") { + // iOS-specific processing +} +``` + +## routing.json + +Routing configuration file. See [Routing](/en/reference/framework/routing) for details. + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [] + } +} +``` + +## Getting Configuration Values + +Use the `config` object to get configuration values in code. + +### Config.ts Example + +```typescript +import stageJson from "./stage.json"; +import configJson from "./config.json"; + +interface IStageConfig { + width: number; + height: number; + fps: number; + options: { + fullScreen: boolean; + tagId: string | null; + bgColor: string; + }; +} + +interface IConfig { + stage: IStageConfig; + api: { + endPoint: string; + }; + content: { + endPoint: string; + }; + spa: boolean; + defaultTop: string; + platform: string; +} + +export const config: IConfig = { + stage: stageJson, + ...configJson +}; +``` + +### Usage Example + +```typescript +import { config } from "@/config/Config"; + +// Stage settings +const stageWidth = config.stage.width; +const stageHeight = config.stage.height; + +// API settings +const apiEndPoint = config.api.endPoint; + +// SPA setting +const isSpa = config.spa; +``` + +## Loading Screen + +The `start` and `end` functions of the class set in `loading.callback` are called. + +```typescript +export class Loading +{ + private shape: Shape; + + constructor() + { + this.shape = new Shape(); + // Initialize loading display + } + + start(): void + { + // Processing when loading starts + stage.addChild(this.shape); + } + + end(): void + { + // Processing when loading ends + this.shape.remove(); + } +} +``` + +## gotoView Callback + +The `execute` function of classes set in `gotoView.callback` is called. Multiple classes can be set as an array and executed sequentially with async/await. + +```typescript +import { app } from "@next2d/framework"; +import { Shape, stage } from "@next2d/display"; + +export class Background +{ + public readonly shape: Shape; + + constructor() + { + this.shape = new Shape(); + } + + execute(): void + { + const context = app.getContext(); + const view = context.view; + if (!view) return; + + // Place background at the back + view.addChildAt(this.shape, 0); + } +} +``` + +## Build Commands + +Build with environment specification: + +```bash +# Local environment +npm run build -- --env=local + +# Development environment +npm run build -- --env=dev + +# Production environment +npm run build -- --env=prd +``` + +Specify platform: + +```bash +npm run build -- --platform=web +npm run build -- --platform=ios +npm run build -- --platform=android +``` + +## Configuration Examples + +### Complete Configuration File Examples + +#### stage.json + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "#1461A0" + } +} +``` + +#### config.json + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/mock/content/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + }, + "content": { + "endPoint": "https://dev-cdn.example.com/content/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + }, + "content": { + "endPoint": "https://cdn.example.com/content/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +## Related + +- [Routing](/en/reference/framework/routing) +- [View/ViewModel](/en/reference/framework/view) diff --git a/specs/en/index.md b/specs/en/index.md new file mode 100644 index 0000000..ea52560 --- /dev/null +++ b/specs/en/index.md @@ -0,0 +1,345 @@ +# Next2D Framework + +Next2D Framework is an MVVM framework for building applications with Next2D Player. It provides routing for single-page applications (SPA), View/ViewModel management, and configuration management. + +## Key Features + +- **MVVM Pattern**: Separation of concerns with Model-View-ViewModel +- **Clean Architecture**: Dependency inversion and loosely coupled design +- **Single Page Application**: URL-based scene management +- **Animation Tool Integration**: Seamless integration with Animation Tool assets +- **TypeScript Support**: Type-safe development +- **Atomic Design**: Recommended component design for reusability + +## Architecture Overview + +This project implements a combination of Clean Architecture and MVVM pattern. + +```mermaid +graph TB + subgraph ViewLayer["View Layer"] + View["View"] + ViewModel["ViewModel"] + UI["UI Components"] + end + + subgraph InterfaceLayer["Interface Layer"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IResponse"] + end + + subgraph ApplicationLayer["Application Layer"] + UseCase["UseCase"] + end + + subgraph DomainLayer["Domain Layer"] + DomainLogic["Domain Logic"] + DomainService["Service"] + end + + subgraph InfraLayer["Infrastructure Layer"] + Repository["Repository"] + ExternalAPI["External API"] + end + + ViewLayer -.->|via interface| InterfaceLayer + ViewLayer -.->|calls| ApplicationLayer + ApplicationLayer -.->|via interface| InterfaceLayer + ApplicationLayer -.->|uses| DomainLayer + ApplicationLayer -.->|calls| InfraLayer + InfraLayer -.->|accesses| ExternalAPI +``` + +### Layer Responsibilities + +| Layer | Path | Role | +|-------|------|------| +| **View** | `view/*`, `ui/*` | Handles screen structure and display | +| **ViewModel** | `view/*` | Bridge between View and Model, event handling | +| **Interface** | `interface/*` | Abstraction layer, type definitions | +| **Application** | `model/application/*/usecase/*` | Business logic implementation (UseCase) | +| **Domain** | `model/domain/*` | Core business rules | +| **Infrastructure** | `model/infrastructure/repository/*` | Data access, external API integration | + +### Dependency Direction + +Following Clean Architecture principles, dependencies always point inward (toward the Domain layer). + +- **View Layer**: Uses Application layer through interfaces +- **Application Layer**: Uses Domain and Infrastructure layers through interfaces +- **Domain Layer**: Depends on nothing (pure business logic) +- **Infrastructure Layer**: Implements Domain layer interfaces + +## Directory Structure + +``` +my-app/ +├── src/ +│ ├── config/ # Configuration files +│ │ ├── stage.json # Stage settings +│ │ ├── config.json # Environment settings +│ │ ├── routing.json # Routing settings +│ │ └── Config.ts # Config type definitions and exports +│ │ +│ ├── interface/ # Interface definitions +│ │ ├── IDraggable.ts # Draggable object +│ │ ├── ITextField.ts # Text field +│ │ ├── IHomeTextResponse.ts # API response type +│ │ └── IViewName.ts # View name type definition +│ │ +│ ├── view/ # View & ViewModel +│ │ ├── top/ +│ │ │ ├── TopView.ts # Screen structure definition +│ │ │ └── TopViewModel.ts # Bridge to business logic +│ │ └── home/ +│ │ ├── HomeView.ts +│ │ └── HomeViewModel.ts +│ │ +│ ├── model/ +│ │ ├── application/ # Application layer +│ │ │ ├── top/ +│ │ │ │ └── usecase/ +│ │ │ │ └── NavigateToViewUseCase.ts +│ │ │ └── home/ +│ │ │ └── usecase/ +│ │ │ ├── StartDragUseCase.ts +│ │ │ ├── StopDragUseCase.ts +│ │ │ └── CenterTextFieldUseCase.ts +│ │ │ +│ │ ├── domain/ # Domain layer +│ │ │ └── callback/ +│ │ │ ├── Background.ts +│ │ │ └── Background/ +│ │ │ └── service/ +│ │ │ ├── BackgroundDrawService.ts +│ │ │ └── BackgroundChangeScaleService.ts +│ │ │ +│ │ └── infrastructure/ # Infrastructure layer +│ │ └── repository/ +│ │ └── HomeTextRepository.ts +│ │ +│ ├── ui/ # UI Components +│ │ ├── animation/ # Animation definitions +│ │ │ └── top/ +│ │ │ └── TopBtnShowAnimation.ts +│ │ │ +│ │ ├── component/ # Atomic Design +│ │ │ ├── atom/ # Smallest unit components +│ │ │ │ ├── ButtonAtom.ts +│ │ │ │ └── TextAtom.ts +│ │ │ ├── molecule/ # Combined Atom components +│ │ │ │ ├── HomeBtnMolecule.ts +│ │ │ │ └── TopBtnMolecule.ts +│ │ │ ├── organism/ # Multiple Molecule combinations +│ │ │ ├── template/ # Page templates +│ │ │ └── page/ # Page components +│ │ │ ├── top/ +│ │ │ │ └── TopPage.ts +│ │ │ └── home/ +│ │ │ └── HomePage.ts +│ │ │ +│ │ └── content/ # Animation Tool generated content +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── assets/ # Static assets +│ │ +│ ├── Packages.ts # Package exports +│ └── index.ts # Entry point +│ +├── file/ # Animation Tool output files +│ └── sample.n2d +│ +├── mock/ # Mock data +│ ├── api/ # API mocks +│ ├── content/ # Content mocks +│ └── img/ # Image mocks +│ +└── package.json +``` + +## Framework Flowchart + +Detailed flow of screen transitions using the gotoView function. + +```mermaid +graph TD + User([User]) -->|Request| GotoView[gotoView Path] + + GotoView --> LoadingCheck{use loading?
Default: true} + + LoadingCheck -->|YES| ScreenOverlay[Screen Overlay] + LoadingCheck -->|NO| RemoveResponse + ScreenOverlay --> LoadingStart[Start Loading] + LoadingStart --> RemoveResponse + + RemoveResponse[Remove Previous Response Data] --> ParseQuery[Parse Query String] + ParseQuery --> UpdateHistory{SPA mode?} + + UpdateHistory -->|YES| PushState[Push History State] + UpdateHistory -->|NO| RequestType + PushState --> RequestType + + RequestType[Request Type] + + RequestType --> JSON[JSON: Get external JSON data] + RequestType --> CONTENT[CONTENT: Get Animation Tool JSON] + RequestType --> CUSTOM[CUSTOM: Request to external API] + + JSON --> CacheCheck{use cache?
Default: false} + CONTENT --> CacheCheck + CUSTOM --> CacheCheck + + CacheCheck -->|YES| CacheData[(Cache)] + CacheCheck -->|NO| GlobalData{{Global Network}} + + CacheData --> Cached{Cached?} + + Cached -->|NO| GlobalData + Cached -->|YES| RegisterResponse + GlobalData --> RegisterResponse + + RegisterResponse[Register Response Data] --> RequestCallback{request callback?} + + RequestCallback -->|YES| ExecRequestCallback[Execute Request Callback] + RequestCallback -->|NO| UnbindView + ExecRequestCallback --> UnbindView + + UnbindView[Previous View: onExit & Unbind] --> BindView[New View/ViewModel: Bind] + BindView --> ViewModelInit[ViewModel: initialize] + + ViewModelInit --> ViewInit[View: initialize] + ViewInit --> AddToStage[Add View to Stage] + AddToStage --> GotoViewCallback{gotoView callback?} + + GotoViewCallback -->|YES| ExecGotoViewCallback[Execute gotoView Callback] + GotoViewCallback -->|NO| LoadingEndCheck + ExecGotoViewCallback --> LoadingEndCheck + + LoadingEndCheck{use loading?
Default: true} + + LoadingEndCheck -->|YES| LoadingEnd[End Loading] + LoadingEndCheck -->|NO| OnEnter + LoadingEnd --> DisposeOverlay[Dispose Screen Overlay] + DisposeOverlay --> OnEnter + + OnEnter[View: onEnter] --> StartDrawing + + StartDrawing[Start Drawing] -->|Response| User + + style User fill:#d5e8d4,stroke:#82b366 + style StartDrawing fill:#dae8fc,stroke:#6c8ebf + style CacheData fill:#fff2cc,stroke:#d6b656 + style GlobalData fill:#f5f5f5,stroke:#666666 +``` + +### Key Flow Steps + +| Step | Description | +|------|-------------| +| **gotoView** | Entry point for screen transitions | +| **Loading** | Loading screen show/hide control | +| **Request Type** | Three types of requests: JSON, CONTENT, CUSTOM | +| **Cache** | Response data cache control | +| **View/ViewModel Bind** | Binding process for new View/ViewModel | +| **onEnter** | Callback after screen display is complete | + +## Key Design Patterns + +### 1. MVVM (Model-View-ViewModel) + +- **View**: Handles screen structure and display. No business logic +- **ViewModel**: Bridge between View and Model. Holds UseCases and processes events +- **Model**: Handles business logic and data access + +### 2. UseCase Pattern + +Create a dedicated UseCase class for each user action: + +```typescript +export class StartDragUseCase +{ + execute(target: IDraggable): void + { + target.startDrag(); + } +} +``` + +### 3. Dependency Inversion + +Depend on interfaces, not concrete classes: + +```typescript +// Good: Depend on interfaces +import type { IDraggable } from "@/interface/IDraggable"; + +function startDrag(target: IDraggable): void +{ + target.startDrag(); +} +``` + +### 4. Repository Pattern + +Abstract data access and implement error handling: + +```typescript +export class HomeTextRepository +{ + static async get(): Promise + { + try { + const response = await fetch(`${config.api.endPoint}api/home.json`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Failed to fetch:", error); + throw error; + } + } +} +``` + +## Quick Start + +### Create Project + +```bash +npx create-next2d-app my-app +cd my-app +npm install +npm start +``` + +### Auto-generate View/ViewModel + +```bash +npm run generate +``` + +This command parses top properties in `routing.json` and generates corresponding View and ViewModel classes. + +## Best Practices + +1. **Interface First**: Always depend on interfaces, not concrete types +2. **Single Responsibility Principle**: Each class has only one responsibility +3. **Dependency Injection**: Inject dependencies via constructor +4. **Error Handling**: Handle errors appropriately in Repository layer +5. **Type Safety**: Avoid `any` type, use explicit type definitions + +## Related Documentation + +### Basics +- [View/ViewModel](/en/reference/framework/view) - Screen display and data binding +- [Routing](/en/reference/framework/routing) - URL-based screen transitions +- [Configuration](/en/reference/framework/config) - Environment and stage settings +- [Animation Tool Integration](/en/reference/framework/animation-tool) - Using Animation Tool assets + +### Next2D Player Integration +- [Next2D Player](/en/reference/player) - Rendering engine +- [MovieClip](/en/reference/player/movie-clip) - Timeline animation +- [Event System](/en/reference/player/events) - User interaction diff --git a/specs/en/routing.md b/specs/en/routing.md new file mode 100644 index 0000000..6673b8a --- /dev/null +++ b/specs/en/routing.md @@ -0,0 +1,388 @@ +# Routing + +Next2D Framework can control scenes via URL as a Single Page Application. Routing is configured in `routing.json`. + +## Basic Configuration + +The top properties for routing can use alphanumeric characters and slashes. The slash is used as a key to access View classes in CamelCase. + +```json +{ + "top": { + "requests": [] + }, + "home": { + "requests": [] + }, + "quest/list": { + "requests": [] + } +} +``` + +In the above example: +- `top` → `TopView` class +- `home` → `HomeView` class +- `quest/list` → `QuestListView` class + +## Route Definition + +### Basic Route + +```json +{ + "top": { + "requests": [] + } +} +``` + +Access: `https://example.com/` or `https://example.com/top` + +### Second Level Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `private` | boolean | false | Controls direct URL access. If true, URL access loads TopView | +| `requests` | array | null | Send requests before View is bound | + +### Private Routes + +To restrict direct URL access: + +```json +{ + "quest/detail": { + "private": true, + "requests": [] + } +} +``` + +When `private: true`, direct URL access redirects to `TopView`. Only accessible via `app.gotoView()`. + +## requests Configuration + +Data can be fetched before View is bound. Retrieved data is available via `app.getResponse()`. + +### requests Array Settings + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `type` | string | content | Fixed values: `json`, `content`, `custom` | +| `path` | string | empty | Request destination path | +| `name` | string | empty | Key name to set in `response` | +| `cache` | boolean | false | Whether to cache data | +| `callback` | string \| array | null | Callback class after request completion | +| `class` | string | empty | Class to execute request (custom type only) | +| `access` | string | public | Function access modifier (`public` or `static`) | +| `method` | string | empty | Function name to execute (custom type only) | + +### Type Variants + +#### json + +Get external JSON data: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + } + ] + } +} +``` + +#### content + +Get Animation Tool JSON: + +```json +{ + "top": { + "requests": [ + { + "type": "content", + "path": "{{content.endPoint}}top.json", + "name": "TopContent" + } + ] + } +} +``` + +#### custom + +Execute request with custom class: + +```json +{ + "user/profile": { + "requests": [ + { + "type": "custom", + "class": "repository.UserRepository", + "access": "static", + "method": "getProfile", + "name": "UserProfile" + } + ] + } +} +``` + +### Variable Expansion + +Enclose with `{{***}}` to get variables from `config.json`: + +```json +{ + "path": "{{api.endPoint}}path/to/api" +} +``` + +### Using Cache + +Setting `cache: true` caches the data. Cached data persists through screen transitions. + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/master.json", + "name": "MasterData", + "cache": true + } + ] + } +} +``` + +Getting cached data: + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +### Callbacks + +Execute callback after request completion: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData", + "callback": "callback.HomeDataCallback" + } + ] + } +} +``` + +Callback class: + +```typescript +export class HomeDataCallback +{ + constructor(data: any) + { + // Retrieved data is passed + } + + execute(): void + { + // Callback processing + } +} +``` + +## Screen Transition + +### app.gotoView() + +Use `app.gotoView()` for screen transitions: + +```typescript +import { app } from "@next2d/framework"; + +// Basic transition +await app.gotoView("home"); + +// Transition by path +await app.gotoView("quest/list"); + +// With query parameters +await app.gotoView("quest/detail?id=123"); +``` + +### Screen Transition in UseCase + +Recommended to handle screen transitions in UseCase: + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +Usage in ViewModel: + +```typescript +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +## Getting Response Data + +Data from `requests` can be retrieved with `app.getResponse()`: + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("TopText")) { + const topText = response.get("TopText") as { word: string }; + this.text = topText.word; + } +} +``` + +**Note:** `response` data is reset on screen transition. Use `cache: true` for data that should persist across screens. + +## SPA Mode + +Configure in `config.json`'s `all.spa`: + +```json +{ + "all": { + "spa": true + } +} +``` + +- `true`: Control scenes via URL (uses History API) +- `false`: Disable URL-based scene control + +## Default Top Page + +Configure in `config.json`: + +```json +{ + "all": { + "defaultTop": "top" + } +} +``` + +If not set, `TopView` class is launched. + +## Auto-generating View/ViewModel + +Auto-generate from `routing.json` settings: + +```bash +npm run generate +``` + +This command parses top properties in `routing.json` and generates corresponding View and ViewModel classes. + +## Configuration Example + +### Complete routing.json Example + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + }, + { + "type": "content", + "path": "{{content.endPoint}}home.json", + "name": "HomeContent", + "cache": true + } + ] + }, + "quest/list": { + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getList", + "name": "QuestList" + } + ] + }, + "quest/detail": { + "private": true, + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getDetail", + "name": "QuestDetail" + } + ] + } +} +``` + +## Related + +- [View/ViewModel](/en/reference/framework/view) +- [Configuration](/en/reference/framework/config) diff --git a/specs/en/view.md b/specs/en/view.md new file mode 100644 index 0000000..5770028 --- /dev/null +++ b/specs/en/view.md @@ -0,0 +1,459 @@ +# View and ViewModel + +Next2D Framework adopts the MVVM (Model-View-ViewModel) pattern. The basic style is to create one set of View and ViewModel per screen. + +## Architecture + +```mermaid +graph TB + subgraph ViewLayer["View Layer"] + ViewRole["Handles screen structure and display"] + ViewRule["No business logic"] + end + + subgraph ViewModelLayer["ViewModel Layer"] + VMRole1["Bridge between View and Model"] + VMRole2["Holds UseCases"] + VMRole3["Event handling"] + end + + subgraph ModelLayer["Model Layer"] + ModelRole1["Business logic (UseCase)"] + ModelRole2["Data access (Repository)"] + end + + ViewLayer <-->|Bidirectional| ViewModelLayer + ViewModelLayer <--> ModelLayer +``` + +## Directory Structure + +``` +src/ +└── view/ + ├── top/ + │ ├── TopView.ts + │ └── TopViewModel.ts + └── home/ + ├── HomeView.ts + └── HomeViewModel.ts +``` + +## View + +View is a container attached to the main context. View handles only the display structure and delegates business logic to the ViewModel. + +### View Responsibilities + +- **Screen structure definition** - UI component placement and coordinate settings +- **Event listener registration** - Connection with ViewModel methods +- **Lifecycle management** - `initialize`, `onEnter`, `onExit` + +### Basic Structure + +```typescript +import type { TopViewModel } from "./TopViewModel"; +import { View } from "@next2d/framework"; +import { TopPage } from "@/ui/component/page/top/TopPage"; + +export class TopView extends View +{ + private readonly _topPage: TopPage; + + constructor(vm: TopViewModel) + { + super(vm); + this._topPage = new TopPage(); + this.addChild(this._topPage); + } + + async initialize(): Promise + { + this._topPage.initialize(this.vm); + } + + async onEnter(): Promise + { + await this._topPage.onEnter(); + } + + async onExit(): Promise + { + return void 0; + } +} +``` + +### Lifecycle + +```mermaid +sequenceDiagram + participant Framework as Framework + participant VM as ViewModel + participant View as View + participant UI as UI Components + + Note over Framework,UI: Screen transition starts + + Framework->>VM: new ViewModel() + Framework->>VM: initialize() + Note over VM: ViewModel initializes first + + Framework->>View: new View(vm) + Framework->>View: initialize() + View->>UI: Create components + View->>VM: Register event listeners + + Framework->>View: onEnter() + View->>UI: Start animations + + Note over Framework,UI: User interacts with screen + + Framework->>View: onExit() + View->>UI: Clean up +``` + +#### initialize() - Initialization + +**When Called:** +- Immediately after View instance is created +- Called only once during screen transition +- Executed **after** ViewModel's `initialize()` + +**Primary Usage:** +- Create and arrange UI components +- Register event listeners +- Add child elements (`addChild`) + +```typescript +async initialize(): Promise +{ + const { HomeBtnMolecule } = await import("@/ui/component/molecule/HomeBtnMolecule"); + const { PointerEvent } = next2d.events; + + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; + + // Delegate events to ViewModel + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); +} +``` + +#### onEnter() - On Screen Shown + +**When Called:** +- After `initialize()` completes +- Just before the screen is displayed + +**Primary Usage:** +- Start entrance animations +- Start timers and intervals +- Set focus + +```typescript +async onEnter(): Promise +{ + const topBtn = this.getChildByName("topBtn") as TopBtnMolecule; + topBtn.playEntrance(() => { + console.log("Animation completed"); + }); +} +``` + +#### onExit() - On Screen Hidden + +**When Called:** +- Just before transitioning to another screen +- Before View is destroyed + +**Primary Usage:** +- Stop animations +- Clear timers and intervals +- Release resources + +```typescript +async onExit(): Promise +{ + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } +} +``` + +## ViewModel + +ViewModel acts as a bridge between View and Model. It holds UseCases and processes events from View to execute business logic. + +### ViewModel Responsibilities + +- **Event processing** - Receive events from View +- **UseCase execution** - Call business logic +- **Dependency management** - Hold UseCase instances +- **State management** - Manage screen-specific state + +### Basic Structure + +```typescript +import { ViewModel, app } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; + +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async initialize(): Promise + { + // Receive data from routing.json requests + const response = app.getResponse(); + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; + } + + getTopText(): string + { + return this.topText; + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +### ViewModel Initialization Timing + +**Important: ViewModel's `initialize()` is called before View's `initialize()`.** + +``` +1. ViewModel instance created + ↓ +2. ViewModel.initialize() ← ViewModel first + ↓ +3. View instance created (ViewModel injected) + ↓ +4. View.initialize() + ↓ +5. View.onEnter() +``` + +This ensures ViewModel data is ready when View initializes. + +```typescript +// HomeViewModel.ts +export class HomeViewModel extends ViewModel +{ + private homeText: string = ""; + + async initialize(): Promise + { + // Fetch data in ViewModel's initialize + const data = await HomeTextRepository.get(); + this.homeText = data.word; + } + + getHomeText(): string + { + return this.homeText; + } +} + +// HomeView.ts +export class HomeView extends View +{ + constructor(private readonly vm: HomeViewModel) + { + super(); + } + + async initialize(): Promise + { + // At this point, vm.initialize() is already complete + const text = this.vm.getHomeText(); + + // Build UI using fetched data + const textField = new TextAtom(text); + this.addChild(textField); + } +} +``` + +## Screen Transition + +Use `app.gotoView()` for screen transitions. + +```typescript +import { app } from "@next2d/framework"; + +// Navigate to specified View +await app.gotoView("home"); + +// Navigate with parameters +await app.gotoView("user/detail?id=123"); +``` + +### Screen Transition in UseCase + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +## Getting Response Data + +Data from `requests` in `routing.json` can be retrieved with `app.getResponse()`. + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("UserData")) { + const userData = response.get("UserData"); + this.userName = userData.name; + } +} +``` + +## Getting Cache Data + +Data with `cache: true` can be retrieved with `app.getCache()`. + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +## Design Principles + +### 1. Separation of Concerns + +```typescript +// Good: View handles display only, ViewModel handles logic +class HomeView extends View +{ + async initialize(): Promise + { + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick); + } +} + +class HomeViewModel extends ViewModel +{ + onClick(event: PointerEvent): void + { + this.someUseCase.execute(); + } +} +``` + +### 2. Dependency Inversion + +ViewModel depends on interfaces, not concrete classes. + +```typescript +// Good: Depend on interfaces +homeContentPointerDownEvent(event: PointerEvent): void +{ + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); +} +``` + +### 3. Always Delegate Events to ViewModel + +Never handle events entirely within View; always delegate to ViewModel. + +## View/ViewModel Templates + +### View + +```typescript +import type { YourViewModel } from "./YourViewModel"; +import { View } from "@next2d/framework"; + +export class YourView extends View +{ + constructor(vm: YourViewModel) + { + super(vm); + } + + async initialize(): Promise + { + // Create and arrange UI components + } + + async onEnter(): Promise + { + // On screen shown + } + + async onExit(): Promise + { + // On screen hidden + } +} +``` + +### ViewModel + +```typescript +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/your/usecase/YourUseCase"; + +export class YourViewModel extends ViewModel +{ + private readonly yourUseCase: YourUseCase; + + constructor() + { + super(); + this.yourUseCase = new YourUseCase(); + } + + async initialize(): Promise + { + return void 0; + } + + yourEventHandler(event: Event): void + { + this.yourUseCase.execute(); + } +} +``` + +## Related + +- [Routing](/en/reference/framework/routing) +- [Configuration](/en/reference/framework/config) diff --git a/specs/ja/animation-tool.md b/specs/ja/animation-tool.md new file mode 100644 index 0000000..db4b1a6 --- /dev/null +++ b/specs/ja/animation-tool.md @@ -0,0 +1,140 @@ +# AnimationTool連携 + +Next2D FrameworkはAnimationToolで作成したアセットとシームレスに連携できます。 + +## 概要 + +AnimationToolは、Next2D Player用のアニメーションやUIコンポーネントを作成するためのツールです。出力されたJSONファイルをフレームワークで読み込み、MovieClipとして利用できます。 + +## ディレクトリ構成 + +``` +src/ +├── ui/ +│ ├── content/ # Animation Tool生成コンテンツ +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── component/ # Atomic Designコンポーネント +│ │ ├── atom/ # 最小単位のコンポーネント +│ │ │ ├── ButtonAtom.ts +│ │ │ └── TextAtom.ts +│ │ ├── molecule/ # Atomを組み合わせたコンポーネント +│ │ │ ├── TopBtnMolecule.ts +│ │ │ └── HomeBtnMolecule.ts +│ │ ├── organism/ # 複数Moleculeの組み合わせ +│ │ ├── template/ # ページテンプレート +│ │ └── page/ # ページコンポーネント +│ │ ├── top/ +│ │ │ └── TopPage.ts +│ │ └── home/ +│ │ └── HomePage.ts +│ │ +│ └── animation/ # コードアニメーション定義 +│ └── top/ +│ └── TopBtnShowAnimation.ts +│ +└── file/ # Animation Tool出力ファイル + └── sample.n2d +``` + +## MovieClipContent + +Animation Toolで作成したコンテンツをラップするクラスです。 + +### 基本構造 + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @see file/sample.n2d + */ +export class TopContent extends MovieClipContent +{ + /** + * Animation Tool上で設定したシンボル名を返す + */ + get namespace(): string + { + return "TopContent"; + } +} +``` + +### namespaceの役割 + +`namespace`プロパティは、Animation Toolで作成したシンボルの名前と一致させます。この名前を使って、読み込まれたJSONデータから対応するMovieClipが生成されます。 + +## コンテンツの読み込み + +### routing.jsonでの設定 + +Animation ToolのJSONファイルは`routing.json`の`requests`で読み込みます。 + +```json +{ + "@sample": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}content/sample.json", + "name": "MainContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { + "type": "cluster", + "path": "@sample" + } + ] + } +} +``` + +#### request設定 + +| プロパティ | 型 | 説明 | +|-----------|------|------| +| `type` | string | `"content"` を指定 | +| `path` | string | JSONファイルへのパス | +| `name` | string | レスポンスに登録されるキー名 | +| `cache` | boolean | キャッシュするかどうか | + +#### cluster機能 + +`@`で始まるキーはクラスターとして定義され、複数のルートで共有できます。`type: "cluster"`で参照します。 + +```json +{ + "@common": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}common.json", + "name": "CommonContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + }, + "home": { + "requests": [ + { "type": "cluster", "path": "@common" } + ] + } +} +``` + +## 関連項目 + +- [View/ViewModel](/ja/reference/framework/view) +- [ルーティング](/ja/reference/framework/routing) +- [設定ファイル](/ja/reference/framework/config) diff --git a/specs/ja/config.md b/specs/ja/config.md new file mode 100644 index 0000000..9d615c5 --- /dev/null +++ b/specs/ja/config.md @@ -0,0 +1,331 @@ +# 設定ファイル + +Next2D Frameworkの設定は3つのJSONファイルで管理します。 + +## ファイル構成 + +``` +src/config/ +├── stage.json # 表示領域の設定 +├── config.json # 環境設定 +└── routing.json # ルーティング設定 +``` + +## stage.json + +表示領域(Stage)の設定を行うJSONファイルです。 + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "transparent" + } +} +``` + +### プロパティ + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `width` | number | 240 | 表示領域の幅 | +| `height` | number | 240 | 表示領域の高さ | +| `fps` | number | 60 | 1秒間に何回描画するか(1〜60) | +| `options` | object | null | オプション設定 | + +### options設定 + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `fullScreen` | boolean | false | Stageで設定した幅と高さを超えて画面全体に描画 | +| `tagId` | string | null | IDを指定すると、指定したIDのエレメント内で描画を行う | +| `bgColor` | string | "transparent" | 背景色を16進数で指定。デフォルトは無色透明 | + +## config.json + +環境ごとの設定を管理するファイルです。`local`、`dev`、`stg`、`prd`、`all`と区切られており、`all`以外は任意の環境名です。 + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +### all設定 + +`all`はどの環境でも書き出される共通変数です。 + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `spa` | boolean | true | Single Page ApplicationとしてURLでシーンを制御 | +| `defaultTop` | string | "top" | ページトップのView。設定がない場合はTopViewクラスが起動 | +| `loading.callback` | string | Loading | ローディング画面のクラス名。start関数とend関数を呼び出す | +| `gotoView.callback` | string \| array | ["callback.Background"] | gotoView完了後のコールバッククラス | + +### platform設定 + +ビルド時の`--platform`で指定した値がセットされます。 + +対応値: `macos`, `windows`, `linux`, `ios`, `android`, `web` + +```typescript +import { config } from "@/config/Config"; + +if (config.platform === "ios") { + // iOS固有の処理 +} +``` + +## routing.json + +ルーティングの設定ファイルです。詳細は[ルーティング](/ja/reference/framework/routing)を参照してください。 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [] + } +} +``` + +## 設定値の取得 + +コード内で設定値を取得するには`config`オブジェクトを使用します。 + +### Config.tsの例 + +```typescript +import stageJson from "./stage.json"; +import configJson from "./config.json"; + +interface IStageConfig { + width: number; + height: number; + fps: number; + options: { + fullScreen: boolean; + tagId: string | null; + bgColor: string; + }; +} + +interface IConfig { + stage: IStageConfig; + api: { + endPoint: string; + }; + content: { + endPoint: string; + }; + spa: boolean; + defaultTop: string; + platform: string; +} + +export const config: IConfig = { + stage: stageJson, + ...configJson +}; +``` + +### 使用例 + +```typescript +import { config } from "@/config/Config"; + +// ステージ設定 +const stageWidth = config.stage.width; +const stageHeight = config.stage.height; + +// API設定 +const apiEndPoint = config.api.endPoint; + +// SPA設定 +const isSpa = config.spa; +``` + +## ローディング画面 + +`loading.callback`で設定したクラスの`start`関数と`end`関数が呼び出されます。 + +```typescript +export class Loading +{ + private shape: Shape; + + constructor() + { + this.shape = new Shape(); + // ローディング表示の初期化 + } + + start(): void + { + // ローディング開始時の処理 + stage.addChild(this.shape); + } + + end(): void + { + // ローディング終了時の処理 + this.shape.remove(); + } +} +``` + +## gotoViewコールバック + +`gotoView.callback`で設定したクラスの`execute`関数が呼び出されます。複数のクラスを配列で設定でき、async/awaitで順次実行されます。 + +```typescript +import { app } from "@next2d/framework"; +import { Shape, stage } from "@next2d/display"; + +export class Background +{ + public readonly shape: Shape; + + constructor() + { + this.shape = new Shape(); + } + + execute(): void + { + const context = app.getContext(); + const view = context.view; + if (!view) return; + + // 背景を最背面に配置 + view.addChildAt(this.shape, 0); + } +} +``` + +## ビルドコマンド + +環境を指定してビルド: + +```bash +# ローカル環境 +npm run build -- --env=local + +# 開発環境 +npm run build -- --env=dev + +# 本番環境 +npm run build -- --env=prd +``` + +プラットフォームを指定: + +```bash +npm run build -- --platform=web +npm run build -- --platform=ios +npm run build -- --platform=android +``` + +## 設定例 + +### 完全な設定ファイルの例 + +#### stage.json + +```json +{ + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "#1461A0" + } +} +``` + +#### config.json + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:3000/" + }, + "content": { + "endPoint": "http://localhost:5500/mock/content/" + } + }, + "dev": { + "api": { + "endPoint": "https://dev-api.example.com/" + }, + "content": { + "endPoint": "https://dev-cdn.example.com/content/" + } + }, + "prd": { + "api": { + "endPoint": "https://api.example.com/" + }, + "content": { + "endPoint": "https://cdn.example.com/content/" + } + }, + "all": { + "spa": true, + "defaultTop": "top", + "loading": { + "callback": "Loading" + }, + "gotoView": { + "callback": ["callback.Background"] + } + } +} +``` + +## 関連項目 + +- [ルーティング](/ja/reference/framework/routing) +- [View/ViewModel](/ja/reference/framework/view) diff --git a/specs/ja/index.md b/specs/ja/index.md new file mode 100644 index 0000000..774e9ee --- /dev/null +++ b/specs/ja/index.md @@ -0,0 +1,345 @@ +# Next2D Framework + +Next2D Frameworkは、Next2D Playerを用いたアプリケーション開発のためのMVVMフレームワークです。シングルページアプリケーション(SPA)のためのルーティング、View/ViewModel管理、環境設定管理などの機能を提供します。 + +## 主な特徴 + +- **MVVMパターン**: Model-View-ViewModelパターンによる関心の分離 +- **クリーンアーキテクチャ**: 依存性の逆転と疎結合な設計 +- **シングルページアプリケーション**: URLベースのシーン管理 +- **Animation Tool連携**: Animation Toolで作成したアセットとの連携 +- **TypeScriptサポート**: 型安全な開発が可能 +- **アトミックデザイン**: 再利用可能なコンポーネント設計を推奨 + +## アーキテクチャ概要 + +このプロジェクトはクリーンアーキテクチャとMVVMパターンを組み合わせて実装されています。 + +```mermaid +graph TB + subgraph ViewLayer["View Layer"] + View["View"] + ViewModel["ViewModel"] + UI["UI Components"] + end + + subgraph InterfaceLayer["Interface Layer"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IResponse"] + end + + subgraph ApplicationLayer["Application Layer"] + UseCase["UseCase"] + end + + subgraph DomainLayer["Domain Layer"] + DomainLogic["Domain Logic"] + DomainService["Service"] + end + + subgraph InfraLayer["Infrastructure Layer"] + Repository["Repository"] + ExternalAPI["External API"] + end + + ViewLayer -.->|interface経由| InterfaceLayer + ViewLayer -.->|calls| ApplicationLayer + ApplicationLayer -.->|interface経由| InterfaceLayer + ApplicationLayer -.->|uses| DomainLayer + ApplicationLayer -.->|calls| InfraLayer + InfraLayer -.->|accesses| ExternalAPI +``` + +### レイヤーの責務 + +| レイヤー | パス | 役割 | +|----------|------|------| +| **View** | `view/*`, `ui/*` | 画面の構造と表示を担当 | +| **ViewModel** | `view/*` | ViewとModelの橋渡し、イベントハンドリング | +| **Interface** | `interface/*` | 抽象化レイヤー、型定義 | +| **Application** | `model/application/*/usecase/*` | ビジネスロジックの実装(UseCase) | +| **Domain** | `model/domain/*` | コアビジネスルール | +| **Infrastructure** | `model/infrastructure/repository/*` | データアクセス、外部API連携 | + +### 依存関係の方向 + +クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かいます。 + +- **View層**: インターフェースを通じてApplication層を使用 +- **Application層**: インターフェースを通じてDomain層とInfrastructure層を使用 +- **Domain層**: 何にも依存しない(純粋なビジネスロジック) +- **Infrastructure層**: Domain層のインターフェースを実装 + +## ディレクトリ構造 + +``` +my-app/ +├── src/ +│ ├── config/ # 設定ファイル +│ │ ├── stage.json # ステージ設定 +│ │ ├── config.json # 環境設定 +│ │ ├── routing.json # ルーティング設定 +│ │ └── Config.ts # 設定の型定義とエクスポート +│ │ +│ ├── interface/ # インターフェース定義 +│ │ ├── IDraggable.ts # ドラッグ可能なオブジェクト +│ │ ├── ITextField.ts # テキストフィールド +│ │ ├── IHomeTextResponse.ts # APIレスポンス型 +│ │ └── IViewName.ts # 画面名の型定義 +│ │ +│ ├── view/ # View & ViewModel +│ │ ├── top/ +│ │ │ ├── TopView.ts # 画面の構造定義 +│ │ │ └── TopViewModel.ts # ビジネスロジックとの橋渡し +│ │ └── home/ +│ │ ├── HomeView.ts +│ │ └── HomeViewModel.ts +│ │ +│ ├── model/ +│ │ ├── application/ # アプリケーション層 +│ │ │ ├── top/ +│ │ │ │ └── usecase/ +│ │ │ │ └── NavigateToViewUseCase.ts +│ │ │ └── home/ +│ │ │ └── usecase/ +│ │ │ ├── StartDragUseCase.ts +│ │ │ ├── StopDragUseCase.ts +│ │ │ └── CenterTextFieldUseCase.ts +│ │ │ +│ │ ├── domain/ # ドメイン層 +│ │ │ └── callback/ +│ │ │ ├── Background.ts +│ │ │ └── Background/ +│ │ │ └── service/ +│ │ │ ├── BackgroundDrawService.ts +│ │ │ └── BackgroundChangeScaleService.ts +│ │ │ +│ │ └── infrastructure/ # インフラ層 +│ │ └── repository/ +│ │ └── HomeTextRepository.ts +│ │ +│ ├── ui/ # UIコンポーネント +│ │ ├── animation/ # アニメーション定義 +│ │ │ └── top/ +│ │ │ └── TopBtnShowAnimation.ts +│ │ │ +│ │ ├── component/ # アトミックデザイン +│ │ │ ├── atom/ # 最小単位のコンポーネント +│ │ │ │ ├── ButtonAtom.ts +│ │ │ │ └── TextAtom.ts +│ │ │ ├── molecule/ # Atomを組み合わせたコンポーネント +│ │ │ │ ├── HomeBtnMolecule.ts +│ │ │ │ └── TopBtnMolecule.ts +│ │ │ ├── organism/ # 複数Moleculeの組み合わせ +│ │ │ ├── template/ # ページテンプレート +│ │ │ └── page/ # ページコンポーネント +│ │ │ ├── top/ +│ │ │ │ └── TopPage.ts +│ │ │ └── home/ +│ │ │ └── HomePage.ts +│ │ │ +│ │ └── content/ # Animation Tool生成コンテンツ +│ │ ├── TopContent.ts +│ │ └── HomeContent.ts +│ │ +│ ├── assets/ # 静的アセット +│ │ +│ ├── Packages.ts # パッケージエクスポート +│ └── index.ts # エントリーポイント +│ +├── file/ # Animation Tool出力ファイル +│ └── sample.n2d +│ +├── mock/ # モックデータ +│ ├── api/ # APIモック +│ ├── content/ # コンテンツモック +│ └── img/ # 画像モック +│ +└── package.json +``` + +## フレームワークフローチャート + +gotoView関数による画面遷移の詳細なフローを示します。 + +```mermaid +graph TD + User([User]) -->|Request| GotoView[gotoView Path] + + GotoView --> LoadingCheck{use loading?
Default: true} + + LoadingCheck -->|YES| ScreenOverlay[Screen Overlay] + LoadingCheck -->|NO| RemoveResponse + ScreenOverlay --> LoadingStart[Start Loading] + LoadingStart --> RemoveResponse + + RemoveResponse[Remove Previous Response Data] --> ParseQuery[Parse Query String] + ParseQuery --> UpdateHistory{SPA mode?} + + UpdateHistory -->|YES| PushState[Push History State] + UpdateHistory -->|NO| RequestType + PushState --> RequestType + + RequestType[Request Type] + + RequestType --> JSON[JSON: Get external JSON data] + RequestType --> CONTENT[CONTENT: Get Animation Tool JSON] + RequestType --> CUSTOM[CUSTOM: Request to external API] + + JSON --> CacheCheck{use cache?
Default: false} + CONTENT --> CacheCheck + CUSTOM --> CacheCheck + + CacheCheck -->|YES| CacheData[(Cache)] + CacheCheck -->|NO| GlobalData{{Global Network}} + + CacheData --> Cached{Cached?} + + Cached -->|NO| GlobalData + Cached -->|YES| RegisterResponse + GlobalData --> RegisterResponse + + RegisterResponse[Register Response Data] --> RequestCallback{request callback?} + + RequestCallback -->|YES| ExecRequestCallback[Execute Request Callback] + RequestCallback -->|NO| UnbindView + ExecRequestCallback --> UnbindView + + UnbindView[Previous View: onExit & Unbind] --> BindView[New View/ViewModel: Bind] + BindView --> ViewModelInit[ViewModel: initialize] + + ViewModelInit --> ViewInit[View: initialize] + ViewInit --> AddToStage[Add View to Stage] + AddToStage --> GotoViewCallback{gotoView callback?} + + GotoViewCallback -->|YES| ExecGotoViewCallback[Execute gotoView Callback] + GotoViewCallback -->|NO| LoadingEndCheck + ExecGotoViewCallback --> LoadingEndCheck + + LoadingEndCheck{use loading?
Default: true} + + LoadingEndCheck -->|YES| LoadingEnd[End Loading] + LoadingEndCheck -->|NO| OnEnter + LoadingEnd --> DisposeOverlay[Dispose Screen Overlay] + DisposeOverlay --> OnEnter + + OnEnter[View: onEnter] --> StartDrawing + + StartDrawing[Start Drawing] -->|Response| User + + style User fill:#d5e8d4,stroke:#82b366 + style StartDrawing fill:#dae8fc,stroke:#6c8ebf + style CacheData fill:#fff2cc,stroke:#d6b656 + style GlobalData fill:#f5f5f5,stroke:#666666 +``` + +### フローの主要ステップ + +| ステップ | 説明 | +|----------|------| +| **gotoView** | 画面遷移のエントリーポイント | +| **Loading** | ローディング画面の表示/非表示制御 | +| **Request Type** | JSON、CONTENT、CUSTOMの3種類のリクエスト | +| **Cache** | レスポンスデータのキャッシュ制御 | +| **View/ViewModel Bind** | 新しいView/ViewModelのバインド処理 | +| **onEnter** | 画面表示完了後のコールバック | + +## 主要な設計パターン + +### 1. MVVM (Model-View-ViewModel) + +- **View**: 画面の構造と表示を担当。ビジネスロジックは持たない +- **ViewModel**: ViewとModelの橋渡し。UseCaseを保持し、イベントを処理 +- **Model**: ビジネスロジックとデータアクセスを担当 + +### 2. UseCaseパターン + +各ユーザーアクションに対して、専用のUseCaseクラスを作成: + +```typescript +export class StartDragUseCase +{ + execute(target: IDraggable): void + { + target.startDrag(); + } +} +``` + +### 3. 依存性の逆転 (Dependency Inversion) + +具象クラスではなく、インターフェースに依存: + +```typescript +// 良い例: インターフェースに依存 +import type { IDraggable } from "@/interface/IDraggable"; + +function startDrag(target: IDraggable): void +{ + target.startDrag(); +} +``` + +### 4. Repositoryパターン + +データアクセスを抽象化し、エラーハンドリングも実装: + +```typescript +export class HomeTextRepository +{ + static async get(): Promise + { + try { + const response = await fetch(`${config.api.endPoint}api/home.json`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Failed to fetch:", error); + throw error; + } + } +} +``` + +## クイックスタート + +### プロジェクトの作成 + +```bash +npx create-next2d-app my-app +cd my-app +npm install +npm start +``` + +### View/ViewModelの自動生成 + +```bash +npm run generate +``` + +このコマンドは`routing.json`のトッププロパティを解析し、対応するViewとViewModelクラスを生成します。 + +## ベストプラクティス + +1. **インターフェース優先**: 具象型ではなく、常にインターフェースに依存 +2. **単一責任の原則**: 各クラスは1つの責務のみを持つ +3. **依存性注入**: コンストラクタで依存を注入 +4. **エラーハンドリング**: Repository層で適切にエラーを処理 +5. **型安全性**: `any`型を避け、明示的な型定義を使用 + +## 関連ドキュメント + +### 基本 +- [View/ViewModel](/ja/reference/framework/view) - 画面表示とデータバインディング +- [ルーティング](/ja/reference/framework/routing) - URLベースの画面遷移 +- [設定ファイル](/ja/reference/framework/config) - 環境設定とステージ設定 +- [Animation Tool連携](/ja/reference/framework/animation-tool) - Animation Toolアセットの活用 + +### Next2D Player連携 +- [Next2D Player](/ja/reference/player) - レンダリングエンジン +- [MovieClip](/ja/reference/player/movie-clip) - タイムラインアニメーション +- [イベントシステム](/ja/reference/player/events) - ユーザーインタラクション diff --git a/specs/ja/routing.md b/specs/ja/routing.md new file mode 100644 index 0000000..28f1f63 --- /dev/null +++ b/specs/ja/routing.md @@ -0,0 +1,388 @@ +# ルーティング + +Next2D FrameworkはシングルページアプリケーションとしてURLでシーンを制御できます。ルーティングは`routing.json`で設定します。 + +## 基本設定 + +ルーティングのトッププロパティは英数字とスラッシュが使用できます。スラッシュをキーにCamelCaseでViewクラスにアクセスします。 + +```json +{ + "top": { + "requests": [] + }, + "home": { + "requests": [] + }, + "quest/list": { + "requests": [] + } +} +``` + +上記の場合: +- `top` → `TopView`クラス +- `home` → `HomeView`クラス +- `quest/list` → `QuestListView`クラス + +## ルート定義 + +### 基本的なルート + +```json +{ + "top": { + "requests": [] + } +} +``` + +アクセス: `https://example.com/` または `https://example.com/top` + +### セカンドレベルプロパティ + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `private` | boolean | false | URLでの直接アクセスを制御。trueの場合、URLでアクセスするとTopViewが読み込まれる | +| `requests` | array | null | Viewがbindされる前にリクエストを送信 | + +### プライベートルート + +URLでの直接アクセスを禁止したい場合: + +```json +{ + "quest/detail": { + "private": true, + "requests": [] + } +} +``` + +`private: true`の場合、URLで直接アクセスすると`TopView`にリダイレクトされます。プログラムからの`app.gotoView()`でのみアクセス可能です。 + +## requestsの設定 + +Viewがbindされる前にデータを取得できます。取得したデータは`app.getResponse()`で取得できます。 + +### requests配列の設定項目 + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `type` | string | content | `json`、`content`、`custom`の固定値 | +| `path` | string | empty | リクエスト先のパス | +| `name` | string | empty | `response`にセットするキー名 | +| `cache` | boolean | false | データをキャッシュするか | +| `callback` | string \| array | null | リクエスト完了後のコールバッククラス | +| `class` | string | empty | リクエストを実行するクラス(typeがcustomの場合のみ) | +| `access` | string | public | 関数へのアクセス修飾子(`public`または`static`) | +| `method` | string | empty | 実行する関数名(typeがcustomの場合のみ) | + +### typeの種類 + +#### json + +外部JSONデータを取得: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + } + ] + } +} +``` + +#### content + +Animation ToolのJSONを取得: + +```json +{ + "top": { + "requests": [ + { + "type": "content", + "path": "{{content.endPoint}}top.json", + "name": "TopContent" + } + ] + } +} +``` + +#### custom + +カスタムクラスでリクエストを実行: + +```json +{ + "user/profile": { + "requests": [ + { + "type": "custom", + "class": "repository.UserRepository", + "access": "static", + "method": "getProfile", + "name": "UserProfile" + } + ] + } +} +``` + +### 変数の展開 + +`{{***}}`で囲むと`config.json`の変数を取得できます: + +```json +{ + "path": "{{api.endPoint}}path/to/api" +} +``` + +### キャッシュの利用 + +`cache: true`を設定すると、データがキャッシュされます。キャッシュしたデータは画面遷移しても初期化されません。 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/master.json", + "name": "MasterData", + "cache": true + } + ] + } +} +``` + +キャッシュデータの取得: + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +### コールバック + +リクエスト完了後にコールバックを実行: + +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData", + "callback": "callback.HomeDataCallback" + } + ] + } +} +``` + +コールバッククラス: + +```typescript +export class HomeDataCallback +{ + constructor(data: any) + { + // 取得したデータが渡される + } + + execute(): void + { + // コールバック処理 + } +} +``` + +## 画面遷移 + +### app.gotoView() + +`app.gotoView()`で画面遷移を行います: + +```typescript +import { app } from "@next2d/framework"; + +// 基本的な遷移 +await app.gotoView("home"); + +// パスで遷移 +await app.gotoView("quest/list"); + +// クエリパラメータ付き +await app.gotoView("quest/detail?id=123"); +``` + +### UseCaseでの画面遷移 + +画面遷移はUseCaseで行うことを推奨します: + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +ViewModelでの使用: + +```typescript +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +## レスポンスデータの取得 + +`requests`で取得したデータは`app.getResponse()`で取得できます: + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("TopText")) { + const topText = response.get("TopText") as { word: string }; + this.text = topText.word; + } +} +``` + +**注意:** `response`データは画面遷移すると初期化されます。画面を跨いで保持したいデータは`cache: true`を設定してください。 + +## SPAモード + +`config.json`の`all.spa`で設定します: + +```json +{ + "all": { + "spa": true + } +} +``` + +- `true`: URLでシーンを制御(History API使用) +- `false`: URLによるシーン制御を無効化 + +## デフォルトのトップページ + +`config.json`で設定: + +```json +{ + "all": { + "defaultTop": "top" + } +} +``` + +設定がない場合は`TopView`クラスが起動します。 + +## View/ViewModelの自動生成 + +`routing.json`の設定から自動生成できます: + +```bash +npm run generate +``` + +このコマンドは`routing.json`のトッププロパティを解析し、対応するViewとViewModelクラスを生成します。 + +## 設定例 + +### 完全な routing.json の例 + +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + }, + { + "type": "content", + "path": "{{content.endPoint}}home.json", + "name": "HomeContent", + "cache": true + } + ] + }, + "quest/list": { + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getList", + "name": "QuestList" + } + ] + }, + "quest/detail": { + "private": true, + "requests": [ + { + "type": "custom", + "class": "repository.QuestRepository", + "access": "static", + "method": "getDetail", + "name": "QuestDetail" + } + ] + } +} +``` + +## 関連項目 + +- [View/ViewModel](/ja/reference/framework/view) +- [設定ファイル](/ja/reference/framework/config) diff --git a/specs/ja/view.md b/specs/ja/view.md new file mode 100644 index 0000000..14b9b62 --- /dev/null +++ b/specs/ja/view.md @@ -0,0 +1,459 @@ +# View と ViewModel + +Next2D FrameworkはMVVM(Model-View-ViewModel)パターンを採用しています。1画面にViewとViewModelをワンセット作成するのが基本スタイルです。 + +## アーキテクチャ + +```mermaid +graph TB + subgraph ViewLayer["View Layer"] + ViewRole["画面の構造と表示を担当"] + ViewRule["ビジネスロジックは持たない"] + end + + subgraph ViewModelLayer["ViewModel Layer"] + VMRole1["ViewとModelの橋渡し"] + VMRole2["UseCaseを保持"] + VMRole3["イベントハンドリング"] + end + + subgraph ModelLayer["Model Layer"] + ModelRole1["ビジネスロジック(UseCase)"] + ModelRole2["データアクセス(Repository)"] + end + + ViewLayer <-->|双方向| ViewModelLayer + ViewModelLayer <--> ModelLayer +``` + +## ディレクトリ構造 + +``` +src/ +└── view/ + ├── top/ + │ ├── TopView.ts + │ └── TopViewModel.ts + └── home/ + ├── HomeView.ts + └── HomeViewModel.ts +``` + +## View + +Viewはメインコンテキストにアタッチされるコンテナです。Viewは表示構造のみを担当し、ビジネスロジックはViewModelに委譲します。 + +### Viewの責務 + +- **画面の構造定義** - UIコンポーネントの配置と座標設定 +- **イベントリスナーの登録** - ViewModelのメソッドと接続 +- **ライフサイクル管理** - `initialize`, `onEnter`, `onExit` + +### 基本構造 + +```typescript +import type { TopViewModel } from "./TopViewModel"; +import { View } from "@next2d/framework"; +import { TopPage } from "@/ui/component/page/top/TopPage"; + +export class TopView extends View +{ + private readonly _topPage: TopPage; + + constructor(vm: TopViewModel) + { + super(vm); + this._topPage = new TopPage(); + this.addChild(this._topPage); + } + + async initialize(): Promise + { + this._topPage.initialize(this.vm); + } + + async onEnter(): Promise + { + await this._topPage.onEnter(); + } + + async onExit(): Promise + { + return void 0; + } +} +``` + +### ライフサイクル + +```mermaid +sequenceDiagram + participant Framework as Framework + participant VM as ViewModel + participant View as View + participant UI as UI Components + + Note over Framework,UI: 画面遷移開始 + + Framework->>VM: new ViewModel() + Framework->>VM: initialize() + Note over VM: ViewModelが先に初期化される + + Framework->>View: new View(vm) + Framework->>View: initialize() + View->>UI: コンポーネント作成 + View->>VM: イベントリスナー登録 + + Framework->>View: onEnter() + View->>UI: アニメーション開始 + + Note over Framework,UI: ユーザーが画面を操作 + + Framework->>View: onExit() + View->>UI: クリーンアップ +``` + +#### initialize() - 初期化 + +**呼び出しタイミング:** +- Viewのインスタンスが生成された直後 +- 画面遷移時に1回だけ呼び出される +- ViewModelの`initialize()`より**後**に実行される + +**主な用途:** +- UIコンポーネントの生成と配置 +- イベントリスナーの登録 +- 子要素の追加(`addChild`) + +```typescript +async initialize(): Promise +{ + const { HomeBtnMolecule } = await import("@/ui/component/molecule/HomeBtnMolecule"); + const { PointerEvent } = next2d.events; + + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; + + // イベントをViewModelに委譲 + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); +} +``` + +#### onEnter() - 画面表示時 + +**呼び出しタイミング:** +- `initialize()`の実行完了後 +- 画面が表示される直前 + +**主な用途:** +- 入場アニメーションの開始 +- タイマーやインターバルの開始 +- フォーカス設定 + +```typescript +async onEnter(): Promise +{ + const topBtn = this.getChildByName("topBtn") as TopBtnMolecule; + topBtn.playEntrance(() => { + console.log("アニメーション完了"); + }); +} +``` + +#### onExit() - 画面非表示時 + +**呼び出しタイミング:** +- 別の画面に遷移する直前 +- Viewが破棄される前 + +**主な用途:** +- アニメーションの停止 +- タイマーやインターバルのクリア +- リソースの解放 + +```typescript +async onExit(): Promise +{ + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } +} +``` + +## ViewModel + +ViewModelはViewとModelの橋渡しを行います。UseCaseを保持し、Viewからのイベントを処理してビジネスロジックを実行します。 + +### ViewModelの責務 + +- **イベント処理** - Viewからのイベントを受け取る +- **UseCaseの実行** - ビジネスロジックを呼び出す +- **依存性の管理** - UseCaseのインスタンスを保持 +- **状態管理** - 画面固有の状態を管理 + +### 基本構造 + +```typescript +import { ViewModel, app } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; + +export class TopViewModel extends ViewModel +{ + private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; + + constructor() + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + + async initialize(): Promise + { + // routing.jsonのrequestsで取得したデータを受け取る + const response = app.getResponse(); + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; + } + + getTopText(): string + { + return this.topText; + } + + async onClickStartButton(): Promise + { + await this.navigateToViewUseCase.execute("home"); + } +} +``` + +### ViewModelの初期化タイミング + +**重要: ViewModelの`initialize()`はViewの`initialize()`より前に呼び出されます。** + +``` +1. ViewModel のインスタンス生成 + ↓ +2. ViewModel.initialize() ← ViewModelが先 + ↓ +3. View のインスタンス生成(ViewModelを注入) + ↓ +4. View.initialize() + ↓ +5. View.onEnter() +``` + +これにより、Viewの初期化時にはViewModelのデータが既に準備されています。 + +```typescript +// HomeViewModel.ts +export class HomeViewModel extends ViewModel +{ + private homeText: string = ""; + + async initialize(): Promise + { + // ViewModelのinitializeで事前にデータ取得 + const data = await HomeTextRepository.get(); + this.homeText = data.word; + } + + getHomeText(): string + { + return this.homeText; + } +} + +// HomeView.ts +export class HomeView extends View +{ + constructor(private readonly vm: HomeViewModel) + { + super(); + } + + async initialize(): Promise + { + // この時点でvm.initialize()は既に完了している + const text = this.vm.getHomeText(); + + // 取得済みのデータを使ってUIを構築 + const textField = new TextAtom(text); + this.addChild(textField); + } +} +``` + +## 画面遷移 + +画面遷移には`app.gotoView()`を使用します。 + +```typescript +import { app } from "@next2d/framework"; + +// 指定のViewに遷移 +await app.gotoView("home"); + +// パラメータ付きで遷移 +await app.gotoView("user/detail?id=123"); +``` + +### UseCaseでの画面遷移 + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase +{ + async execute(viewName: string): Promise + { + await app.gotoView(viewName); + } +} +``` + +## レスポンスデータの取得 + +`routing.json`で設定した`requests`のデータは`app.getResponse()`で取得できます。 + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); + + if (response.has("UserData")) { + const userData = response.get("UserData"); + this.userName = userData.name; + } +} +``` + +## キャッシュデータの取得 + +`cache: true`を設定したデータは`app.getCache()`で取得できます。 + +```typescript +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} +``` + +## 設計原則 + +### 1. 関心の分離 + +```typescript +// 良い例: Viewは表示のみ、ViewModelはロジック +class HomeView extends View +{ + async initialize(): Promise + { + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick); + } +} + +class HomeViewModel extends ViewModel +{ + onClick(event: PointerEvent): void + { + this.someUseCase.execute(); + } +} +``` + +### 2. 依存性の逆転 + +ViewModelはインターフェースに依存し、具象クラスに依存しません。 + +```typescript +// 良い例: インターフェースに依存 +homeContentPointerDownEvent(event: PointerEvent): void +{ + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); +} +``` + +### 3. イベントは必ずViewModelに委譲 + +View内でイベント処理を完結させず、必ずViewModelに委譲します。 + +## View/ViewModel作成のテンプレート + +### View + +```typescript +import type { YourViewModel } from "./YourViewModel"; +import { View } from "@next2d/framework"; + +export class YourView extends View +{ + constructor(vm: YourViewModel) + { + super(vm); + } + + async initialize(): Promise + { + // UIコンポーネントの作成と配置 + } + + async onEnter(): Promise + { + // 画面表示時の処理 + } + + async onExit(): Promise + { + // 画面非表示時の処理 + } +} +``` + +### ViewModel + +```typescript +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/your/usecase/YourUseCase"; + +export class YourViewModel extends ViewModel +{ + private readonly yourUseCase: YourUseCase; + + constructor() + { + super(); + this.yourUseCase = new YourUseCase(); + } + + async initialize(): Promise + { + return void 0; + } + + yourEventHandler(event: Event): void + { + this.yourUseCase.execute(); + } +} +``` + +## 関連項目 + +- [ルーティング](/ja/reference/framework/routing) +- [設定ファイル](/ja/reference/framework/config) diff --git a/src/application/Application.test.ts b/src/application/Application.test.ts new file mode 100644 index 0000000..5a000c9 --- /dev/null +++ b/src/application/Application.test.ts @@ -0,0 +1,268 @@ +import { Application } from "./Application"; +import { MovieClip } from "@next2d/display"; +import { Context } from "./Context"; +import { $setConfig, $getConfig } from "./variable/Config"; +import { $setContext, $getContext } from "./variable/Context"; +import { $setPackages, packages } from "./variable/Packages"; +import { response } from "../infrastructure/variable/Response"; +import { cache } from "./variable/Cache"; +import { View } from "../view/View"; +import { ViewModel } from "../view/ViewModel"; +import { describe, expect, it, beforeEach, vi } from "vitest"; + +Object.defineProperty(window, "next2d", { + "value": { + "createRootMovieClip": vi.fn().mockResolvedValue(new MovieClip()) + }, + "writable": true +}); + +describe("Application Test", () => +{ + beforeEach(() => + { + response.clear(); + cache.clear(); + packages.clear(); + }); + + describe("constructor", () => + { + it("should initialize with default values", () => + { + const app = new Application(); + + expect(app.popstate).toBe(false); + expect(app.currentName).toBe(""); + }); + }); + + describe("initialize", () => + { + it("should initialize application with config and packages", () => + { + const app = new Application(); + const config = { + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }; + + class TestClass {} + const pkgs = [["TestClass", TestClass]] as const; + + const result = app.initialize(config, pkgs); + + expect(result).toBe(app); + expect($getConfig()).toEqual(config); + expect(packages.has("TestClass")).toBe(true); + }); + + it("should initialize with empty packages", () => + { + const app = new Application(); + const config = { + "platform": "web", + "spa": true, + "stage": { + "width": 1920, + "height": 1080, + "fps": 30 + } + }; + + const result = app.initialize(config, []); + + expect(result).toBe(app); + expect($getConfig()).toEqual(config); + }); + }); + + describe("run", () => + { + it("should run the application", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const app = new Application(); + + await expect(app.run()).resolves.toBeUndefined(); + }); + }); + + describe("gotoView", () => + { + it("should navigate to specified view", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + class TestViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class TestView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} + } + + packages.set("TopView", TestView); + packages.set("TopViewModel", TestViewModel); + + const root = new MovieClip(); + $setContext(new Context(root)); + + const app = new Application(); + + await app.gotoView("top"); + + expect(app.currentName).toBe("top"); + }); + + it("should navigate with default empty name", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "defaultTop": "home", + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + class HomeViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class HomeView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} + } + + packages.set("HomeView", HomeView); + packages.set("HomeViewModel", HomeViewModel); + + const root = new MovieClip(); + $setContext(new Context(root)); + + const app = new Application(); + + await app.gotoView(); + + expect(app.currentName).toBe("home"); + }); + }); + + describe("getContext", () => + { + it("should return the current context", () => + { + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + const app = new Application(); + const result = app.getContext(); + + expect(result).toBe(context); + expect(result.root).toBe(root); + }); + }); + + describe("getResponse", () => + { + it("should return the response map", () => + { + const app = new Application(); + + response.set("test", { "data": "value" }); + + const result = app.getResponse(); + + expect(result).toBe(response); + expect(result.get("test")).toEqual({ "data": "value" }); + }); + + it("should return empty map when no responses", () => + { + const app = new Application(); + const result = app.getResponse(); + + expect(result.size).toBe(0); + }); + }); + + describe("getCache", () => + { + it("should return the cache map", () => + { + const app = new Application(); + + cache.set("cacheKey", { "cached": true }); + + const result = app.getCache(); + + expect(result).toBe(cache); + expect(result.get("cacheKey")).toEqual({ "cached": true }); + }); + + it("should return empty map when no cache", () => + { + const app = new Application(); + const result = app.getCache(); + + expect(result.size).toBe(0); + }); + }); + + describe("popstate", () => + { + it("should be settable", () => + { + const app = new Application(); + + expect(app.popstate).toBe(false); + app.popstate = true; + expect(app.popstate).toBe(true); + }); + }); + + describe("currentName", () => + { + it("should be settable", () => + { + const app = new Application(); + + expect(app.currentName).toBe(""); + app.currentName = "newPage"; + expect(app.currentName).toBe("newPage"); + }); + }); +}); diff --git a/src/application/Application.ts b/src/application/Application.ts index 7ea6226..984a407 100644 --- a/src/application/Application.ts +++ b/src/application/Application.ts @@ -1,12 +1,12 @@ import type { IConfig } from "../interface/IConfig"; import type { IPackages } from "../interface/IPackages"; import type { Context } from "./Context"; -import { execute as applicationInitializeService } from "./Application/service/ApplicationInitializeService"; -import { execute as applicationGotoViewUseCase } from "./Application/usecase/ApplicationGotoViewUseCase"; -import { execute as contextRunService } from "./Context/service/ContextRunService"; +import { execute as applicationInitializeUseCase } from "./usecase/ApplicationInitializeUseCase"; +import { execute as applicationGotoViewUseCase } from "./usecase/ApplicationGotoViewUseCase"; +import { execute as contextRunUseCase } from "./usecase/ContextRunUseCase"; import { $getConfig } from "./variable/Config"; import { $getContext } from "./variable/Context"; -import { response } from "../infrastructure/Response/variable/Response"; +import { response } from "../infrastructure/variable/Response"; import { cache } from "./variable/Cache"; /** @@ -57,7 +57,7 @@ export class Application */ initialize (config: IConfig, packages: IPackages): Application { - return applicationInitializeService(this, config, packages); + return applicationInitializeUseCase(this, config, packages); } /** @@ -70,7 +70,7 @@ export class Application */ async run (): Promise { - await contextRunService($getConfig()); + await contextRunUseCase($getConfig()); } /** @@ -105,11 +105,11 @@ export class Application * @description configで設定したリクエストのレスポンスマップを返却します * Returns the response map of the request set in config * - * @return {Map} + * @return {Map} * @method * @public */ - getResponse (): Map + getResponse (): Map { return response; } @@ -118,11 +118,11 @@ export class Application * @description キャッシュのMapオブジェクトを返却します * Returns the Map object of the cache * - * @return {Map} + * @return {Map} * @method * @public */ - getCache (): Map + getCache (): Map { return cache; } diff --git a/src/application/Application/service/ApplicationInitializeService.test.ts b/src/application/Application/service/ApplicationInitializeService.test.ts deleted file mode 100644 index b7d3c8c..0000000 --- a/src/application/Application/service/ApplicationInitializeService.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { IConfig } from "../../../interface/IConfig"; -import type { IPackages } from "../../../interface/IPackages"; -import { execute } from "./ApplicationInitializeService"; -import { Application } from "../../Application"; -import { View } from "../../../view/View"; -import { $getConfig } from "../../variable/Config"; -import { packages } from "../../variable/Packages"; -import { describe, expect, it, vi } from "vitest"; - -describe("ApplicationInitializeService", () => -{ - it("test case", () => - { - let state = ""; - window.addEventListener = vi.fn((name) => - { - state = name; - }); - - const app = new Application(); - const config: IConfig = { - "platform": "web", - "stage": { - "width": 640, - "height": 480, - "fps": 60 - }, - "spa": true - }; - - const buildPackages: IPackages = [[ - "view", View - ]]; - - expect(state).toBe(""); - expect(packages.size).toBe(0); - expect($getConfig()).toBe(undefined); - - execute(app, config, buildPackages); - - expect($getConfig()).toBe(config); - expect(packages.size).toBe(1); - expect(packages.get("view")).toBe(View); - expect(state).toBe("popstate"); - }); - - it("test case", () => - { - let state = ""; - window.addEventListener = vi.fn((name) => - { - state = name; - }); - - const app = new Application(); - const config: IConfig = { - "platform": "web", - "stage": { - "width": 640, - "height": 480, - "fps": 60 - }, - "spa": false - }; - - const buildPackages: IPackages = [[ - "view", View - ]]; - - expect(state).toBe(""); - execute(app, config, buildPackages); - expect(state).toBe(""); - }); -}); \ No newline at end of file diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts deleted file mode 100644 index bab84bb..0000000 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { execute } from "./ApplicationGotoViewUseCase"; -import { describe, expect, it, vi, beforeEach } from "vitest"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../Context"; -import { $setConfig } from "../../variable/Config"; -import { $setContext } from "../../variable/Context"; -import { response } from "../../../infrastructure/Response/variable/Response"; - -vi.mock("../../../domain/screen/Capture/service/AddScreenCaptureService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) -})); - -vi.mock("../../../domain/screen/Capture/service/DisposeCaptureService", () => ({ - execute: vi.fn() -})); - -vi.mock("../../../domain/loading/Loading/service/LoadingStartService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) -})); - -vi.mock("../../../domain/loading/Loading/service/LoadingEndService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) -})); - -vi.mock("../../../infrastructure/Response/usecase/ResponseRemoveVariableUseCase", () => ({ - execute: vi.fn() -})); - -vi.mock("../service/ApplicationQueryStringParserService", () => ({ - execute: vi.fn() -})); - -vi.mock("../../../infrastructure/Request/usecase/RequestUseCase", () => ({ - execute: vi.fn().mockResolvedValue([]) -})); - -vi.mock("../../../domain/callback/service/CallbackService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) -})); - -describe("ApplicationGotoViewUseCase Test", () => -{ - let mockApplication: any; - let mockContext: Context; - let root: MovieClip; - - beforeEach(() => - { - response.clear(); - - mockApplication = { - currentName: "", - popstate: false - }; - - root = new MovieClip(); - mockContext = new Context(root); - mockContext.unbind = vi.fn().mockResolvedValue(undefined); - mockContext.bind = vi.fn().mockResolvedValue(null); - - $setContext(mockContext); - $setConfig({ - platform: "web", - spa: false, - stage: { - width: 800, - height: 600, - fps: 60 - } - }); - - global.history = { - pushState: vi.fn() - } as any; - - global.location = { - origin: "http://localhost" - } as any; - - vi.clearAllMocks(); - }); - - it("execute test case1: basic navigation without loading", async () => - { - const { execute: applicationQueryStringParserService } = await import("../service/ApplicationQueryStringParserService"); - const { execute: requestUseCase } = await import("../../../infrastructure/Request/usecase/RequestUseCase"); - - vi.mocked(applicationQueryStringParserService).mockReturnValue({ - name: "home", - queryString: "" - }); - - vi.mocked(requestUseCase).mockResolvedValue([]); - - await execute(mockApplication, "home"); - - expect(mockContext.unbind).toHaveBeenCalled(); - expect(mockApplication.currentName).toBe("home"); - expect(mockContext.bind).toHaveBeenCalledWith("home"); - }); - - it("execute test case2: navigation with response data", async () => - { - const { execute: applicationQueryStringParserService } = await import("../service/ApplicationQueryStringParserService"); - const { execute: requestUseCase } = await import("../../../infrastructure/Request/usecase/RequestUseCase"); - - vi.mocked(applicationQueryStringParserService).mockReturnValue({ - name: "dashboard", - queryString: "" - }); - - const mockResponses = [ - { name: "user", response: { id: 1, name: "Test User" } }, - { name: "settings", response: { theme: "dark" } } - ]; - - vi.mocked(requestUseCase).mockResolvedValue(mockResponses); - - await execute(mockApplication, "dashboard"); - - expect(response.get("user")).toEqual({ id: 1, name: "Test User" }); - expect(response.get("settings")).toEqual({ theme: "dark" }); - expect(mockApplication.currentName).toBe("dashboard"); - }); - - it("execute test case3: handle response without name", async () => - { - const { execute: applicationQueryStringParserService } = await import("../service/ApplicationQueryStringParserService"); - const { execute: requestUseCase } = await import("../../../infrastructure/Request/usecase/RequestUseCase"); - - vi.mocked(applicationQueryStringParserService).mockReturnValue({ - name: "test", - queryString: "" - }); - - const mockResponses = [ - { name: "", response: { data: "should be skipped" } }, - { name: "valid", response: { data: "should be set" } } - ]; - - vi.mocked(requestUseCase).mockResolvedValue(mockResponses); - - await execute(mockApplication, "test"); - - expect(response.has("")).toBe(false); - expect(response.get("valid")).toEqual({ data: "should be set" }); - }); -}); diff --git a/src/application/Context.ts b/src/application/Context.ts index 761b481..0a2afe3 100644 --- a/src/application/Context.ts +++ b/src/application/Context.ts @@ -1,8 +1,6 @@ import type { View } from "../view/View"; import type { ViewModel } from "../view/ViewModel"; import type { Sprite } from "@next2d/display"; -import { execute as contextUnbindService } from "./Context/service/ContextUnbindService"; -import { execute as contextBindUseCase } from "./Context/usecase/ContextBindUseCase"; /** * @description メインコンテキスト、ViewとViewModelのunbind、bindをコントロールします。 @@ -65,31 +63,4 @@ export class Context { return this._$root; } - - /** - * @description ViewクラスをrootのSpriteにアタッチします。 - * Attach the View class to the root Sprite. - * - * @param {string} name - * @return {Promise} - * @method - * @public - */ - async bind (name: string): Promise - { - return await contextBindUseCase(this, name); - } - - /** - * @description ViewとViewModelのバインドを解除します。 - * Unbinds View and ViewModel. - * - * @return {Promise} - * @method - * @public - */ - async unbind (): Promise - { - await contextUnbindService(this); - } } \ No newline at end of file diff --git a/src/application/Context/service/ContextToCamelCaseService.test.ts b/src/application/Context/service/ContextToCamelCaseService.test.ts deleted file mode 100644 index dd030d5..0000000 --- a/src/application/Context/service/ContextToCamelCaseService.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { execute } from "./ContextToCamelCaseService"; -import { describe, expect, it } from "vitest"; - -describe("ContextToCamelCaseService Test", () => -{ - it("execute test case1", () => - { - expect(execute("home")).toBe("Home"); - }); - - it("execute test case2", () => - { - expect(execute("quest/list")).toBe("QuestList"); - }); - - it("execute test case3", () => - { - expect(execute("game/list/page")).toBe("GameListPage"); - }); -}); \ No newline at end of file diff --git a/src/application/Context/service/ContextToCamelCaseService.ts b/src/application/Context/service/ContextToCamelCaseService.ts deleted file mode 100644 index fbc423f..0000000 --- a/src/application/Context/service/ContextToCamelCaseService.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @description キャメルケースに変換 - * Convert to CamelCase - * - * @param {string} name - * @return {string} - * @method - * @public - */ -export const execute = (name: string): string => -{ - const names: string[] = name.split("/"); - - let viewName: string = ""; - for (let idx: number = 0; names.length > idx; ++idx) { - name = names[idx]; - viewName += name - .charAt(0) - .toUpperCase() + name.slice(1); - } - - return viewName; -}; \ No newline at end of file diff --git a/src/application/Context/service/ContextUnbindService.test.ts b/src/application/Context/service/ContextUnbindService.test.ts deleted file mode 100644 index 9d4cda7..0000000 --- a/src/application/Context/service/ContextUnbindService.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ViewModel } from "../../../view/ViewModel"; -import { execute } from "./ContextUnbindService"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../application/Context"; -import { $setContext } from "../../../application/variable/Context"; -import { $setConfig } from "../../../application/variable/Config"; -import { View } from "../../../view/View"; -import { describe, expect, it } from "vitest"; - -describe("ContextUnbindService Test", () => -{ - it("execute test case", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - - const root = new MovieClip(); - const context = new Context(root); - $setContext(context); - - let state = "none"; - context.view = new View(); - root.addChild(context.view); - context.viewModel = { - "unbind": (view: View) => - { - state = "unbind"; - } - } as ViewModel; - - expect(state).toBe("none"); - expect(root.numChildren).toBe(1); - - await execute(context); - - expect(state).toBe("unbind"); - expect(root.numChildren).toBe(0); - }); -}); \ No newline at end of file diff --git a/src/application/Context/service/ContextUnbindService.ts b/src/application/Context/service/ContextUnbindService.ts deleted file mode 100644 index 9b21385..0000000 --- a/src/application/Context/service/ContextUnbindService.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Context } from "../../Context"; - -/** - * @description ViewとViewModelのバインドを解除します。 - * Unbinds View and ViewModel. - * - * @param {Context} context - * @return {Promise} - * @method - * @protected - */ -export const execute = async (context: Context): Promise => -{ - if (!context.view || !context.viewModel) { - return ; - } - - await context.viewModel.unbind(context.view); - - const root = context.root; - if (!root) { - return ; - } - - root.removeChild(context.view); -}; \ No newline at end of file diff --git a/src/application/Context/usecase/ContextBindUseCase.test.ts b/src/application/Context/usecase/ContextBindUseCase.test.ts deleted file mode 100644 index 162daf3..0000000 --- a/src/application/Context/usecase/ContextBindUseCase.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { execute } from "./ContextBindUseCase"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../application/Context"; -import { $setContext } from "../../../application/variable/Context"; -import { $setConfig } from "../../../application/variable/Config"; -import { packages } from "../../../application/variable/Packages"; -import { View } from "../../../view/View"; -import { ViewModel } from "../../../view/ViewModel"; -import { describe, expect, it } from "vitest"; - -describe("ContextBindUseCase Test", () => -{ - it("execute test case1", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - - let state = "none"; - class TestViewModel extends ViewModel - { - async bind () - { - state = "bind"; - } - } - - packages.clear(); - packages.set("TestView", View); - packages.set("TestViewModel", TestViewModel); - - const root = new MovieClip(); - const context = new Context(root); - $setContext(context); - - expect(state).toBe("none"); - expect(root.numChildren).toBe(0); - - await execute(context, "test"); - - expect(state).toBe("bind"); - expect(root.numChildren).toBe(1); - }); -}); \ No newline at end of file diff --git a/src/application/Context/usecase/ContextBindUseCase.ts b/src/application/Context/usecase/ContextBindUseCase.ts deleted file mode 100644 index 942366b..0000000 --- a/src/application/Context/usecase/ContextBindUseCase.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Context } from "../../Context"; -import type { View } from "../../../view/View"; -import type { ViewModel } from "../../../view/ViewModel"; -import { packages } from "../../variable/Packages"; -import { execute as contextToCamelCaseService } from "../service/ContextToCamelCaseService"; - -/** - * @description ViewとViewModelのbindを行います。 - * Binds View and ViewModel. - * - * @param {Context} context - * @param {string} name - * @return {Promise} - * @method - * @protected - */ -export const execute = async (context: Context, name: string): Promise => -{ - const viewName = `${contextToCamelCaseService(name)}View`; - const viewModelName = `${viewName}Model`; - - if (!packages.size - || !packages.has(viewName) - || !packages.has(viewModelName) - ) { - throw new Error("not found view or viewMode."); - } - - /** - * 遷移先のViewとViewModelを準備 - * Prepare the destination View and ViewModel - */ - const ViewModelClass: any = packages.get(viewModelName) as unknown as ViewModel; - context.viewModel = (new ViewModelClass() as ViewModel); - - const ViewClass: any = packages.get(viewName) as unknown as View; - context.view = (new ViewClass() as View); - - /** - * ViewModelにViewをbindしてページを生成 - * Bind a View to a ViewModel to generate a page - */ - await context.viewModel.bind(context.view); - - /** - * rootの子要素を全て削除 - * Remove all child elements of root - */ - const root = context.root; - while (root.numChildren) { - root.removeChildAt(0); - } - - /** - * stageの一番背面にviewをセット - * Set the view at the very back of the stage - */ - root.addChildAt(context.view, 0); - - return context.view; -}; \ No newline at end of file diff --git a/src/application/content/Builder/service/ContentBuilderService.test.ts b/src/application/content/Builder/service/ContentBuilderService.test.ts index 233b607..3a7e4eb 100644 --- a/src/application/content/Builder/service/ContentBuilderService.test.ts +++ b/src/application/content/Builder/service/ContentBuilderService.test.ts @@ -2,11 +2,16 @@ import { execute } from "./ContentBuilderService"; import { ShapeContent } from "../../ShapeContent"; import { loaderInfoMap } from "../../../variable/LoaderInfoMap"; import { Shape, type LoaderInfo } from "@next2d/display"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach } from "vitest"; describe("ContentBuilderService Test", () => { - it("test case", () => + beforeEach(() => + { + loaderInfoMap.clear(); + }); + + it("should sync display object when all data is valid", () => { const displayObject = new ShapeContent(); @@ -18,9 +23,8 @@ describe("ContentBuilderService Test", () => const map: Map = new Map(); map.set(Shape.namespace, 1); - loaderInfoMap.clear(); loaderInfoMap.set(Shape.namespace, { - "data": { + "data": { "stage": { "width": 100, "height": 100, @@ -49,4 +53,72 @@ describe("ContentBuilderService Test", () => expect(state).toBe("sync"); expect(displayObject.characterId).toBe(1); }); + + it("should return early when loaderInfo is not found", () => + { + const displayObject = new ShapeContent(); + displayObject.$sync = vi.fn(); + + execute(displayObject); + + expect(displayObject.$sync).not.toHaveBeenCalled(); + expect(displayObject.characterId).toBe(-1); + }); + + it("should return early when loaderInfo.data is null", () => + { + const displayObject = new ShapeContent(); + displayObject.$sync = vi.fn(); + + loaderInfoMap.set(Shape.namespace, { + "data": null + } as unknown as LoaderInfo); + + execute(displayObject); + + expect(displayObject.$sync).not.toHaveBeenCalled(); + expect(displayObject.characterId).toBe(-1); + }); + + it("should return early when characterId is not found in symbols", () => + { + const displayObject = new ShapeContent(); + displayObject.$sync = vi.fn(); + + const map: Map = new Map(); + // Do not set the namespace in the map + loaderInfoMap.set(Shape.namespace, { + "data": { + "symbols": map, + "characters": [] + } + } as unknown as LoaderInfo); + + execute(displayObject); + + expect(displayObject.$sync).not.toHaveBeenCalled(); + expect(displayObject.characterId).toBe(-1); + }); + + it("should return early when character is not found", () => + { + const displayObject = new ShapeContent(); + displayObject.$sync = vi.fn(); + + const map: Map = new Map(); + map.set(Shape.namespace, 5); // characterId 5 doesn't exist in characters array + loaderInfoMap.set(Shape.namespace, { + "data": { + "symbols": map, + "characters": [ + { "extends": Shape.namespace } + ] + } + } as unknown as LoaderInfo); + + execute(displayObject); + + expect(displayObject.$sync).not.toHaveBeenCalled(); + expect(displayObject.characterId).toBe(-1); + }); }); \ No newline at end of file diff --git a/src/application/content/MovieClipContent.ts b/src/application/content/MovieClipContent.ts index 70bee1c..d977f08 100644 --- a/src/application/content/MovieClipContent.ts +++ b/src/application/content/MovieClipContent.ts @@ -17,21 +17,6 @@ export class MovieClipContent extends MovieClip constructor () { super(); - contentBuilderService(this); - - // initial processing - this.initialize(); } - - /** - * @description constructorが起動した後にコールされます。 - * Called after the constructor is invoked. - * - * @return {void} - * @method - * @abstract - */ - // eslint-disable-next-line no-empty-function - initialize (): void {} } \ No newline at end of file diff --git a/src/application/content/ShapeContent.ts b/src/application/content/ShapeContent.ts index a8753e2..40fcc8b 100644 --- a/src/application/content/ShapeContent.ts +++ b/src/application/content/ShapeContent.ts @@ -17,21 +17,6 @@ export class ShapeContent extends Shape constructor () { super(); - contentBuilderService(this); - - // initial processing - this.initialize(); } - - /** - * @description constructorが起動した後にコールされます。 - * Called after the constructor is invoked. - * - * @return {void} - * @method - * @abstract - */ - // eslint-disable-next-line no-empty-function - initialize (): void {} } \ No newline at end of file diff --git a/src/application/content/TextFieldContent.ts b/src/application/content/TextFieldContent.ts index c9af0ff..cd03080 100644 --- a/src/application/content/TextFieldContent.ts +++ b/src/application/content/TextFieldContent.ts @@ -17,21 +17,6 @@ export class TextFieldContent extends TextField constructor () { super(); - contentBuilderService(this); - - // initial processing - this.initialize(); } - - /** - * @description constructorが起動した後にコールされます。 - * Called after the constructor is invoked. - * - * @return {void} - * @method - * @abstract - */ - // eslint-disable-next-line no-empty-function - initialize (): void {} } \ No newline at end of file diff --git a/src/application/content/VideoContent.ts b/src/application/content/VideoContent.ts index 3c4ebc1..b35031e 100644 --- a/src/application/content/VideoContent.ts +++ b/src/application/content/VideoContent.ts @@ -17,21 +17,6 @@ export class VideoContent extends Video constructor () { super(); - contentBuilderService(this); - - // initial processing - this.initialize(); } - - /** - * @description constructorが起動した後にコールされます。 - * Called after the constructor is invoked. - * - * @return {void} - * @method - * @abstract - */ - // eslint-disable-next-line no-empty-function - initialize (): void {} } \ No newline at end of file diff --git a/src/application/Application/service/ApplicationQueryStringParserService.test.ts b/src/application/service/QueryStringParserService.test.ts similarity index 83% rename from src/application/Application/service/ApplicationQueryStringParserService.test.ts rename to src/application/service/QueryStringParserService.test.ts index 57412b7..1fb656e 100644 --- a/src/application/Application/service/ApplicationQueryStringParserService.test.ts +++ b/src/application/service/QueryStringParserService.test.ts @@ -1,7 +1,7 @@ -import type { IQueryObject } from "../../../interface/IQueryObject"; -import { execute } from "./ApplicationQueryStringParserService"; -import { query } from "../../variable/Query"; -import { $setConfig } from "../../variable/Config"; +import type { IQueryObject } from "../../interface/IQueryObject"; +import { execute } from "./QueryStringParserService"; +import { query } from "../variable/Query"; +import { $setConfig } from "../variable/Config"; import { describe, expect, it, vi } from "vitest"; Object.defineProperty(window, "location", { @@ -11,12 +11,12 @@ Object.defineProperty(window, "location", { }) }); -describe("ApplicationQueryStringParserService", () => +describe("QueryStringParserService", () => { it("parse query test case1", () => { query.clear(); - query.set("test", 123); + query.set("test", "123"); expect(query.size).toBe(1); const object: IQueryObject = execute(); @@ -48,9 +48,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute(""); - expect(query.size).toBe(2); - expect(query.get("q")).toBe("abc"); - expect(query.get("sample")).toBe("1"); + expect(query.size).toBe(0); + expect(query.has("q")).toBe(false); + expect(query.has("sample")).toBe(false); expect(object.name).toBe("top"); expect(object.queryString).toBe("?q=abc&sample=1"); }); @@ -85,9 +85,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute(""); - expect(query.size).toBe(2); - expect(query.get("q")).toBe("xyz"); - expect(query.get("sample")).toBe("0"); + expect(query.size).toBe(0); + expect(query.has("q")).toBe(false); + expect(query.has("sample")).toBe(false); expect(object.name).toBe("top"); expect(object.queryString).toBe("?q=xyz&sample=0"); }); @@ -124,9 +124,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute(""); - expect(query.size).toBe(2); - expect(query.get("q")).toBe("xyz"); - expect(query.get("sample")).toBe("0"); + expect(query.size).toBe(0); + expect(query.has("q")).toBe(false); + expect(query.has("sample")).toBe(false); expect(object.name).toBe("quest/list"); expect(object.queryString).toBe("?q=xyz&sample=0"); }); @@ -163,9 +163,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute(""); - expect(query.size).toBe(2); - expect(query.get("q")).toBe("xyz"); - expect(query.get("sample")).toBe("0"); + expect(query.size).toBe(0); + expect(query.has("q")).toBe(false); + expect(query.has("sample")).toBe(false); expect(object.name).toBe("top"); expect(object.queryString).toBe("?q=xyz&sample=0"); }); @@ -203,9 +203,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute(""); - expect(query.size).toBe(2); - expect(query.get("q")).toBe("xyz"); - expect(query.get("sample")).toBe("0"); + expect(query.size).toBe(0); + expect(query.has("q")).toBe(false); + expect(query.has("sample")).toBe(false); expect(object.name).toBe("quest/detail"); expect(object.queryString).toBe("?q=xyz&sample=0"); }); @@ -218,9 +218,9 @@ describe("ApplicationQueryStringParserService", () => const object: IQueryObject = execute("page/test?abc=123&xyz=999"); - expect(query.size).toBe(2); - expect(query.get("abc")).toBe("123"); - expect(query.get("xyz")).toBe("999"); + expect(query.size).toBe(0); + expect(query.has("abc")).toBe(false); + expect(query.has("xyz")).toBe(false); expect(object.name).toBe("page/test"); expect(object.queryString).toBe("?abc=123&xyz=999"); }); @@ -248,4 +248,4 @@ describe("ApplicationQueryStringParserService", () => expect(query.size).toBe(0); expect(object.name).toBe("top"); }); -}); \ No newline at end of file +}); diff --git a/src/application/Application/service/ApplicationQueryStringParserService.ts b/src/application/service/QueryStringParserService.ts similarity index 65% rename from src/application/Application/service/ApplicationQueryStringParserService.ts rename to src/application/service/QueryStringParserService.ts index 265ae6e..2a5f54a 100644 --- a/src/application/Application/service/ApplicationQueryStringParserService.ts +++ b/src/application/service/QueryStringParserService.ts @@ -1,6 +1,6 @@ -import type { IQueryObject } from "../../../interface/IQueryObject"; -import { $getConfig } from "../../variable/Config"; -import { query } from "../../variable/Query"; +import type { IQueryObject } from "../../interface/IQueryObject"; +import { $getConfig } from "../variable/Config"; +import { query } from "../variable/Query"; /** * @description 指定されたQueryStringか、URLのQueryStringをquery mapに登録 @@ -17,9 +17,7 @@ export const execute = (name: string = ""): IQueryObject => * 前のシーンのクエリデータを初期化 * Initialize query data from previous scene */ - if (query.size) { - query.clear(); - } + query.clear(); /** * QueryStringがあれば分解 @@ -28,11 +26,6 @@ export const execute = (name: string = ""): IQueryObject => let queryString = ""; if (!name && location.search) { queryString = location.search; - const parameters = queryString.slice(1).split("&"); - for (let idx = 0; idx < parameters.length; ++idx) { - const pair = parameters[idx].split("="); - query.set(pair[0], pair[1]); - } } const config = $getConfig(); @@ -61,21 +54,13 @@ export const execute = (name: string = ""): IQueryObject => * 任意で設定したQueryStringを分解 * Decompose an arbitrarily set QueryString */ - if (name.indexOf("?") > -1) { - - const names = name.split("?"); - - name = names[0]; - queryString = `?${names[1]}`; - - const parameters = names[1].split("&"); - for (let idx = 0; idx < parameters.length; ++idx) { - const pair = parameters[idx].split("="); - query.set(pair[0], pair[1]); - } + const questionIdx = name.indexOf("?"); + if (questionIdx > -1) { + queryString = name.slice(questionIdx); + name = name.slice(0, questionIdx); } - if (name.slice(0, 1) === ".") { + if (name.charAt(0) === ".") { name = name.split("/").slice(1).join("/") || defaultTop; } @@ -87,4 +72,4 @@ export const execute = (name: string = ""): IQueryObject => "name": name, "queryString": queryString }; -}; \ No newline at end of file +}; diff --git a/src/application/Config/service/ConfigParserRequestsPropertyService.test.ts b/src/application/service/RoutingRequestsParserService.test.ts similarity index 93% rename from src/application/Config/service/ConfigParserRequestsPropertyService.test.ts rename to src/application/service/RoutingRequestsParserService.test.ts index 55d172f..c0b96ee 100644 --- a/src/application/Config/service/ConfigParserRequestsPropertyService.test.ts +++ b/src/application/service/RoutingRequestsParserService.test.ts @@ -1,9 +1,9 @@ -import { execute } from "./ConfigParserRequestsPropertyService"; -import { $setConfig } from "../../variable/Config"; -import type { IConfig } from "../../../interface/IConfig"; +import { execute } from "./RoutingRequestsParserService"; +import { $setConfig } from "../variable/Config"; +import type { IConfig } from "../../interface/IConfig"; import { describe, expect, it } from "vitest"; -describe("ConfigParserRequestsPropertyService Test", () => +describe("RoutingRequestsParserService Test", () => { it("request parse no match test case1", () => { @@ -137,4 +137,4 @@ describe("ConfigParserRequestsPropertyService Test", () => expect(object2.type).toBe("json"); expect(object2.name).toBe("TopText"); }); -}); \ No newline at end of file +}); diff --git a/src/application/Config/service/ConfigParserRequestsPropertyService.ts b/src/application/service/RoutingRequestsParserService.ts similarity index 75% rename from src/application/Config/service/ConfigParserRequestsPropertyService.ts rename to src/application/service/RoutingRequestsParserService.ts index aa42ffc..33c4ed0 100644 --- a/src/application/Config/service/ConfigParserRequestsPropertyService.ts +++ b/src/application/service/RoutingRequestsParserService.ts @@ -1,6 +1,6 @@ -import type { IRequest } from "../../../interface/IRequest"; -import type { IRouting } from "../../../interface/IRouting"; -import { $getConfig } from "../../../application/variable/Config"; +import type { IRequest } from "../../interface/IRequest"; +import type { IRouting } from "../../interface/IRouting"; +import { $getConfig } from "../variable/Config"; /** * @description routing.jsonに設定されたrequestsを返却します。 @@ -15,21 +15,20 @@ import { $getConfig } from "../../../application/variable/Config"; */ export const execute = (name: string): IRequest[] => { - const requests: IRequest[] = []; - const config = $getConfig(); if (!config || !config.routing) { - return requests; + return []; } const routing: IRouting = config.routing[name]; if (!routing || !routing.requests) { - return requests; + return []; } - for (let idx: number = 0; idx < routing.requests.length; idx++) { + const requests: IRequest[] = []; + for (let idx = 0; idx < routing.requests.length; ++idx) { - const request: IRequest = routing.requests[idx]; + const request = routing.requests[idx]; if (request.type !== "cluster") { requests.push(request); @@ -49,4 +48,4 @@ export const execute = (name: string): IRequest[] => } return requests; -}; \ No newline at end of file +}; diff --git a/src/application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/usecase/ApplicationGotoViewUseCase.test.ts new file mode 100644 index 0000000..41c8fbc --- /dev/null +++ b/src/application/usecase/ApplicationGotoViewUseCase.test.ts @@ -0,0 +1,246 @@ +import { execute } from "./ApplicationGotoViewUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { MovieClip } from "@next2d/display"; +import { Context } from "../Context"; +import { $setConfig } from "../variable/Config"; +import { $setContext } from "../variable/Context"; +import { response } from "../../infrastructure/variable/Response"; + +vi.mock("../../domain/service/ScreenOverlayService", () => ({ + ScreenOverlayService: { + add: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn() + } +})); + +vi.mock("../../domain/service/LoadingService", () => ({ + LoadingService: { + start: vi.fn().mockResolvedValue(undefined), + end: vi.fn().mockResolvedValue(undefined), + getInstance: vi.fn().mockReturnValue(null) + } +})); + +vi.mock("../../infrastructure/usecase/ResponseRemoveVariableUseCase", () => ({ + execute: vi.fn() +})); + +vi.mock("../service/QueryStringParserService", () => ({ + execute: vi.fn() +})); + +vi.mock("../../infrastructure/usecase/RequestUseCase", () => ({ + execute: vi.fn().mockResolvedValue([]), + getRequests: vi.fn().mockReturnValue([]) +})); + +vi.mock("./ExecuteCallbackUseCase", () => ({ + execute: vi.fn().mockResolvedValue(undefined) +})); + +vi.mock("../../domain/service/ViewBinderService", () => ({ + ViewBinderService: { + bind: vi.fn().mockResolvedValue({ + onEnter: vi.fn().mockResolvedValue(undefined) + }), + unbind: vi.fn().mockResolvedValue(undefined) + } +})); + +describe("ApplicationGotoViewUseCase Test", () => +{ + let mockApplication: any; + let mockContext: Context; + let root: MovieClip; + + beforeEach(() => + { + response.clear(); + + mockApplication = { + currentName: "", + popstate: false + }; + + root = new MovieClip(); + mockContext = new Context(root); + + $setContext(mockContext); + $setConfig({ + platform: "web", + spa: false, + stage: { + width: 800, + height: 600, + fps: 60 + } + }); + + global.history = { + pushState: vi.fn() + } as any; + + global.location = { + origin: "http://localhost" + } as any; + + vi.clearAllMocks(); + }); + + it("execute test case1: basic navigation without loading", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + const { ViewBinderService } = await import("../../domain/service/ViewBinderService"); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "home", + queryString: "" + }); + + vi.mocked(requestUseCase).mockResolvedValue([]); + + await execute(mockApplication, "home"); + + expect(mockApplication.currentName).toBe("home"); + expect(ViewBinderService.bind).toHaveBeenCalledWith(mockContext, "home"); + }); + + it("execute test case2: navigation with response data", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "dashboard", + queryString: "" + }); + + const mockResponses = [ + { name: "user", response: { id: 1, name: "Test User" } }, + { name: "settings", response: { theme: "dark" } } + ]; + + vi.mocked(requestUseCase).mockResolvedValue(mockResponses); + + await execute(mockApplication, "dashboard"); + + expect(response.get("user")).toEqual({ id: 1, name: "Test User" }); + expect(response.get("settings")).toEqual({ theme: "dark" }); + expect(mockApplication.currentName).toBe("dashboard"); + }); + + it("execute test case3: handle response without name", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "test", + queryString: "" + }); + + const mockResponses = [ + { name: "", response: { data: "should be skipped" } }, + { name: "valid", response: { data: "should be set" } } + ]; + + vi.mocked(requestUseCase).mockResolvedValue(mockResponses); + + await execute(mockApplication, "test"); + + expect(response.has("")).toBe(false); + expect(response.get("valid")).toEqual({ data: "should be set" }); + }); + + it("execute test case4: navigation with loading enabled", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + const { LoadingService } = await import("../../domain/service/LoadingService"); + const { ScreenOverlayService } = await import("../../domain/service/ScreenOverlayService"); + + $setConfig({ + platform: "web", + spa: false, + stage: { + width: 800, + height: 600, + fps: 60 + }, + loading: { + callback: async () => null + } + }); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "home", + queryString: "" + }); + vi.mocked(requestUseCase).mockResolvedValue([]); + + await execute(mockApplication, "home"); + + expect(ScreenOverlayService.add).toHaveBeenCalled(); + expect(LoadingService.start).toHaveBeenCalled(); + expect(LoadingService.end).toHaveBeenCalled(); + expect(ScreenOverlayService.dispose).toHaveBeenCalled(); + }); + + it("execute test case5: response with callback should execute callback", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + const { execute: executeCallbackUseCase } = await import("./ExecuteCallbackUseCase"); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "page", + queryString: "" + }); + + const mockCallback = vi.fn(); + const mockResponses = [ + { name: "data", response: { value: 123 }, callback: mockCallback } + ]; + vi.mocked(requestUseCase).mockResolvedValue(mockResponses); + + await execute(mockApplication, "page"); + + expect(executeCallbackUseCase).toHaveBeenCalledWith(mockCallback, { value: 123 }); + }); + + it("execute test case6: gotoView callback should be executed when view is returned", async () => + { + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + const { ViewBinderService } = await import("../../domain/service/ViewBinderService"); + const { execute: executeCallbackUseCase } = await import("./ExecuteCallbackUseCase"); + + const mockGotoViewCallback = vi.fn(); + $setConfig({ + platform: "web", + spa: false, + stage: { + width: 800, + height: 600, + fps: 60 + }, + gotoView: { + callback: mockGotoViewCallback + } + }); + + vi.mocked(queryStringParserService).mockReturnValue({ + name: "withCallback", + queryString: "" + }); + vi.mocked(requestUseCase).mockResolvedValue([]); + + const mockView = { name: "MockView", onEnter: vi.fn().mockResolvedValue(undefined) }; + vi.mocked(ViewBinderService.bind).mockResolvedValue(mockView as any); + + await execute(mockApplication, "withCallback"); + + expect(executeCallbackUseCase).toHaveBeenCalledWith(mockGotoViewCallback, mockView); + }); +}); diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts b/src/application/usecase/ApplicationGotoViewUseCase.ts similarity index 51% rename from src/application/Application/usecase/ApplicationGotoViewUseCase.ts rename to src/application/usecase/ApplicationGotoViewUseCase.ts index cf1a7d3..f05073f 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.ts @@ -1,15 +1,17 @@ -import type { Application } from "../../Application"; -import { $getConfig } from "../../variable/Config"; -import { $getContext } from "../../variable/Context"; -import { response } from "../../../infrastructure/Response/variable/Response"; -import { execute as addScreenCaptureService } from "../../../domain/screen/Capture/service/AddScreenCaptureService"; -import { execute as disposeCaptureService } from "../../../domain/screen/Capture/service/DisposeCaptureService"; -import { execute as loadingStartService } from "../../../domain/loading/Loading/service/LoadingStartService"; -import { execute as loadingEndService } from "../../../domain/loading/Loading/service/LoadingEndService"; -import { execute as responseRemoveVariableUseCase } from "../../../infrastructure/Response/usecase/ResponseRemoveVariableUseCase"; -import { execute as applicationQueryStringParserService } from "../service/ApplicationQueryStringParserService"; -import { execute as requestUseCase } from "../../../infrastructure/Request/usecase/RequestUseCase"; -import { execute as callbackService } from "../../../domain/callback/service/CallbackService"; +import type { Application } from "../Application"; +import { $getConfig } from "../variable/Config"; +import { $getContext } from "../variable/Context"; +import { response } from "../../infrastructure/variable/Response"; +import { execute as queryStringParserService } from "../service/QueryStringParserService"; +import { + execute as requestUseCase, + getRequests +} from "../../infrastructure/usecase/RequestUseCase"; +import { execute as executeCallbackUseCase } from "./ExecuteCallbackUseCase"; +import { execute as responseRemoveVariableUseCase } from "../../infrastructure/usecase/ResponseRemoveVariableUseCase"; +import { ViewBinderService } from "../../domain/service/ViewBinderService"; +import { LoadingService } from "../../domain/service/LoadingService"; +import { ScreenOverlayService } from "../../domain/service/ScreenOverlayService"; /** * @description 指定されたパス、もしくはURLのクラスを起動 @@ -21,41 +23,40 @@ import { execute as callbackService } from "../../../domain/callback/service/Cal * @method * @protected */ -export const execute = async (application: Application, name: string = ""): Promise => -{ +export const execute = async ( + application: Application, + name: string = "" +): Promise => { + const config = $getConfig(); - if (config.loading) { + const hasLoading = !!config.loading; + + if (hasLoading) { /** * 現時点の描画をキャプチャーして表示 * Capture and display the current drawing */ - await addScreenCaptureService(); + await ScreenOverlayService.add(); /** * ローディング表示を起動 * Launch loading display */ - await loadingStartService(); + await LoadingService.start(); } - /** - * 現在の画面のViewとViewModelをunbind - * Unbind the View and ViewModel of the current screen - */ - const context = $getContext(); - await context.unbind(); - /** * 前の画面で取得したレスポンスデータを初期化 * Initialize the response data obtained on the previous screen */ - responseRemoveVariableUseCase(application.currentName); + const previousRequests = getRequests(application.currentName); + responseRemoveVariableUseCase(previousRequests); /** * 指定されたパス、もしくはURLからアクセス先を算出 * Calculate the access point from the specified path or URL */ - const queryObject = applicationQueryStringParserService(name); + const queryObject = queryStringParserService(name); /** * 現在の画面名を更新 @@ -81,46 +82,66 @@ export const execute = async (application: Application, name: string = ""): Prom * Execute request processing set by routing.json */ const responses = await requestUseCase(application.currentName); + // await new Promise((resolve) => setTimeout(resolve, 3000)); /** - * レスポンス情報をマップに登録 - * Response information is registered on the map + * レスポンス情報をマップに登録し、コールバックを実行 + * Register response information on the map and execute callbacks */ for (let idx = 0; idx < responses.length; ++idx) { const object = responses[idx]; - if (!object.name) { - continue; + if (object.name) { + response.set(object.name, object.response); } - response.set(object.name, object.response); - } - - if (config.loading) { /** - * ローディング表示を終了 - * End loading display + * リクエストごとのコールバック処理を実行 + * Execute callback for each request */ - await loadingEndService(); - - /** - * 前の画面のキャプチャーを終了 - * End previous screen capture - */ - disposeCaptureService(); + if (object.callback) { + await executeCallbackUseCase(object.callback, object.response); + } } + /** + * 現在の画面のViewとViewModelをunbind + * Unbind the View and ViewModel of the current screen + */ + const context = $getContext(); + await ViewBinderService.unbind(context); + /** * ViewとViewModelを起動 * Start View and ViewModel */ - const view = await context.bind(application.currentName); + const view = await ViewBinderService.bind(context, application.currentName); /** * コールバック設定があれば実行 * Execute callback settings if any. */ if (view && config.gotoView) { - await callbackService(config.gotoView.callback, view); + await executeCallbackUseCase(config.gotoView.callback, view); } -}; \ No newline at end of file + + if (hasLoading) { + /** + * ローディング表示を終了 + * End loading display + */ + await LoadingService.end(); + + /** + * 前の画面のキャプチャーを終了 + * End previous screen capture + */ + ScreenOverlayService.dispose(); + } + + /** + * 画面表示時の処理を実行 + * Execute processing when the screen is displayed + */ + await view.onEnter(); +}; diff --git a/src/application/usecase/ApplicationInitializeUseCase.test.ts b/src/application/usecase/ApplicationInitializeUseCase.test.ts new file mode 100644 index 0000000..c27fbd6 --- /dev/null +++ b/src/application/usecase/ApplicationInitializeUseCase.test.ts @@ -0,0 +1,113 @@ +import type { IConfig } from "../../interface/IConfig"; +import type { IPackages } from "../../interface/IPackages"; +import { execute } from "./ApplicationInitializeUseCase"; +import { Application } from "../Application"; +import { View } from "../../view/View"; +import { $getConfig } from "../variable/Config"; +import { packages } from "../variable/Packages"; +import { describe, expect, it, vi } from "vitest"; + +describe("ApplicationInitializeUseCase", () => +{ + it("test case", () => + { + let state = ""; + window.addEventListener = vi.fn((name) => + { + state = name; + }); + + const app = new Application(); + const config: IConfig = { + "platform": "web", + "stage": { + "width": 640, + "height": 480, + "fps": 60 + }, + "spa": true + }; + + const buildPackages: IPackages = [[ + "view", View + ]]; + + expect(state).toBe(""); + expect(packages.size).toBe(0); + expect($getConfig()).toBe(undefined); + + execute(app, config, buildPackages); + + expect($getConfig()).toBe(config); + expect(packages.size).toBe(1); + expect(packages.get("view")).toBe(View); + expect(state).toBe("popstate"); + }); + + it("test case", () => + { + let state = ""; + window.addEventListener = vi.fn((name) => + { + state = name; + }); + + const app = new Application(); + const config: IConfig = { + "platform": "web", + "stage": { + "width": 640, + "height": 480, + "fps": 60 + }, + "spa": false + }; + + const buildPackages: IPackages = [[ + "view", View + ]]; + + expect(state).toBe(""); + execute(app, config, buildPackages); + expect(state).toBe(""); + }); + + it("popstate handler sets popstate flag and triggers gotoView", async () => + { + let popstateCallback: (() => Promise) | null = null; + window.addEventListener = vi.fn((name, callback) => + { + if (name === "popstate") { + popstateCallback = callback as () => Promise; + } + }); + + const app = new Application(); + app.gotoView = vi.fn().mockResolvedValue(undefined); + + const config: IConfig = { + "platform": "web", + "stage": { + "width": 640, + "height": 480, + "fps": 60 + }, + "spa": true + }; + + const buildPackages: IPackages = [[ + "view", View + ]]; + + execute(app, config, buildPackages); + + expect(popstateCallback).not.toBeNull(); + expect(app.popstate).toBe(false); + + // Trigger the popstate event handler + await popstateCallback!(); + + expect(app.popstate).toBe(true); + expect(app.gotoView).toHaveBeenCalled(); + }); +}); diff --git a/src/application/Application/service/ApplicationInitializeService.ts b/src/application/usecase/ApplicationInitializeUseCase.ts similarity index 63% rename from src/application/Application/service/ApplicationInitializeService.ts rename to src/application/usecase/ApplicationInitializeUseCase.ts index 7d34b81..6a8b83c 100644 --- a/src/application/Application/service/ApplicationInitializeService.ts +++ b/src/application/usecase/ApplicationInitializeUseCase.ts @@ -1,14 +1,9 @@ -import type { IConfig } from "../../../interface/IConfig"; -import type { IPackages } from "../../../interface/IPackages"; -import type { Application } from "../../Application"; -import { $setConfig } from "../../variable/Config"; -import { $setPackages } from "../../variable/Packages"; - -/** - * @type {Promise} - * @private - */ -let $popstateQueue: Promise = Promise.resolve(); +import type { IConfig } from "../../interface/IConfig"; +import type { IPackages } from "../../interface/IPackages"; +import type { Application } from "../Application"; +import { $setConfig } from "../variable/Config"; +import { $setPackages } from "../variable/Packages"; +import { popstateQueue, setPopstateQueue } from "../variable/PopstateQueue"; /** * @description アプリケーションの初期化処理を実行します @@ -38,9 +33,9 @@ export const execute = ( window.addEventListener("popstate", async (): Promise => { application.popstate = true; - $popstateQueue = $popstateQueue.then(() => application.gotoView()); + setPopstateQueue(popstateQueue.then(() => application.gotoView())); }); } return application; -}; \ No newline at end of file +}; diff --git a/src/application/Context/service/ContextRunService.test.ts b/src/application/usecase/ContextRunUseCase.test.ts similarity index 75% rename from src/application/Context/service/ContextRunService.test.ts rename to src/application/usecase/ContextRunUseCase.test.ts index 3b3169d..e4cf1cc 100644 --- a/src/application/Context/service/ContextRunService.test.ts +++ b/src/application/usecase/ContextRunUseCase.test.ts @@ -1,5 +1,5 @@ -import { execute } from "./ContextRunService"; -import { $getContext } from "../../../application/variable/Context"; +import { execute } from "./ContextRunUseCase"; +import { $getContext } from "../variable/Context"; import { describe, expect, it, vi } from "vitest"; import { MovieClip } from "@next2d/display"; @@ -13,7 +13,7 @@ Object.defineProperty(window, "next2d", { }) }); -describe("ContextRunService Test", () => +describe("ContextRunUseCase Test", () => { it("execute test case", async () => { @@ -30,6 +30,6 @@ describe("ContextRunService Test", () => await execute(config); const context = $getContext(); expect(context).not.toBeNull(); - expect(context.root.instanceId).toBe(root.instanceId) + expect(context.root.instanceId).toBe(root.instanceId); }); -}); \ No newline at end of file +}); diff --git a/src/application/Context/service/ContextRunService.ts b/src/application/usecase/ContextRunUseCase.ts similarity index 74% rename from src/application/Context/service/ContextRunService.ts rename to src/application/usecase/ContextRunUseCase.ts index 9bccfa1..504b39b 100644 --- a/src/application/Context/service/ContextRunService.ts +++ b/src/application/usecase/ContextRunUseCase.ts @@ -1,6 +1,6 @@ -import type { IConfig } from "../../../interface/IConfig"; -import { Context } from "../../Context"; -import { $setContext } from "../../variable/Context"; +import type { IConfig } from "../../interface/IConfig"; +import { Context } from "../Context"; +import { $setContext } from "../variable/Context"; /** * @description コンテキストを起動します。 @@ -21,4 +21,4 @@ export const execute = async (config: IConfig): Promise => ); $setContext(new Context(root)); -}; \ No newline at end of file +}; diff --git a/src/domain/callback/service/CallbackService.test.ts b/src/application/usecase/ExecuteCallbackUseCase.test.ts similarity index 60% rename from src/domain/callback/service/CallbackService.test.ts rename to src/application/usecase/ExecuteCallbackUseCase.test.ts index a845341..733936e 100644 --- a/src/domain/callback/service/CallbackService.test.ts +++ b/src/application/usecase/ExecuteCallbackUseCase.test.ts @@ -1,24 +1,23 @@ -import { execute } from "./CallbackService"; -import { packages } from "../../../application/variable/Packages"; +import { execute } from "./ExecuteCallbackUseCase"; +import { packages } from "../variable/Packages"; import { describe, expect, it } from "vitest"; -describe("CallbackService Test", () => +describe("ExecuteCallbackUseCase Test", () => { - it("execute test case1", async () => + it("should return undefined when no callback provided", async () => { - const result = await execute() + const result = await execute(); expect(result).toBe(undefined); }); - it("execute single test", async () => + it("should execute single callback", async () => { - // mock let state = "none"; const SingleTest = class SingleTest { - execute (value: any): any + execute (value: unknown): void { - state = value; + state = value as string; } }; @@ -30,13 +29,12 @@ describe("CallbackService Test", () => expect(state).toBe("single test"); }); - it("execute multiple test", async () => + it("should execute multiple callbacks", async () => { - // mock let state1 = "none"; const MultipleTestCase1 = class MultipleTest { - execute (value: any): any + execute (value: unknown): void { state1 = `${value}_1`; } @@ -45,7 +43,7 @@ describe("CallbackService Test", () => let state2 = "none"; const MultipleTestCase2 = class MultipleTest { - execute (value: any): any + execute (value: unknown): void { state2 = `${value}_2`; } @@ -58,9 +56,16 @@ describe("CallbackService Test", () => expect(state1).toBe("none"); expect(state2).toBe("none"); - await execute(["multiple.test.case1", "multiple.test.case2"], "multiple test") + await execute(["multiple.test.case1", "multiple.test.case2"], "multiple test"); expect(state1).toBe("multiple test_1"); expect(state2).toBe("multiple test_2"); }); -}); \ No newline at end of file + + it("should skip non-existent callbacks", async () => + { + packages.clear(); + const result = await execute("NonExistentCallback", "test"); + expect(result).toBe(undefined); + }); +}); diff --git a/src/domain/callback/service/CallbackService.ts b/src/application/usecase/ExecuteCallbackUseCase.ts similarity index 53% rename from src/domain/callback/service/CallbackService.ts rename to src/application/usecase/ExecuteCallbackUseCase.ts index 4cafd77..a4d0b80 100644 --- a/src/domain/callback/service/CallbackService.ts +++ b/src/application/usecase/ExecuteCallbackUseCase.ts @@ -1,22 +1,24 @@ -import { packages } from "../../../application/variable/Packages"; +import type { ICallback } from "../../interface/ICallback"; +import type { Constructor } from "../../interface/IPackages"; +import { packages } from "../variable/Packages"; /** - * @description configで指定されたクラスのexecute関数を実行 - * Execute function of the class specified in config. + * @description configで指定されたクラスのexecute関数を実行するユースケース + * UseCase to execute the function of the class specified in config. * * @param {string | string[]} [callback=""] - * @param {*} [value=null] - * @return {Promise} + * @param {unknown} [value=null] + * @return {Promise} * @method * @public */ export const execute = async ( callback: string | string[] = "", - value: any = null -): Promise[]|void> => { + value: unknown = null +): Promise => { if (!callback) { - return ; + return; } const callbacks: string[] = typeof callback === "string" @@ -30,7 +32,7 @@ export const execute = async ( continue; } - const CallbackClass: any = packages.get(name); + const CallbackClass = packages.get(name) as Constructor; await new CallbackClass().execute(value); } -}; \ No newline at end of file +}; diff --git a/src/application/variable/Cache.ts b/src/application/variable/Cache.ts index d2b89e7..9fecf5d 100644 --- a/src/application/variable/Cache.ts +++ b/src/application/variable/Cache.ts @@ -1,5 +1,5 @@ /** - * @type {Map} + * @type {Map} * @protected */ -export const cache: Map = new Map(); \ No newline at end of file +export const cache: Map = new Map(); \ No newline at end of file diff --git a/src/application/variable/Context.test.ts b/src/application/variable/Context.test.ts new file mode 100644 index 0000000..d295ac0 --- /dev/null +++ b/src/application/variable/Context.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vitest"; + +describe("Context variable Test", () => +{ + it("$getContext should throw error when context is not initialized", async () => + { + // Reset modules to get a fresh Context module state + vi.resetModules(); + const { $getContext } = await import("./Context"); + + expect(() => $getContext()).toThrow("Context is not initialized. Call run() first."); + }); + + it("$setContext should set the context and $getContext should return it", async () => + { + vi.resetModules(); + const { $getContext, $setContext } = await import("./Context"); + const { Context } = await import("../Context"); + const { MovieClip } = await import("@next2d/display"); + + const root = new MovieClip(); + const context = new Context(root); + + $setContext(context); + const result = $getContext(); + + expect(result).toBe(context); + expect(result.root).toBe(root); + }); + + it("$getContext should return the same context that was set", async () => + { + vi.resetModules(); + const { $getContext, $setContext } = await import("./Context"); + const { Context } = await import("../Context"); + const { MovieClip } = await import("@next2d/display"); + + const root1 = new MovieClip(); + const context1 = new Context(root1); + $setContext(context1); + expect($getContext()).toBe(context1); + + const root2 = new MovieClip(); + const context2 = new Context(root2); + $setContext(context2); + expect($getContext()).toBe(context2); + }); +}); diff --git a/src/application/variable/Context.ts b/src/application/variable/Context.ts index 5f76ca0..f197888 100644 --- a/src/application/variable/Context.ts +++ b/src/application/variable/Context.ts @@ -1,22 +1,26 @@ import type { Context } from "../Context"; /** - * @type {Context} - * @public + * @type {Context | null} + * @private */ -let $context: Context; +let $context: Context | null = null; /** * @description コンテキストを取得します * Get the context * * @return {Context} + * @throws {Error} コンテキストが初期化されていない場合 * @method * @protected */ export const $getContext = (): Context => { - return $context as NonNullable; + if (!$context) { + throw new Error("Context is not initialized. Call run() first."); + } + return $context; }; /** diff --git a/src/application/variable/Packages.ts b/src/application/variable/Packages.ts index f6055bb..7912ba5 100644 --- a/src/application/variable/Packages.ts +++ b/src/application/variable/Packages.ts @@ -1,16 +1,18 @@ +import type { Constructor, IPackages } from "../../interface/IPackages"; + /** - * @type {Map} + * @type {Map} * @protected */ -export let packages: Map = new Map(); +export let packages: Map = new Map(); /** - * @param {Array>} package_list + * @param {IPackages} package_list * @return {void} * @method * @protected */ -export const $setPackages = (package_list: any): void => +export const $setPackages = (package_list: IPackages): void => { packages = new Map(package_list); }; \ No newline at end of file diff --git a/src/application/variable/PopstateQueue.test.ts b/src/application/variable/PopstateQueue.test.ts new file mode 100644 index 0000000..88b5821 --- /dev/null +++ b/src/application/variable/PopstateQueue.test.ts @@ -0,0 +1,26 @@ +import { popstateQueue, setPopstateQueue } from "./PopstateQueue"; +import { describe, expect, it } from "vitest"; + +describe("PopstateQueue Test", () => +{ + it("popstateQueue should be a resolved Promise initially", async () => + { + const result = await popstateQueue; + expect(result).toBeUndefined(); + }); + + it("setPopstateQueue should update the popstateQueue", async () => + { + let executed = false; + const newQueue = Promise.resolve().then(() => + { + executed = true; + }); + + setPopstateQueue(newQueue); + + await popstateQueue; + + expect(executed).toBe(true); + }); +}); diff --git a/src/application/variable/PopstateQueue.ts b/src/application/variable/PopstateQueue.ts new file mode 100644 index 0000000..903266f --- /dev/null +++ b/src/application/variable/PopstateQueue.ts @@ -0,0 +1,22 @@ +/** + * @description popstateイベントのキュー + * Queue for popstate events + * + * @type {Promise} + * @protected + */ +export let popstateQueue: Promise = Promise.resolve(); + +/** + * @description popstateキューを設定 + * Set popstate queue + * + * @param {Promise} queue + * @return {void} + * @method + * @protected + */ +export const setPopstateQueue = (queue: Promise): void => +{ + popstateQueue = queue; +}; diff --git a/src/application/variable/Query.ts b/src/application/variable/Query.ts index 3046049..7358b26 100644 --- a/src/application/variable/Query.ts +++ b/src/application/variable/Query.ts @@ -1,5 +1,5 @@ /** - * @type {Map} + * @type {Map} * @protected */ -export const query: Map = new Map(); \ No newline at end of file +export const query: Map = new Map(); \ No newline at end of file diff --git a/src/domain/entity/DefaultLoader.test.ts b/src/domain/entity/DefaultLoader.test.ts new file mode 100644 index 0000000..8edf589 --- /dev/null +++ b/src/domain/entity/DefaultLoader.test.ts @@ -0,0 +1,294 @@ +import { DefaultLoader } from "./DefaultLoader"; +import { MovieClip, Shape } from "@next2d/display"; +import { Context } from "../../application/Context"; +import { $setContext } from "../../application/variable/Context"; +import { $setConfig } from "../../application/variable/Config"; +import { describe, expect, it, beforeEach, vi } from "vitest"; + +describe("DefaultLoader Test", () => +{ + beforeEach(() => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + }); + + describe("constructor", () => + { + it("should create DefaultLoader with sprite", () => + { + const loader = new DefaultLoader(); + + expect(loader.sprite).toBeDefined(); + expect(loader.sprite.numChildren).toBe(3); + }); + }); + + describe("initialize", () => + { + it("should add 3 shapes to sprite", () => + { + const loader = new DefaultLoader(); + + expect(loader.sprite.numChildren).toBe(3); + for (let i = 0; i < 3; i++) { + const child = loader.sprite.getChildAt(i); + expect(child).toBeInstanceOf(Shape); + } + }); + }); + + describe("start", () => + { + it("should return early when context root getter returns falsy", () => + { + const mockContext = { + "root": null, + "view": null, + "viewModel": null + } as unknown as Context; + $setContext(mockContext); + + const loader = new DefaultLoader(); + + loader.start(); + }); + + it("should add sprite to root when root exists", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + + expect(root.numChildren).toBe(0); + loader.start(); + expect(root.numChildren).toBe(1); + expect(root.getChildAt(0)).toBe(loader.sprite); + }); + + it("should set initial scale and alpha on shapes", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + loader.start(); + + for (let i = 0; i < 3; i++) { + const shape = loader.sprite.getChildAt(i); + if (shape) { + expect(shape.scaleX).toBe(0.1); + expect(shape.scaleY).toBe(0.1); + expect(shape.alpha).toBe(0); + } + } + }); + + it("should position sprite in center of stage", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + loader.start(); + + const config = { "stage": { "width": 800, "height": 600 } }; + const expectedX = (config.stage.width - loader.sprite.width) / 2; + const expectedY = (config.stage.height - loader.sprite.height) / 2; + + expect(loader.sprite.x).toBe(expectedX); + expect(loader.sprite.y).toBe(expectedY); + }); + + it("should draw circles on shapes", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + loader.start(); + + for (let i = 0; i < 3; i++) { + const shape = loader.sprite.getChildAt(i); + if (shape) { + expect(shape.graphics).toBeDefined(); + } + } + }); + + it("should setup tween jobs on shapes", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + loader.start(); + + for (let i = 0; i < 3; i++) { + const shape = loader.sprite.getChildAt(i); + if (shape) { + expect(shape.hasLocalVariable("expandJob")).toBe(true); + expect(shape.hasLocalVariable("reduceJob")).toBe(true); + } + } + }); + + it("should not redraw shapes if size matches", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + + loader.start(); + const firstShape = loader.sprite.getChildAt(0); + const width1 = firstShape?.width; + + loader.start(); + const width2 = firstShape?.width; + + expect(width1).toBe(width2); + }); + }); + + describe("end", () => + { + it("should return early when context root getter returns falsy", () => + { + const mockContext = { + "root": null, + "view": null, + "viewModel": null + } as unknown as Context; + $setContext(mockContext); + + const loader = new DefaultLoader(); + + loader.end(); + }); + + it("should remove sprite from root", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + + loader.start(); + expect(root.numChildren).toBe(1); + + loader.end(); + expect(root.numChildren).toBe(0); + }); + + it("should stop all tween jobs", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + + loader.start(); + + for (let i = 0; i < 3; i++) { + const shape = loader.sprite.getChildAt(i); + if (shape) { + expect(shape.hasLocalVariable("expandJob")).toBe(true); + expect(shape.hasLocalVariable("reduceJob")).toBe(true); + } + } + + loader.end(); + + expect(root.numChildren).toBe(0); + }); + + it("should handle shapes without jobs gracefully", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + root.addChild(loader.sprite); + + loader.end(); + + expect(root.numChildren).toBe(0); + }); + }); + + describe("sprite property", () => + { + it("should be readonly", () => + { + const loader = new DefaultLoader(); + const sprite = loader.sprite; + + expect(sprite).toBe(loader.sprite); + }); + }); + + describe("edge cases", () => + { + it("start should handle null shape gracefully when sprite children are removed", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + // Remove all children to trigger the null check + while (loader.sprite.numChildren > 0) { + loader.sprite.removeChildAt(0); + } + + // Should not throw + loader.start(); + }); + + it("end should handle null shape gracefully when sprite children are removed", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + root.addChild(loader.sprite); + + // Remove all children from sprite + while (loader.sprite.numChildren > 0) { + loader.sprite.removeChildAt(0); + } + + // Should not throw + loader.end(); + + expect(root.numChildren).toBe(0); + }); + + it("start should skip redrawing when shape width matches minSize", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + const loader = new DefaultLoader(); + + // First start - shapes are drawn + loader.start(); + + // Get the first shape's width after drawing + const shape0 = loader.sprite.getChildAt(0); + expect(shape0).toBeTruthy(); + + // Second start - should skip the redraw branch (line 175) + // because shape.width === minSize is already true + loader.start(); + }); + }); +}); diff --git a/src/domain/entity/DefaultLoader.ts b/src/domain/entity/DefaultLoader.ts new file mode 100644 index 0000000..4a00a82 --- /dev/null +++ b/src/domain/entity/DefaultLoader.ts @@ -0,0 +1,230 @@ +import type { Job } from "@next2d/ui"; +import { Sprite, Shape } from "@next2d/display"; +import { $getConfig } from "../../application/variable/Config"; +import { $getContext } from "../../application/variable/Context"; +import { Tween, Easing } from "@next2d/ui"; + +/** + * @description アニメーションの定数 + * Animation constants + */ +const ANIMATION_DURATION = 0.4; +const ANIMATION_DELAY_INTERVAL = 0.15; + +/** + * @description 拡大アニメーションのジョブを作成 + * Create expand animation job + * + * @param {Shape} shape + * @param {number} delay + * @return {Job} + */ +const createExpandJob = (shape: Shape, delay: number): Job => +{ + return Tween.add( + shape, + { "scaleX": 0.1, "scaleY": 0.1, "alpha": 0 }, + { "scaleX": 1, "scaleY": 1, "alpha": 1 }, + delay, + ANIMATION_DURATION, + Easing.inOutCubic + ); +}; + +/** + * @description 縮小アニメーションのジョブを作成 + * Create reduce animation job + * + * @param {Shape} shape + * @param {number} delay + * @return {Job} + */ +const createReduceJob = (shape: Shape, delay: number): Job => +{ + return Tween.add( + shape, + { "scaleX": 1, "scaleY": 1, "alpha": 1 }, + { "scaleX": 0.1, "scaleY": 0.1, "alpha": 0 }, + delay, + ANIMATION_DURATION, + Easing.inOutCubic + ); +}; + +/** + * @description デフォルトのローディング演出 + * Default loading direction + * + * @class + */ +export class DefaultLoader +{ + /** + * @description ローディング演出に使用するSprite + * Sprite used for loading direction + * + * @type {Sprite} + * @public + */ + public readonly sprite: Sprite; + + /** + * @constructor + */ + constructor () + { + this.sprite = new Sprite(); + this.initialize(); + } + + /** + * @description ローディング演出の初期化 + * Initialization of loading direction + * + * @return {void} + * @method + * @public + */ + initialize (): void + { + for (let idx = 0; idx < 3; ++idx) { + this.sprite.addChild(new Shape()); + } + } + + /** + * @description Canvasが設置されたDOMにローディング演出を登録、既にDOMがあれば演出を表示 + * Register loading direction in the DOM where Canvas is installed, + * and display the direction if the DOM already exists. + * + * @return {void} + * @method + * @public + */ + start (): void + { + const root = $getContext().root; + if (!root) { + return; + } + + const config = $getConfig(); + const sprite = this.sprite; + + const minSize = Math.ceil(Math.min(config.stage.width, config.stage.height) / 100); + const halfSize = minSize / 2; + + for (let idx = 0; idx < 3; ++idx) { + + const shape = sprite.getChildAt(idx); + if (!shape) { + continue; + } + + /** + * 初期値を設定 + * Set initial values + */ + shape.scaleX = 0.1; + shape.scaleY = 0.1; + shape.alpha = 0; + + /** + * 既存のジョブを停止してクリア + * Stop and clear existing jobs + */ + if (shape.hasLocalVariable("expandJob")) { + (shape.getLocalVariable("expandJob") as Job).stop(); + } + if (shape.hasLocalVariable("reduceJob")) { + (shape.getLocalVariable("reduceJob") as Job).stop(); + } + + /** + * アニメーションジョブを作成 + * 初回のみ遅延を設定し、ループ時はdelayなしで統一サイクル + * Create animation jobs + * Set delay only for the first time, no delay for loop with unified cycle + * + * 初回: expandJob(delay) -> reduceJob(0) -> loopExpandJob(0) -> loopReduceJob(0) -> ... + */ + const initialDelay = ANIMATION_DELAY_INTERVAL * idx; + + // 初回の拡大アニメーション(遅延あり) + const expandJob = createExpandJob(shape, initialDelay); + + // 縮小アニメーション(遅延なし) + const reduceJob = createReduceJob(shape, 0); + + // ループ用の拡大アニメーション(遅延なし) + const loopExpandJob = createExpandJob(shape, 0); + + // ジョブチェーンを構築(ループ) + expandJob.nextJob = reduceJob; + reduceJob.nextJob = loopExpandJob; + loopExpandJob.nextJob = reduceJob; + + // ローカル変数に保存(end時に停止するため) + shape.setLocalVariable("expandJob", expandJob); + shape.setLocalVariable("reduceJob", reduceJob); + shape.setLocalVariable("loopExpandJob", loopExpandJob); + + expandJob.start(); + + if (shape.width === minSize) { + continue; + } + + shape + .graphics + .clear() + .beginFill("#ffffff") + .drawCircle(0, 0, halfSize); + + shape.x = minSize * 2 * idx; + } + + sprite.x = (config.stage.width - sprite.width) / 2; + sprite.y = (config.stage.height - sprite.height) / 2; + root.addChild(sprite); + } + + /** + * @description ローディング演出を非表示にする + * Hide loading direction + * + * @return {void} + * @method + * @public + */ + end (): void + { + const root = $getContext().root; + if (!root) { + return; + } + + const sprite = this.sprite; + for (let idx = 0; idx < 3; ++idx) { + + const shape = sprite.getChildAt(idx); + if (!shape) { + continue; + } + + /** + * 全てのジョブを停止 + * Stop all jobs + */ + const jobNames = ["expandJob", "reduceJob", "loopExpandJob"]; + for (let jIdx = 0; jIdx < jobNames.length; ++jIdx) { + const jobName = jobNames[jIdx]; + if (shape.hasLocalVariable(jobName)) { + (shape.getLocalVariable(jobName) as Job).stop(); + } + } + } + + root.removeChild(sprite); + } +} diff --git a/src/domain/loading/DefaultLoader.ts b/src/domain/loading/DefaultLoader.ts deleted file mode 100644 index a278c22..0000000 --- a/src/domain/loading/DefaultLoader.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Sprite } from "@next2d/display"; -import { execute as defaultLoadingInitializeService } from "./DefaultLoading/service/DefaultLoadingInitializeService"; -import { execute as defaultLoaderStartService } from "./DefaultLoading/service/DefaultLoaderStartService"; -import { execute as defaultLoaderEndService } from "./DefaultLoading/service/DefaultLoaderEndService"; - -/** - * @description デフォルトのローディング演出 - * Default loading direction - * - * @class - */ -export class DefaultLoader -{ - /** - * @description ローディング演出に使用するSprite - * Sprite used for loading direction - * - * @type {Sprite} - * @public - */ - public readonly sprite: Sprite; - - /** - * @constructor - */ - constructor () - { - this.sprite = new Sprite(); - this.initialize(); - } - - /** - * @description ローディング演出の初期化 - * Initialization of loading direction - * - * @return {void} - * @method - * @public - */ - initialize (): void - { - defaultLoadingInitializeService(this); - } - - /** - * @description Canvasが設置されたDOMにローディング演出を登録、既にDOMがあれば演出を表示 - * Register loading direction in the DOM where Canvas is installed, - * and display the direction if the DOM already exists. - * - * @return {void} - * @method - * @public - */ - start (): void - { - defaultLoaderStartService(this); - } - - /** - * @description ローディング演出を非表示にする - * Hide loading direction - * - * @return {void} - * @method - * @public - */ - end (): void - { - defaultLoaderEndService(this); - } -} \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.test.ts b/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.test.ts deleted file mode 100644 index 09e6535..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Shape } from "@next2d/display"; -import type { Job } from "@next2d/ui"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../../application/Context"; -import { $setContext } from "../../../../application/variable/Context"; -import { $setConfig } from "../../../../application/variable/Config"; -import { DefaultLoader } from "../../DefaultLoader"; -import { execute } from "./DefaultLoaderEndService"; -import { describe, expect, it } from "vitest"; - -describe("DefaultLoaderEndService Test", () => -{ - it("execute test", () => - { - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - $setContext(new Context(new MovieClip())); - - // mock - const defaultLoader = new DefaultLoader(); - defaultLoader.start(); - - const sprite = defaultLoader.sprite; - const length = sprite.numChildren; - for (let idx = 0; idx < length; ++idx) { - const shape = sprite.getChildAt(idx) as Shape; - expect(shape.hasLocalVariable("reduceJob")).toBe(true); - expect(shape.hasLocalVariable("expandJob")).toBe(true); - - const expandJob = shape.getLocalVariable("expandJob") as Job; - const reduceJob = shape.getLocalVariable("reduceJob") as Job; - expect(expandJob.stopFlag).toBe(false); - expect(reduceJob.stopFlag).toBe(false); - } - - execute(defaultLoader); - - for (let idx = 0; idx < length; ++idx) { - const shape = sprite.getChildAt(idx) as Shape; - const expandJob = shape.getLocalVariable("expandJob") as Job; - const reduceJob = shape.getLocalVariable("reduceJob") as Job; - expect(expandJob.entries).toBeNull(); - expect(reduceJob.entries).toBeNull(); - } - }); -}); \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.ts b/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.ts deleted file mode 100644 index 12d7360..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DefaultLoader } from "../../DefaultLoader"; -import type { Shape } from "@next2d/display"; -import type { Job } from "@next2d/ui"; -import { $getContext } from "../../../../application/variable/Context"; - -/** - * @description ローダーのアニメーションを終了 - * End loader animation - * - * @param {DefaultLoader} default_loader - * @return {void} - * @method - * @protected - */ -export const execute = (default_loader: DefaultLoader): void => -{ - const root = $getContext().root; - if (!root) { - return ; - } - - const sprite = default_loader.sprite; - for (let idx = 0; idx < 3; ++idx) { - - const shape = sprite.getChildAt(idx); - if (!shape) { - continue ; - } - - if (shape.hasLocalVariable("expandJob")) { - const expandJob = shape.getLocalVariable("expandJob") as Job; - expandJob.stop(); - } - - if (shape.hasLocalVariable("reduceJob")) { - const reduceJob = shape.getLocalVariable("reduceJob") as Job; - reduceJob.stop(); - } - } - - root.removeChild(sprite); -}; \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.test.ts b/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.test.ts deleted file mode 100644 index 1a5c600..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Shape } from "@next2d/display"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../../application/Context"; -import { $setContext } from "../../../../application/variable/Context"; -import { $setConfig } from "../../../../application/variable/Config"; -import { DefaultLoader } from "../../DefaultLoader"; -import { execute } from "./DefaultLoaderStartService"; -import { describe, expect, it } from "vitest"; - -describe("DefaultLoaderStartService Test", () => -{ - it("execute test", async () => - { - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - $setContext(new Context(new MovieClip())); - - // mock - const defaultLoader = new DefaultLoader(); - - const sprite = defaultLoader.sprite; - const length = sprite.numChildren; - for (let idx = 0; idx < length; ++idx) { - const shape = sprite.getChildAt(idx) as Shape; - expect(shape.hasLocalVariable("reduceJob")).toBe(false); - expect(shape.hasLocalVariable("expandJob")).toBe(false); - } - - execute(defaultLoader); - - for (let idx = 0; idx < length; ++idx) { - const shape = sprite.getChildAt(idx) as Shape; - expect(shape.hasLocalVariable("reduceJob")).toBe(true); - expect(shape.hasLocalVariable("expandJob")).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts b/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts deleted file mode 100644 index 5cfe2c8..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { DefaultLoader } from "../../DefaultLoader"; -import type { Shape } from "@next2d/display"; -import type { Job } from "@next2d/ui"; -import { $getConfig } from "../../../../application/variable/Config"; -import { $getContext } from "../../../../application/variable/Context"; -import { - Tween, - Easing -} from "@next2d/ui"; - -/** - * @description ローダーのアニメーションを実行 - * Execute loader animation - * - * @param {DefaultLoader} default_loader - * @return {void} - * @method - * @protected - */ -export const execute = (default_loader: DefaultLoader): void => -{ - const root = $getContext().root; - if (!root) { - return ; - } - - const config = $getConfig(); - const sprite = default_loader.sprite; - - const minSize = Math.ceil(Math.min(config.stage.width, config.stage.height) / 100); - const halfSize = minSize / 2; - for (let idx = 0; idx < 3; ++idx) { - - const shape = sprite.getChildAt(idx); - if (!shape) { - continue ; - } - - /** - * 初期値を設定 - * Set initial values - */ - shape.scaleX = 0.1; - shape.scaleY = 0.1; - shape.alpha = 0; - - let reduceJob: Job; - if (shape.hasLocalVariable("reduceJob")) { - reduceJob = shape.getLocalVariable("reduceJob") as Job; - reduceJob.stop(); - } else { - reduceJob = Tween.add( - shape, - { - "scaleX": 0.1, - "scaleY": 0.1, - "alpha": 0 - }, - { - "scaleX": 1, - "scaleY": 1, - "alpha": 1 - }, - 0.12, - 0.5, - Easing.inOutCubic - ); - shape.setLocalVariable("reduceJob", reduceJob); - } - - let expandJob: Job; - if (shape.hasLocalVariable("expandJob")) { - expandJob = shape.getLocalVariable("expandJob") as Job; - expandJob.stop(); - } else { - expandJob = Tween.add( - shape, - { - "scaleX": 0.1, - "scaleY": 0.1, - "alpha": 0 - }, - { - "scaleX": 1, - "scaleY": 1, - "alpha": 1 - }, - 0.12, - 0.5, - Easing.inOutCubic - ); - shape.setLocalVariable("expandJob", expandJob); - } - - reduceJob.nextJob = expandJob; - expandJob.nextJob = reduceJob; - - if (idx) { - setTimeout((): void => - { - expandJob.start(); - }, 120 * idx); - } else { - expandJob.start(); - } - - if (shape.width === minSize) { - continue; - } - - shape - .graphics - .clear() - .beginFill("#ffffff") - .drawCircle(0, 0, halfSize); - - shape.x = minSize * 2 * idx; - } - - sprite.x = (config.stage.width - sprite.width) / 2; - sprite.y = (config.stage.height - sprite.height) / 2; - root.addChild(sprite); -}; \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.test.ts b/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.test.ts deleted file mode 100644 index 2e2b330..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DefaultLoader } from "../../DefaultLoader"; -import { execute } from "./DefaultLoadingInitializeService"; -import { Sprite } from "@next2d/display"; -import { describe, expect, it } from "vitest"; - -describe("DefaultLoadingInitializeService Test", () => -{ - it("execute test", async () => - { - // mock - const defaultLoader = { - "sprite": new Sprite(), - } as DefaultLoader; - - expect(defaultLoader.sprite.numChildren).toBe(0); - execute(defaultLoader); - expect(defaultLoader.sprite.numChildren).toBe(3); - }); -}); \ No newline at end of file diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.ts b/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.ts deleted file mode 100644 index b3f7efd..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DefaultLoader } from "../../DefaultLoader"; -import { Shape } from "@next2d/display"; - -/** - * @description ローディング演出の初期登録 - * Initial registration of loading direction - * - * @param {DefaultLoader} default_loader - * @return {void} - * @method - * @protected - */ -export const execute = (default_loader: DefaultLoader): void => -{ - for (let idx = 0; idx < 3; ++idx) { - default_loader.sprite.addChild(new Shape()); - } -}; \ No newline at end of file diff --git a/src/domain/loading/Loading/service/LoadingEndService.test.ts b/src/domain/loading/Loading/service/LoadingEndService.test.ts deleted file mode 100644 index 645897b..0000000 --- a/src/domain/loading/Loading/service/LoadingEndService.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { execute } from "./LoadingEndService"; -import { $setConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { $setInstance } from "../../Loading"; -import { describe, expect, it } from "vitest"; - -describe("LoadingEndService Test", () => -{ - it("execute test", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - }, - "loading": { - "callback": "LoaderTest" - } - }); - - let state = "none"; - class LoaderTest { - end (): void { - state = "start"; - } - } - - packages.clear(); - packages.set("LoaderTest", LoaderTest); - - $setInstance(null); - expect(state).toBe("none"); - await execute(); - expect(state).toBe("start"); - }); -}); \ No newline at end of file diff --git a/src/domain/loading/Loading/service/LoadingEndService.ts b/src/domain/loading/Loading/service/LoadingEndService.ts deleted file mode 100644 index 921e02a..0000000 --- a/src/domain/loading/Loading/service/LoadingEndService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DefaultLoader } from "../../DefaultLoader"; -import { $getConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; - -/** - * @description ローダーのアニメーションを終了 - * End loader animation - * - * @return {Promise} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - const config = $getConfig(); - if (!config || !config.loading) { - return ; - } - - const name: string | undefined = config.loading.callback; - if (!name) { - return ; - } - - let instance = $getInstance(); - if (!instance) { - - const LoaderClass: any = packages.has(name) - ? packages.get(name) - : DefaultLoader; - - instance = new LoaderClass(); - $setInstance(instance); - } - - await instance.end(); -}; \ No newline at end of file diff --git a/src/domain/loading/Loading/service/LoadingStartService.test.ts b/src/domain/loading/Loading/service/LoadingStartService.test.ts deleted file mode 100644 index a3132d8..0000000 --- a/src/domain/loading/Loading/service/LoadingStartService.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { execute } from "./LoadingStartService"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../../application/Context"; -import { $setContext } from "../../../../application/variable/Context"; -import { $setConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; -import { describe, expect, it } from "vitest"; - -describe("LoadingStartService Test", () => -{ - it("execute test case1", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - }, - "loading": { - "callback": "Loader" - } - }); - - const root = new MovieClip(); - $setContext(new Context(root)); - - $setInstance(null); - expect($getInstance()).toBeNull(); - await execute(); - expect($getInstance()).not.toBeNull(); - }); - - it("execute test case2", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - }, - "loading": { - "callback": "LoaderTest" - } - }); - - let state = "none"; - class LoaderTest { - start (): void { - state = "start"; - } - } - - packages.clear(); - packages.set("LoaderTest", LoaderTest); - - $setInstance(null); - expect(state).toBe("none"); - await execute(); - expect(state).toBe("start"); - }); -}); \ No newline at end of file diff --git a/src/domain/loading/Loading/service/LoadingStartService.ts b/src/domain/loading/Loading/service/LoadingStartService.ts deleted file mode 100644 index fab5f17..0000000 --- a/src/domain/loading/Loading/service/LoadingStartService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DefaultLoader } from "../../DefaultLoader"; -import { $getConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; - -/** - * @description ローダーのアニメーションを実行 - * Execute loader animation - * - * @return {Promise} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - const config = $getConfig(); - if (!config || !config.loading) { - return ; - } - - const name: string | void = config.loading.callback; - if (!name) { - return ; - } - - let instance = $getInstance(); - if (!instance) { - - const LoaderClass: any = packages.has(name) - ? packages.get(name) - : DefaultLoader; - - instance = new LoaderClass(); - $setInstance(instance); - } - - await instance.start(); -}; \ No newline at end of file diff --git a/src/domain/screen/Capture.ts b/src/domain/screen/Capture.ts deleted file mode 100644 index 550013e..0000000 --- a/src/domain/screen/Capture.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Shape } from "@next2d/display"; - -/** - * @type {Shape} - * @private - */ -export const shape: Shape = new Shape(); - -/** - * @type {Shape} - * @private - */ -export const bitmap: Shape = new Shape(); \ No newline at end of file diff --git a/src/domain/screen/Capture/service/AddScreenCaptureService.test.ts b/src/domain/screen/Capture/service/AddScreenCaptureService.test.ts deleted file mode 100644 index 4494622..0000000 --- a/src/domain/screen/Capture/service/AddScreenCaptureService.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { execute } from "./AddScreenCaptureService"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../../application/Context"; -import { $setContext } from "../../../../application/variable/Context"; -import { $setConfig } from "../../../../application/variable/Config"; -import { describe, expect, it, vi } from "vitest"; - -Object.defineProperty(window, "next2d", { - "get": vi.fn().mockReturnValue({ - "captureToCanvas": async () => { - return document.createElement("canvas"); - } - }) -}); - -describe("AddScreenCaptureService Test", () => -{ - it("execute test", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - - const root = new MovieClip(); - $setContext(new Context(root)); - - expect(root.numChildren).toBe(0); - expect(root.mouseChildren).toBe(true); - await execute(); - expect(root.numChildren).toBe(1); - expect(root.mouseChildren).toBe(false); - }); -}); \ No newline at end of file diff --git a/src/domain/screen/Capture/service/AddScreenCaptureService.ts b/src/domain/screen/Capture/service/AddScreenCaptureService.ts deleted file mode 100644 index b21f341..0000000 --- a/src/domain/screen/Capture/service/AddScreenCaptureService.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { $getConfig } from "../../../../application/variable/Config"; -import { $getContext } from "../../../../application/variable/Context"; -import { Matrix } from "@next2d/geom"; -import { shape } from "../../Capture"; -import { - stage, - BitmapData, - Shape -} from "@next2d/display"; - -/** - * @type {number} - * @private - */ -let $cacheX: number = 0; - -/** - * @type {number} - * @private - */ -let $cacheY: number = 0; - -/** - * @description 画面キャプチャーのShapeをStageに追加 - * Add Screen Capture Shape to Stage - * - * @return {void} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - const root = $getContext().root; - if (!root) { - return ; - } - - /** - * マウス操作を強制停止 - * Mouse operation is forced to stop - */ - root.mouseChildren = false; - - const scale = stage.rendererScale; - const config = $getConfig(); - const width = config.stage.width; - const height = config.stage.height; - - const tx = (stage.rendererWidth - stage.stageWidth * scale) / 2; - const ty = (stage.rendererHeight - stage.stageHeight * scale) / 2; - - /** - * 現在の描画をcanvasに転写 - * Transfer the current drawing to canvas - */ - const rectangle = root.getBounds(); - if (rectangle.width > 0 && rectangle.height > 0) { - - const canvas = await next2d.captureToCanvas(root, { - "matrix": new Matrix( - scale, 0, 0, scale, - -rectangle.x * scale, - -rectangle.y * scale - ) - }); - - const bitmapData = new BitmapData(canvas.width, canvas.height); - bitmapData.canvas = canvas; - - const bitmap = new Shape(); - bitmap.setBitmapBuffer( - canvas.width, canvas.height, - bitmapData.buffer as Uint8Array - ); - - bitmap.scaleX = 1 / scale; - bitmap.scaleY = 1 / scale; - bitmap.x = -tx / scale; - bitmap.y = -ty / scale; - - root.addChild(bitmap); - } - - if (shape.width !== width || shape.width !== height) { - shape - .graphics - .clear() - .beginFill(0, 0.8) - .drawRect(0, 0, width, height) - .endFill(); - } - - if (tx && $cacheX !== tx) { - $cacheX = tx; - shape.width = stage.rendererWidth / scale; - shape.x = -tx / scale; - } - - if (ty && $cacheY !== ty) { - $cacheY = ty; - shape.height = stage.rendererHeight / scale; - shape.y = -ty / scale; - } - - root.addChild(shape); -}; \ No newline at end of file diff --git a/src/domain/screen/Capture/service/DisposeCaptureService.test.ts b/src/domain/screen/Capture/service/DisposeCaptureService.test.ts deleted file mode 100644 index 3b7f5e9..0000000 --- a/src/domain/screen/Capture/service/DisposeCaptureService.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { execute } from "./DisposeCaptureService"; -import { MovieClip } from "@next2d/display"; -import { Context } from "../../../../application/Context"; -import { $setContext } from "../../../../application/variable/Context"; -import { $setConfig } from "../../../../application/variable/Config"; -import { - shape, - bitmap -} from "../../Capture"; -import { describe, expect, it } from "vitest"; - -describe("DisposeCaptureService Test", () => -{ - it("execute test", async () => - { - // mock - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - - const root = new MovieClip(); - $setContext(new Context(root)); - - root.mouseChildren = false; - root.addChild(shape); - root.addChild(bitmap); - - expect(root.numChildren).toBe(2); - expect(root.mouseChildren).toBe(false); - execute(); - expect(root.numChildren).toBe(0); - expect(root.mouseChildren).toBe(true); - }); -}); \ No newline at end of file diff --git a/src/domain/screen/Capture/service/DisposeCaptureService.ts b/src/domain/screen/Capture/service/DisposeCaptureService.ts deleted file mode 100644 index e6cdb87..0000000 --- a/src/domain/screen/Capture/service/DisposeCaptureService.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { $getContext } from "../../../../application/variable/Context"; - -/** - * @description 画面キャプチャーのShapeをStageから削除 - * Delete Screen Capture Shape from Stage - * - * @return {void} - * @method - * @protected - */ -export const execute = (): void => -{ - const root = $getContext().root; - if (!root) { - return ; - } - - /** - * rootの子要素を全て削除 - * Remove all child elements of root - */ - while (root.numChildren > 0) { - root.removeChildAt(0); - } - - /** - * マウス操作を有効化 - * Enable Mouse Operation - */ - root.mouseChildren = true; -}; \ No newline at end of file diff --git a/src/domain/service/LoadingService.test.ts b/src/domain/service/LoadingService.test.ts new file mode 100644 index 0000000..ae35395 --- /dev/null +++ b/src/domain/service/LoadingService.test.ts @@ -0,0 +1,235 @@ +import { LoadingService } from "./LoadingService"; +import { MovieClip } from "@next2d/display"; +import { Context } from "../../application/Context"; +import { $setContext } from "../../application/variable/Context"; +import { $setConfig } from "../../application/variable/Config"; +import { $setPackages, packages } from "../../application/variable/Packages"; +import { $setInstance, $getInstance } from "../variable/Loading"; +import { DefaultLoader } from "../entity/DefaultLoader"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("LoadingService Test", () => +{ + beforeEach(() => + { + $setInstance(null as any); + }); + + describe("getInstance", () => + { + it("should return null if config has no loading", () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const result = LoadingService.getInstance(); + + expect(result).toBeNull(); + }); + + it("should return null if loading callback is not set", () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "" + } + }); + + const result = LoadingService.getInstance(); + + expect(result).toBeNull(); + }); + + it("should return DefaultLoader if callback class not in packages", () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "CustomLoader" + } + }); + + $setPackages([]); + + const result = LoadingService.getInstance(); + + expect(result).toBeInstanceOf(DefaultLoader); + }); + + it("should return same instance on multiple calls", () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "CustomLoader" + } + }); + + $setPackages([]); + + const result1 = LoadingService.getInstance(); + const result2 = LoadingService.getInstance(); + + expect(result1).toBe(result2); + }); + }); + + describe("start", () => + { + it("should create loader and call start", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "Loader" + } + }); + + const root = new MovieClip(); + $setContext(new Context(root)); + + $setInstance(null); + expect($getInstance()).toBeNull(); + await LoadingService.start(); + expect($getInstance()).not.toBeNull(); + }); + + it("should call custom loader start method", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "LoaderTest" + } + }); + + let state = "none"; + class LoaderTest { + start(): void { + state = "start"; + } + } + + packages.clear(); + packages.set("LoaderTest", LoaderTest); + + $setInstance(null); + expect(state).toBe("none"); + await LoadingService.start(); + expect(state).toBe("start"); + }); + }); + + describe("end", () => + { + it("should call custom loader end method", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + }, + "loading": { + "callback": "LoaderTest" + } + }); + + let state = "none"; + class LoaderTest { + end(): void { + state = "end"; + } + } + + packages.clear(); + packages.set("LoaderTest", LoaderTest); + + $setInstance(null); + expect(state).toBe("none"); + await LoadingService.end(); + expect(state).toBe("end"); + }); + + it("should do nothing if no loader configured", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + await LoadingService.end(); + + expect($getInstance()).toBeNull(); + }); + }); + + describe("start edge cases", () => + { + it("should return early if getInstance returns null", async () => + { + // Config without loading - getInstance will return null + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + $setInstance(null); + + // This should return early without throwing + await LoadingService.start(); + + expect($getInstance()).toBeNull(); + }); + }); +}); diff --git a/src/domain/service/LoadingService.ts b/src/domain/service/LoadingService.ts new file mode 100644 index 0000000..7c0357e --- /dev/null +++ b/src/domain/service/LoadingService.ts @@ -0,0 +1,79 @@ +import type { ILoading } from "../../interface/ILoading"; +import type { Constructor } from "../../interface/IPackages"; +import { DefaultLoader } from "../entity/DefaultLoader"; +import { $getConfig } from "../../application/variable/Config"; +import { packages } from "../../application/variable/Packages"; +import { + $getInstance, + $setInstance +} from "../variable/Loading"; + +/** + * @description ローディング処理を管理するドメインサービス + * Domain service for managing loading operations + */ +export const LoadingService = +{ + /** + * @description ローダーのインスタンスを取得または作成 + * Get or create loader instance + * + * @return {ILoading | null} + */ + "getInstance": (): ILoading | null => + { + const config = $getConfig(); + if (!config || !config.loading) { + return null; + } + + const name: string | undefined = config.loading.callback; + if (!name) { + return null; + } + + let instance = $getInstance(); + if (!instance) { + const LoaderClass = packages.has(name) + ? packages.get(name) as Constructor + : DefaultLoader; + + instance = new LoaderClass(); + $setInstance(instance); + } + + return instance; + }, + + /** + * @description ローダーのアニメーションを開始 + * Start loader animation + * + * @return {Promise} + */ + "start": async (): Promise => + { + const instance = LoadingService.getInstance(); + if (!instance) { + return; + } + + await instance.start(); + }, + + /** + * @description ローダーのアニメーションを終了 + * End loader animation + * + * @return {Promise} + */ + "end": async (): Promise => + { + const instance = LoadingService.getInstance(); + if (!instance) { + return; + } + + await instance.end(); + } +}; diff --git a/src/domain/service/ScreenOverlayService.test.ts b/src/domain/service/ScreenOverlayService.test.ts new file mode 100644 index 0000000..12fc697 --- /dev/null +++ b/src/domain/service/ScreenOverlayService.test.ts @@ -0,0 +1,294 @@ +import { ScreenOverlayService } from "./ScreenOverlayService"; +import { MovieClip, Shape, BitmapData, stage } from "@next2d/display"; +import { Context } from "../../application/Context"; +import { $setContext, $getContext } from "../../application/variable/Context"; +import { $setConfig } from "../../application/variable/Config"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +Object.defineProperty(window, "next2d", { + "get": vi.fn().mockReturnValue({ + "captureToCanvas": async () => { + const canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + return canvas; + } + }) +}); + +describe("ScreenOverlayService Test", () => +{ + beforeEach(() => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + }); + + describe("add", () => + { + it("should add overlay shape to stage", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + expect(root.numChildren).toBe(0); + expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); + await ScreenOverlayService.add(); + expect(root.numChildren).toBe(1); + expect(root.mouseChildren).toBe(false); + expect(root.mouseEnabled).toBe(false); + }); + + it("should return early when context root getter returns falsy", async () => + { + const mockContext = { + "root": null, + "view": null, + "viewModel": null + } as unknown as Context; + $setContext(mockContext); + + await ScreenOverlayService.add(); + }); + + it("should add overlay and disable mouse when rectangle has size", async () => + { + const root = new MovieClip(); + const shape = new Shape(); + shape.graphics.beginFill(0xff0000).drawRect(0, 0, 100, 100).endFill(); + root.addChild(shape); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + + expect(root.mouseChildren).toBe(false); + expect(root.mouseEnabled).toBe(false); + }); + + it("should handle bgColor option", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60, + "options": { + "bgColor": "#ffffff" + } + } + }); + + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + + expect(root.mouseChildren).toBe(false); + }); + + it("should handle empty bgColor option", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60, + "options": { + "bgColor": "" + } + } + }); + + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + + expect(root.mouseChildren).toBe(false); + }); + + it("should draw overlay shape with correct dimensions", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + + expect(root.numChildren).toBeGreaterThan(0); + }); + + it("should add multiple times and update cache values", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + const firstCount = root.numChildren; + + await ScreenOverlayService.add(); + + expect(root.numChildren).toBeGreaterThanOrEqual(firstCount); + }); + }); + + describe("dispose", () => + { + it("should remove all children and enable mouse", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + root.mouseChildren = false; + root.mouseEnabled = false; + root.addChild(new Shape()); + root.addChild(new Shape()); + + expect(root.numChildren).toBe(2); + expect(root.mouseChildren).toBe(false); + expect(root.mouseEnabled).toBe(false); + ScreenOverlayService.dispose(); + expect(root.numChildren).toBe(2); + expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); + }); + + it("should return early when context root getter returns falsy", () => + { + const mockContext = { + "root": null, + "view": null, + "viewModel": null + } as unknown as Context; + $setContext(mockContext); + + ScreenOverlayService.dispose(); + }); + + it("should handle already empty root", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + expect(root.numChildren).toBe(0); + ScreenOverlayService.dispose(); + expect(root.numChildren).toBe(0); + expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); + }); + + it("should remove overlay shape added by add method", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenOverlayService.add(); + expect(root.numChildren).toBeGreaterThan(0); + + ScreenOverlayService.dispose(); + expect(root.numChildren).toBe(0); + }); + }); + + describe("cache update branches", () => + { + it("should update cacheX when tx changes", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + // First call sets initial cache values + await ScreenOverlayService.add(); + + // Modify stage properties to create non-zero tx + const originalRendererWidth = stage.rendererWidth; + const originalStageWidth = stage.stageWidth; + const originalRendererScale = stage.rendererScale; + + Object.defineProperty(stage, "rendererWidth", { + "get": () => 1000, + "configurable": true + }); + Object.defineProperty(stage, "stageWidth", { + "get": () => 800, + "configurable": true + }); + Object.defineProperty(stage, "rendererScale", { + "get": () => 1, + "configurable": true + }); + + // This should trigger the tx cache update branch + await ScreenOverlayService.add(); + + // Restore original values + Object.defineProperty(stage, "rendererWidth", { + "get": () => originalRendererWidth, + "configurable": true + }); + Object.defineProperty(stage, "stageWidth", { + "get": () => originalStageWidth, + "configurable": true + }); + Object.defineProperty(stage, "rendererScale", { + "get": () => originalRendererScale, + "configurable": true + }); + }); + + it("should update cacheY when ty changes", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + // First call sets initial cache values + await ScreenOverlayService.add(); + + // Modify stage properties to create non-zero ty + const originalRendererHeight = stage.rendererHeight; + const originalStageHeight = stage.stageHeight; + const originalRendererScale = stage.rendererScale; + + Object.defineProperty(stage, "rendererHeight", { + "get": () => 800, + "configurable": true + }); + Object.defineProperty(stage, "stageHeight", { + "get": () => 600, + "configurable": true + }); + Object.defineProperty(stage, "rendererScale", { + "get": () => 1, + "configurable": true + }); + + // This should trigger the ty cache update branch + await ScreenOverlayService.add(); + + // Restore original values + Object.defineProperty(stage, "rendererHeight", { + "get": () => originalRendererHeight, + "configurable": true + }); + Object.defineProperty(stage, "stageHeight", { + "get": () => originalStageHeight, + "configurable": true + }); + Object.defineProperty(stage, "rendererScale", { + "get": () => originalRendererScale, + "configurable": true + }); + }); + }); +}); diff --git a/src/domain/service/ScreenOverlayService.ts b/src/domain/service/ScreenOverlayService.ts new file mode 100644 index 0000000..3fa6f66 --- /dev/null +++ b/src/domain/service/ScreenOverlayService.ts @@ -0,0 +1,99 @@ +import { $getContext } from "../../application/variable/Context"; +import { stage, Shape } from "@next2d/display"; + +/** + * @type {Shape} + * @private + */ +const shape: Shape = new Shape(); +shape.isBitmap = true; +shape.$bitmapBuffer = new Uint8Array([0, 0, 0, 128]); +shape.graphics.xMin = 0; +shape.graphics.yMin = 0; +shape.graphics.xMax = 1; +shape.graphics.yMax = 1; + +/** + * @type {number} + * @private + */ +let cacheX: number = -1; + +/** + * @type {number} + * @private + */ +let cacheY: number = -1; + +/** + * @description スクリーンの最前面に半透明の黒を重ねる + * Overlay a semi-transparent black at the forefront of the screen + */ +export const ScreenOverlayService = +{ + /** + * @description 画面キャプチャーのShapeをStageに追加 + * Add Screen Capture Shape to Stage + * + * @return {Promise} + */ + "add": async (): Promise => + { + const root = $getContext().root; + if (!root) { + return; + } + + /** + * マウス操作を強制停止 + * Mouse operation is forced to stop + */ + root.mouseChildren = false; + root.mouseEnabled = false; + + const scale = stage.rendererScale; + const tx = (stage.rendererWidth - stage.stageWidth * scale) / 2; + const ty = (stage.rendererHeight - stage.stageHeight * scale) / 2; + + if (cacheX !== tx) { + cacheX = tx; + shape.width = stage.rendererWidth / scale; + shape.x = -tx / scale; + } + + if (cacheY !== ty) { + cacheY = ty; + shape.height = stage.rendererHeight / scale; + shape.y = -ty / scale; + } + + root.addChild(shape); + }, + + /** + * @description 画面キャプチャーのShapeをStageから削除 + * Delete Screen Capture Shape from Stage + * + * @return {void} + */ + "dispose": (): void => + { + const root = $getContext().root; + if (!root) { + return; + } + + /** + * rootの子要素を全て削除(末尾から削除することでO(n)に最適化) + * Remove all child elements of root (optimized to O(n) by removing from end) + */ + root.removeChild(shape); + + /** + * マウス操作を有効化 + * Enable Mouse Operation + */ + root.mouseChildren = true; + root.mouseEnabled = true; + } +}; diff --git a/src/domain/service/ViewBinderService.test.ts b/src/domain/service/ViewBinderService.test.ts new file mode 100644 index 0000000..fb1dd4d --- /dev/null +++ b/src/domain/service/ViewBinderService.test.ts @@ -0,0 +1,281 @@ +import { ViewBinderService } from "./ViewBinderService"; +import { MovieClip } from "@next2d/display"; +import { Context } from "../../application/Context"; +import { $setContext } from "../../application/variable/Context"; +import { $setConfig } from "../../application/variable/Config"; +import { packages } from "../../application/variable/Packages"; +import { View } from "../../view/View"; +import { ViewModel } from "../../view/ViewModel"; +import { describe, expect, it } from "vitest"; + +describe("ViewBinderService Test", () => +{ + describe("bind", () => + { + it("should bind View and ViewModel", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + let viewModelInitialized = false; + let viewInitialized = false; + + class TestViewModel extends ViewModel + { + async initialize () + { + viewModelInitialized = true; + } + } + + class TestView extends View + { + async initialize () + { + viewInitialized = true; + } + + async onEnter () + { + } + + async onExit () + { + } + } + + packages.clear(); + packages.set("TestView", TestView); + packages.set("TestViewModel", TestViewModel); + + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + expect(viewModelInitialized).toBe(false); + expect(viewInitialized).toBe(false); + expect(root.numChildren).toBe(0); + + await ViewBinderService.bind(context, "test"); + + expect(viewModelInitialized).toBe(true); + expect(viewInitialized).toBe(true); + expect(root.numChildren).toBe(1); + expect(context.view).toBeInstanceOf(TestView); + expect(context.viewModel).toBeInstanceOf(TestViewModel); + }); + + it("should throw error when packages not found", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + packages.clear(); + + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + await expect(ViewBinderService.bind(context, "notfound")).rejects.toThrow("not found view or viewModel."); + }); + + it("should remove existing children before adding view", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + class TestViewModel extends ViewModel + { + async initialize(): Promise {} + } + class TestView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} + } + + packages.clear(); + packages.set("TestView", TestView); + packages.set("TestViewModel", TestViewModel); + + const root = new MovieClip(); + root.addChild(new MovieClip()); + root.addChild(new MovieClip()); + + const context = new Context(root); + $setContext(context); + + expect(root.numChildren).toBe(2); + + await ViewBinderService.bind(context, "test"); + + expect(root.numChildren).toBe(3); + expect(context.view).toBeInstanceOf(TestView); + }); + }); + + describe("unbind", () => + { + it("should unbind View and ViewModel", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + let onExitCalled = false; + + class TestViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class TestView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit () + { + onExitCalled = true; + } + } + + const vm = new TestViewModel(); + context.view = new TestView(vm); + root.addChild(context.view); + context.viewModel = vm; + + expect(onExitCalled).toBe(false); + expect(root.numChildren).toBe(1); + + await ViewBinderService.unbind(context); + + expect(onExitCalled).toBe(true); + expect(root.numChildren).toBe(0); + }); + + it("should return early if view is null", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + class TestViewModel extends ViewModel + { + async initialize(): Promise {} + } + + context.view = null; + context.viewModel = new TestViewModel(); + + await ViewBinderService.unbind(context); + + expect(root.numChildren).toBe(0); + }); + + it("should return early if viewModel is null", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const root = new MovieClip(); + const context = new Context(root); + $setContext(context); + + class TestViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class TestView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} + } + + const vm = new TestViewModel(); + context.view = new TestView(vm); + context.viewModel = null; + + await ViewBinderService.unbind(context); + + expect(root.numChildren).toBe(0); + }); + + it("should return early if root is null", async () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + // Create a mock context with null root + const mockContext = { + "root": null, + "view": { + "onExit": async () => {} + }, + "viewModel": { + "unbind": () => {} + } + } as unknown as Context; + + // Should not throw + await ViewBinderService.unbind(mockContext); + }); + }); +}); diff --git a/src/domain/service/ViewBinderService.ts b/src/domain/service/ViewBinderService.ts new file mode 100644 index 0000000..d19c75b --- /dev/null +++ b/src/domain/service/ViewBinderService.ts @@ -0,0 +1,85 @@ +import type { Context } from "../../application/Context"; +import type { View } from "../../view/View"; +import type { ViewModel } from "../../view/ViewModel"; +import type { Constructor } from "../../interface/IPackages"; +import { packages } from "../../application/variable/Packages"; +import { toCamelCase } from "../../shared/util/ToCamelCase"; + +/** + * @description ViewとViewModelのバインドを行うドメインサービス + * Domain service for binding View and ViewModel + */ +export const ViewBinderService = +{ + /** + * @description ViewとViewModelをバインドします + * Binds View and ViewModel + * + * @param {Context} context + * @param {string} name + * @return {Promise} + */ + "bind": async (context: Context, name: string): Promise => + { + const viewName = `${toCamelCase(name)}View`; + const viewModelName = `${viewName}Model`; + + if (!packages.size + || !packages.has(viewName) + || !packages.has(viewModelName) + ) { + throw new Error("not found view or viewModel."); + } + + /** + * 遷移先のViewとViewModelを起動、初期化処理を実行 + * Start the destination View and ViewModel, and execute the initialization process + */ + const ViewModelClass = packages.get(viewModelName) as Constructor; + context.viewModel = new ViewModelClass(); + await context.viewModel.initialize(); + + const ViewClass = packages.get(viewName) as Constructor; + context.view = new ViewClass(context.viewModel); + await context.view.initialize(); + + /** + * stageの一番背面にviewをセット + * Set the view at the very back of the stage + */ + const root = context.root; + root.addChildAt(context.view, 0); + + return context.view; + }, + + /** + * @description ViewとViewModelのバインドを解除します + * Unbinds View and ViewModel + * + * @param {Context} context + * @return {Promise} + */ + "unbind": async (context: Context): Promise => + { + if (!context.view || !context.viewModel) { + return; + } + + /** + * ViewのonExitをコール + * Call View's onExit + */ + await context.view.onExit(); + + /** + * ViewをStageから削除 + * Remove View from Stage + */ + const root = context.root; + if (!root) { + return; + } + root.removeChild(context.view); + } +}; diff --git a/src/domain/loading/Loading.ts b/src/domain/variable/Loading.ts similarity index 61% rename from src/domain/loading/Loading.ts rename to src/domain/variable/Loading.ts index a7bed1b..957ee9f 100644 --- a/src/domain/loading/Loading.ts +++ b/src/domain/variable/Loading.ts @@ -1,7 +1,7 @@ import type { ILoading } from "../../interface/ILoading"; /** - * @type {object} + * @type {ILoading | null} * @default null * @private */ @@ -11,25 +11,25 @@ let $instance: ILoading | null = null; * @description ローダーのインスタンスを取得 * Get loader instance * - * @return {ILoading} + * @return {ILoading | null} * @method - * @public + * @protected */ -export const $getInstance = (): ILoading => +export const $getInstance = (): ILoading | null => { - return $instance as ILoading; + return $instance; }; /** * @description ローダーのインスタンスを設定 * Set loader instance * - * @param {ILoading} instance + * @param {ILoading | null} instance * @return {void} * @method - * @public + * @protected */ -export const $setInstance = (instance: ILoading): void => +export const $setInstance = (instance: ILoading | null): void => { $instance = instance; -}; \ No newline at end of file +}; diff --git a/src/index.ts b/src/index.ts index 58a36e7..2e7798b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import { TextFieldContent } from "./application/content/TextFieldContent"; import { VideoContent } from "./application/content/VideoContent"; // output build version -console.log("%c Next2D Framework %c 3.0.13 %c https://next2d.app", +console.log("%c Next2D Framework %c 4.0.0 %c https://next2d.app", "color: #fff; background: #5f5f5f", "color: #fff; background: #4bc729", ""); diff --git a/src/infrastructure/Request/repository/RequestContentRepository.ts b/src/infrastructure/Request/repository/RequestContentRepository.ts deleted file mode 100644 index 388e441..0000000 --- a/src/infrastructure/Request/repository/RequestContentRepository.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { IRequest } from "../../../interface/IRequest"; -import { Loader } from "@next2d/display"; -import { URLRequest } from "@next2d/net"; -import { cache } from "../../../application/variable/Cache"; -import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { execute as callbackService } from "../../../domain/callback/service/CallbackService"; - -/** - * @description 指定先のJSONを非同期で取得 - * Asynchronously obtain JSON of the specified destination - * - * @param {IRequest} request_object - * @return {Promise} - * @method - * @public - */ -export const execute = async (request_object: IRequest): Promise => -{ - if (!request_object.path || !request_object.name) { - throw new Error("`path` and `name` must be set for content requests."); - } - - const name = request_object.name; - - /** - * キャッシュを利用する場合はキャッシュデータをチェック - * Check cache data if cache is used - */ - if (request_object.cache) { - - if (cache.size && cache.has(name)) { - - const value: any = cache.get(name); - - if (request_object.callback) { - await callbackService(request_object.callback, value); - } - - return new ResponseDTO(name, value); - } - } - - const urlRequest = new URLRequest(request_object.path); - - const method: string = request_object.method - ? request_object.method.toUpperCase() - : "GET"; - - switch (method) { - - case "DELETE": - case "GET": - case "HEAD": - case "OPTIONS": - case "POST": - case "PUT": - urlRequest.method = method; - break; - - default: - urlRequest.method = "GET"; - break; - - } - - if (request_object.headers) { - for (const [name, value] of Object.entries(request_object.headers)) { - urlRequest - .requestHeaders - .push({ name, value }); - } - } - - if (request_object.body) { - urlRequest.data = JSON.stringify(request_object.body); - } - - const loader = new Loader(); - await loader.load(urlRequest); - - const content = loader.content; - - /** - * Animation Toolで設定したシンボルをマップに登録 - * Register the symbols set by Animation Tool to the map - */ - const loaderInfo = loader.loaderInfo; - if (loaderInfo.data) { - const symbols: Map = loaderInfo.data.symbols; - if (symbols.size) { - for (const name of symbols.keys()) { - loaderInfoMap.set(name, loaderInfo); - } - } - } - - if (request_object.cache) { - cache.set(request_object.name, content); - } - - if (request_object.callback) { - await callbackService(request_object.callback, content); - } - - return new ResponseDTO(request_object.name, content); -}; \ No newline at end of file diff --git a/src/infrastructure/Request/repository/RequestCustomRepository.test.ts b/src/infrastructure/Request/repository/RequestCustomRepository.test.ts deleted file mode 100644 index e09537a..0000000 --- a/src/infrastructure/Request/repository/RequestCustomRepository.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { execute } from "./RequestCustomRepository"; -import { packages } from "../../../application/variable/Packages"; -import type { IRequest } from "../../../interface/IRequest"; -import { describe, expect, it } from "vitest"; - -describe("RequestCustomRepository Test", () => -{ - it("execute public test", async () => - { - // mock - const object: IRequest = { - "type": "custom", - "name": "CustomRepository", - "path": "next2d", - "method": "publicGet", - "access": "public", - "class": "CustomClass" - }; - - const CustomClass = class CustomClass - { - publicGet () - { - return "publicGet"; - } - }; - - packages.clear(); - packages.set("CustomClass", CustomClass); - - const responseDTO = await execute(object); - expect(responseDTO.name).toBe("CustomRepository"); - expect(responseDTO.response).toBe("publicGet"); - }); - - it("execute static test", async () => - { - // mock - const object: IRequest = { - "type": "custom", - "name": "CustomRepository", - "path": "next2d", - "method": "staticGet", - "access": "static", - "class": "CustomClass" - }; - - const CustomClass = class CustomClass - { - static staticGet () - { - return "staticGet"; - } - }; - - packages.clear(); - packages.set("CustomClass", CustomClass); - - const responseDTO = await execute(object) - expect(responseDTO.name).toBe("CustomRepository"); - expect(responseDTO.response).toBe("staticGet"); - }); -}); diff --git a/src/infrastructure/Request/repository/RequestCustomRepository.ts b/src/infrastructure/Request/repository/RequestCustomRepository.ts deleted file mode 100644 index 9fb5544..0000000 --- a/src/infrastructure/Request/repository/RequestCustomRepository.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { IRequest } from "../../../interface/IRequest"; -import { packages } from "../../../application/variable/Packages"; -import { cache } from "../../../application/variable/Cache"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { execute as callbackService } from "../../../domain/callback/service/CallbackService"; - -/** - * @description 指定先の外部データを非同期で取得 - * Asynchronous acquisition of external data at specified destination - * - * @param {IRequest} request_object - * @return {Promise} - * @method - * @public - */ -export const execute = async (request_object: IRequest): Promise => -{ - if (!request_object.class - || !request_object.access - || !request_object.method - || !request_object.name - ) { - throw new Error("`class`, `access`, `method` and `name` must be set for custom requests."); - } - - const name = request_object.name; - - /** - * キャッシュを利用する場合はキャッシュデータをチェック - * Check cache data if cache is used - */ - if (request_object.cache) { - - if (cache.size && cache.has(name)) { - - const value: any = cache.get(name); - - if (request_object.callback) { - await callbackService(request_object.callback, value); - } - - return new ResponseDTO(name, value); - } - } - - const className = request_object.class; - if (!packages.has(className)) { - throw new Error("package not found."); - } - - const CallbackClass: any = packages.get(className); - const value = request_object.access === "static" - ? await CallbackClass[request_object.method]() - : await new CallbackClass()[request_object.method](); - - if (request_object.cache) { - cache.set(name, value); - } - - if (request_object.callback) { - await callbackService(request_object.callback, value); - } - - return new ResponseDTO(name, value); -}; \ No newline at end of file diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.ts b/src/infrastructure/Request/repository/RequestJsonRepository.ts deleted file mode 100644 index 9c30745..0000000 --- a/src/infrastructure/Request/repository/RequestJsonRepository.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { IRequest } from "../../../interface/IRequest"; -import { cache } from "../../../application/variable/Cache"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { execute as callbackService } from "../../../domain/callback/service/CallbackService"; - -/** - * @description 指定先のJSONを非同期で取得 - * Asynchronously obtain JSON of the specified destination - * - * @param {IRequest} request_object - * @return {Promise} - * @method - * @public - */ -export const execute = async (request_object: IRequest): Promise => -{ - if (!request_object.path || !request_object.name) { - throw new Error("`path` and `name` must be set for json requests."); - } - - const name = request_object.name; - - /** - * キャッシュを利用する場合はキャッシュデータをチェック - * Check cache data if cache is used - */ - if (request_object.cache) { - if (cache.size && cache.has(name)) { - - const value: any = cache.get(name); - - if (request_object.callback) { - await callbackService(request_object.callback, value); - } - - return new ResponseDTO(name, value); - } - } - - const options: RequestInit = {}; - - const method = options.method = request_object.method - ? request_object.method.toUpperCase() - : "GET"; - - const body = request_object.body - && method === "POST" || method === "PUT" - ? JSON.stringify(request_object.body) - : null; - - if (body) { - options.body = body; - } - - if (request_object.headers) { - options.headers = request_object.headers; - } - - const response = await fetch(request_object.path, options); - const value = await response.json(); - - if (request_object.cache) { - cache.set(name, value); - } - - if (request_object.callback) { - await callbackService(request_object.callback, value); - } - - return new ResponseDTO(name, value); -}; \ No newline at end of file diff --git a/src/infrastructure/Request/usecase/RequestUseCase.ts b/src/infrastructure/Request/usecase/RequestUseCase.ts deleted file mode 100644 index ddd2978..0000000 --- a/src/infrastructure/Request/usecase/RequestUseCase.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { execute as requestContentRepository } from "../repository/RequestContentRepository"; -import { execute as requestCustomRepository } from "../repository/RequestCustomRepository"; -import { execute as requestJsonRepository } from "../repository/RequestJsonRepository"; -import { execute as configParserRequestsPropertyService } from "../../../application/Config/service/ConfigParserRequestsPropertyService"; - -/** - * @description Routing設定で指定したタイプへリクエストを実行 - * Execute requests to the type specified in Routing settings - * - * @param {string} name - * @return {Promise} - * @method - * @public - */ -export const execute = async (name: string): Promise => -{ - const responses: ResponseDTO[] = []; - const requests = configParserRequestsPropertyService(name); - for (let idx = 0; idx < requests.length; ++idx) { - - const requestObject = requests[idx]; - switch (requestObject.type) { - - case "json": - { - const response = await requestJsonRepository(requestObject); - if (!response) { - continue; - } - responses.push(response); - } - break; - - case "content": - { - const response = await requestContentRepository(requestObject); - if (!response) { - continue; - } - responses.push(response); - } - break; - - case "custom": - { - const response = await requestCustomRepository(requestObject); - if (!response) { - continue; - } - responses.push(response); - } - break; - - default: - break; - - } - } - - return responses; -}; \ No newline at end of file diff --git a/src/infrastructure/Response/dto/ResponseDTO.test.ts b/src/infrastructure/Response/dto/ResponseDTO.test.ts deleted file mode 100644 index 2778376..0000000 --- a/src/infrastructure/Response/dto/ResponseDTO.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ResponseDTO } from "./ResponseDTO"; -import { describe, expect, it } from "vitest"; - -describe("ResponseDTOTest", () => -{ - it("execute test case1", () => - { - const responseDTO = new ResponseDTO(); - expect(responseDTO.name).toBe(""); - expect(responseDTO.response).toBe(null); - }); - - it("execute test case2", () => - { - const responseDTO = new ResponseDTO("sample", 100); - expect(responseDTO.name).toBe("sample"); - expect(responseDTO.response).toBe(100); - }); -}); \ No newline at end of file diff --git a/src/infrastructure/Response/variable/Response.ts b/src/infrastructure/Response/variable/Response.ts deleted file mode 100644 index 09a4b9b..0000000 --- a/src/infrastructure/Response/variable/Response.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @type {Map} - * @protected - */ -export const response: Map = new Map(); \ No newline at end of file diff --git a/src/infrastructure/dto/ResponseDTO.test.ts b/src/infrastructure/dto/ResponseDTO.test.ts new file mode 100644 index 0000000..9a6605c --- /dev/null +++ b/src/infrastructure/dto/ResponseDTO.test.ts @@ -0,0 +1,37 @@ +import { ResponseDTO } from "./ResponseDTO"; +import { describe, expect, it } from "vitest"; + +describe("ResponseDTOTest", () => +{ + it("execute test case1", () => + { + const responseDTO = new ResponseDTO("", null); + expect(responseDTO.name).toBe(""); + expect(responseDTO.response).toBe(null); + expect(responseDTO.callback).toBeUndefined(); + }); + + it("execute test case2", () => + { + const responseDTO = new ResponseDTO("sample", 100); + expect(responseDTO.name).toBe("sample"); + expect(responseDTO.response).toBe(100); + expect(responseDTO.callback).toBeUndefined(); + }); + + it("execute test case3 with callback", () => + { + const responseDTO = new ResponseDTO("sample", { data: "test" }, "CallbackClass"); + expect(responseDTO.name).toBe("sample"); + expect(responseDTO.response).toEqual({ data: "test" }); + expect(responseDTO.callback).toBe("CallbackClass"); + }); + + it("execute test case4 with callback array", () => + { + const responseDTO = new ResponseDTO("sample", 200, ["Callback1", "Callback2"]); + expect(responseDTO.name).toBe("sample"); + expect(responseDTO.response).toBe(200); + expect(responseDTO.callback).toEqual(["Callback1", "Callback2"]); + }); +}); diff --git a/src/infrastructure/Response/dto/ResponseDTO.ts b/src/infrastructure/dto/ResponseDTO.ts similarity index 51% rename from src/infrastructure/Response/dto/ResponseDTO.ts rename to src/infrastructure/dto/ResponseDTO.ts index a625e67..e98e526 100644 --- a/src/infrastructure/Response/dto/ResponseDTO.ts +++ b/src/infrastructure/dto/ResponseDTO.ts @@ -4,14 +4,13 @@ * * @class */ -export class ResponseDTO +export class ResponseDTO { /** * @description キャッシュのキー名 * Key name of cache * - * @return {string} - * @default "" + * @type {string} * @readonly * @public */ @@ -21,22 +20,33 @@ export class ResponseDTO * @description レスポンスデータ * response data * - * @return {*} - * @default null + * @type {T} * @readonly * @public */ - public readonly response: any; + public readonly response: T; /** - * @param {string} [name=""] - * @param {*} [response=null] + * @description コールバッククラス名 + * Callback class name + * + * @type {string | string[] | undefined} + * @readonly + * @public + */ + public readonly callback?: string | string[]; + + /** + * @param {string} name + * @param {T} response + * @param {string | string[]} [callback] * @constructor * @public */ - constructor (name: string = "", response: any = null) + constructor (name: string, response: T, callback?: string | string[]) { - this.name = name; + this.name = name; this.response = response; + this.callback = callback; } -} \ No newline at end of file +} diff --git a/src/infrastructure/repository/ContentRepository.test.ts b/src/infrastructure/repository/ContentRepository.test.ts new file mode 100644 index 0000000..22f9aff --- /dev/null +++ b/src/infrastructure/repository/ContentRepository.test.ts @@ -0,0 +1,156 @@ +import { execute } from "./ContentRepository"; +import { cache } from "../../application/variable/Cache"; +import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; +import { describe, expect, it, beforeEach, vi } from "vitest"; + +vi.mock("@next2d/display", () => ({ + "Loader": class MockLoader { + private _content: any = { "type": "content" }; + private _loaderInfo: any = { + "data": { + "symbols": new Map([["Symbol1", 1], ["Symbol2", 2]]) + } + }; + + get content (): any + { + return this._content; + } + + get loaderInfo (): any + { + return this._loaderInfo; + } + + async load (_: any): Promise + { + return; + } + } +})); + +vi.mock("@next2d/net", () => ({ + "URLRequest": class MockURLRequest { + public method: string = "GET"; + public data: any = null; + public requestHeaders: Array<{ name: string; value: string }> = []; + + constructor (public url: string) {} + } +})); + +describe("ContentRepository Test", () => +{ + beforeEach(() => + { + cache.clear(); + loaderInfoMap.clear(); + }); + + describe("execute", () => + { + it("should throw error when path is not set", async () => + { + await expect(execute({ + "type": "content", + "name": "test" + })).rejects.toThrow("`path` and `name` must be set for content requests."); + }); + + it("should throw error when name is not set", async () => + { + await expect(execute({ + "type": "content", + "path": "/path/to/content" + })).rejects.toThrow("`path` and `name` must be set for content requests."); + }); + + it("should throw error when both path and name are not set", async () => + { + await expect(execute({ + "type": "content" + })).rejects.toThrow("`path` and `name` must be set for content requests."); + }); + + it("should load content successfully", async () => + { + const result = await execute({ + "type": "content", + "path": "/path/to/content.json", + "name": "testContent" + }); + + expect(result.name).toBe("testContent"); + expect(result.response).toEqual({ "type": "content" }); + }); + + it("should register symbols to loaderInfoMap", async () => + { + expect(loaderInfoMap.size).toBe(0); + + await execute({ + "type": "content", + "path": "/path/to/content.json", + "name": "testContent" + }); + + expect(loaderInfoMap.size).toBe(2); + expect(loaderInfoMap.has("Symbol1")).toBe(true); + expect(loaderInfoMap.has("Symbol2")).toBe(true); + }); + + it("should use POST method when specified", async () => + { + const result = await execute({ + "type": "content", + "path": "/path/to/content.json", + "name": "testContent", + "method": "POST" + }); + + expect(result.name).toBe("testContent"); + }); + + it("should include headers when specified", async () => + { + const result = await execute({ + "type": "content", + "path": "/path/to/content.json", + "name": "testContent", + "headers": { + "Authorization": "Bearer token123", + "Content-Type": "application/json" + } + }); + + expect(result.name).toBe("testContent"); + }); + + it("should include body when specified", async () => + { + const result = await execute({ + "type": "content", + "path": "/path/to/content.json", + "name": "testContent", + "body": { "key": "value" } + }); + + expect(result.name).toBe("testContent"); + }); + + it("should return cached response when available", async () => + { + cache.set("cachedContent", { "cached": true }); + + const result = await execute({ + "type": "content", + "path": "/path/to/cached.json", + "name": "cachedContent", + "cache": true + }); + + expect(result.name).toBe("cachedContent"); + expect(result.response).toEqual({ "cached": true }); + }); + }); +}); diff --git a/src/infrastructure/repository/ContentRepository.ts b/src/infrastructure/repository/ContentRepository.ts new file mode 100644 index 0000000..88bfb31 --- /dev/null +++ b/src/infrastructure/repository/ContentRepository.ts @@ -0,0 +1,65 @@ +import type { IRequest } from "../../interface/IRequest"; +import { Loader } from "@next2d/display"; +import { URLRequest } from "@next2d/net"; +import { ResponseDTO } from "../dto/ResponseDTO"; +import { normalizeHttpMethod } from "../../shared/util/NormalizeHttpMethod"; +import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; + +/** + * @description 指定先のコンテンツを非同期で取得 + * Asynchronously obtain content of the specified destination + * + * @param {IRequest} request_object + * @return {Promise} + * @method + * @public + */ +export const execute = async (request_object: IRequest): Promise => +{ + if (!request_object.path || !request_object.name) { + throw new Error("`path` and `name` must be set for content requests."); + } + + const cachedResponse = requestCacheCheckService(request_object); + if (cachedResponse) { + return cachedResponse; + } + + const urlRequest = new URLRequest(request_object.path); + urlRequest.method = normalizeHttpMethod(request_object.method); + + if (request_object.headers) { + for (const [name, value] of Object.entries(request_object.headers)) { + urlRequest + .requestHeaders + .push({ name, value }); + } + } + + if (request_object.body) { + urlRequest.data = JSON.stringify(request_object.body); + } + + const loader = new Loader(); + await loader.load(urlRequest); + + const content = loader.content; + + /** + * Animation Toolで設定したシンボルをマップに登録 + * Register the symbols set by Animation Tool to the map + */ + const loaderInfo = loader.loaderInfo; + if (loaderInfo.data) { + const symbols: Map = loaderInfo.data.symbols; + if (symbols.size) { + for (const symbolName of symbols.keys()) { + loaderInfoMap.set(symbolName, loaderInfo); + } + } + } + + return requestResponseProcessService(request_object, content); +}; diff --git a/src/infrastructure/repository/CustomRepository.test.ts b/src/infrastructure/repository/CustomRepository.test.ts new file mode 100644 index 0000000..211bf62 --- /dev/null +++ b/src/infrastructure/repository/CustomRepository.test.ts @@ -0,0 +1,124 @@ +import { execute } from "./CustomRepository"; +import { packages } from "../../application/variable/Packages"; +import { cache } from "../../application/variable/Cache"; +import type { IRequest } from "../../interface/IRequest"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("CustomRepository Test", () => +{ + beforeEach(() => + { + packages.clear(); + cache.clear(); + }); + + it("execute public test", async () => + { + // mock + const object: IRequest = { + "type": "custom", + "name": "CustomRepository", + "path": "next2d", + "method": "publicGet", + "access": "public", + "class": "CustomClass" + }; + + const CustomClass = class CustomClass + { + publicGet () + { + return "publicGet"; + } + }; + + packages.set("CustomClass", CustomClass); + + const responseDTO = await execute(object); + expect(responseDTO.name).toBe("CustomRepository"); + expect(responseDTO.response).toBe("publicGet"); + }); + + it("execute static test", async () => + { + // mock + const object: IRequest = { + "type": "custom", + "name": "CustomRepository", + "path": "next2d", + "method": "staticGet", + "access": "static", + "class": "CustomClass" + }; + + const CustomClass = class CustomClass + { + static staticGet () + { + return "staticGet"; + } + }; + + packages.set("CustomClass", CustomClass); + + const responseDTO = await execute(object); + expect(responseDTO.name).toBe("CustomRepository"); + expect(responseDTO.response).toBe("staticGet"); + }); + + it("should throw error when required fields are missing", async () => + { + const object: IRequest = { + "type": "custom", + "name": "", + "path": "next2d" + }; + + await expect(execute(object)).rejects.toThrow( + "`class`, `access`, `method` and `name` must be set for custom requests." + ); + }); + + it("should return cached response when cache is available", async () => + { + const object: IRequest = { + "type": "custom", + "name": "CachedCustom", + "path": "next2d", + "method": "getData", + "access": "public", + "class": "CustomClass", + "cache": true + }; + + const cachedValue = { data: "cached" }; + cache.set("CachedCustom", cachedValue); + + const CustomClass = class CustomClass + { + getData () + { + return "fresh data"; + } + }; + packages.set("CustomClass", CustomClass); + + const responseDTO = await execute(object); + expect(responseDTO.name).toBe("CachedCustom"); + expect(responseDTO.response).toEqual(cachedValue); + }); + + it("should throw error when package is not found", async () => + { + const object: IRequest = { + "type": "custom", + "name": "CustomRepository", + "path": "next2d", + "method": "getData", + "access": "public", + "class": "NonExistentClass" + }; + + await expect(execute(object)).rejects.toThrow("package not found."); + }); +}); diff --git a/src/infrastructure/repository/CustomRepository.ts b/src/infrastructure/repository/CustomRepository.ts new file mode 100644 index 0000000..46ce82d --- /dev/null +++ b/src/infrastructure/repository/CustomRepository.ts @@ -0,0 +1,51 @@ +import type { IRequest } from "../../interface/IRequest"; +import type { Constructor } from "../../interface/IPackages"; +import { ResponseDTO } from "../dto/ResponseDTO"; +import { packages } from "../../application/variable/Packages"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; + +/** + * @description Customリクエスト用のクラスインターフェース + * Class interface for Custom request + */ +interface ICustomClass { + [key: string]: () => Promise | unknown; +} + +/** + * @description 指定先の外部データを非同期で取得 + * Asynchronous acquisition of external data at specified destination + * + * @param {IRequest} request_object + * @return {Promise} + * @method + * @public + */ +export const execute = async (request_object: IRequest): Promise => +{ + if (!request_object.class + || !request_object.access + || !request_object.method + || !request_object.name + ) { + throw new Error("`class`, `access`, `method` and `name` must be set for custom requests."); + } + + const cachedResponse = requestCacheCheckService(request_object); + if (cachedResponse) { + return cachedResponse; + } + + const className = request_object.class; + if (!packages.has(className)) { + throw new Error("package not found."); + } + + const CustomClass = packages.get(className) as Constructor & ICustomClass; + const value = request_object.access === "static" + ? await CustomClass[request_object.method]() + : await new CustomClass()[request_object.method](); + + return requestResponseProcessService(request_object, value); +}; diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.test.ts b/src/infrastructure/repository/JsonRepository.test.ts similarity index 57% rename from src/infrastructure/Request/repository/RequestJsonRepository.test.ts rename to src/infrastructure/repository/JsonRepository.test.ts index 426c6dc..98ea73b 100644 --- a/src/infrastructure/Request/repository/RequestJsonRepository.test.ts +++ b/src/infrastructure/repository/JsonRepository.test.ts @@ -1,8 +1,8 @@ -import type { IRequest } from "../../../interface/IRequest"; -import { execute } from "./RequestJsonRepository"; +import type { IRequest } from "../../interface/IRequest"; +import { execute } from "./JsonRepository"; import { describe, expect, it, vi } from "vitest"; -describe("RequestJsonRepository Test", () => +describe("JsonRepository Test", () => { it("execute fetch get test", async () => { @@ -14,13 +14,15 @@ describe("RequestJsonRepository Test", () => "method": "GET" }; - const responseMock = (url: any, options: any) => + const responseMock = (url: string, options: RequestInit) => { expect(url).toBe(object.path); expect(options.method).toBe(object.method); return Promise.resolve({ + "ok": true, "status": 200, + "statusText": "OK", "json": () => { return Promise.resolve("success fetch json"); } @@ -49,16 +51,18 @@ describe("RequestJsonRepository Test", () => } }; - const responseMock = (url: any, options: any) => + const responseMock = (url: string, options: RequestInit) => { expect(url).toBe(object.path); expect(options.method).toBe(object.method); expect(options.body).toBe(JSON.stringify(object.body)); - expect(options.headers["Content-Type"]).toBe("application/json"); + expect((options.headers as Record)["Content-Type"]).toBe("application/json"); return Promise.resolve({ + "ok": true, "status": 200, + "statusText": "OK", "json": () => { return Promise.resolve("success fetch json"); } @@ -87,15 +91,17 @@ describe("RequestJsonRepository Test", () => } }; - const responseMock = (url: any, options: any) => + const responseMock = (url: string, options: RequestInit) => { expect(url).toBe(object.path); expect(options.method).toBe(object.method); expect(options.body).toBe(JSON.stringify(object.body)); - expect(options.headers["Content-Type"]).toBe("application/json"); + expect((options.headers as Record)["Content-Type"]).toBe("application/json"); return Promise.resolve({ + "ok": true, "status": 200, + "statusText": "OK", "json": () => { return Promise.resolve("success fetch json"); } @@ -107,4 +113,53 @@ describe("RequestJsonRepository Test", () => expect(responseDTO.name).toBe("JsonRepository"); expect(responseDTO.response).toBe("success fetch json"); }); -}); \ No newline at end of file + + it("execute fetch error test", async () => + { + // mock + const object: IRequest = { + "type": "json", + "name": "JsonRepository", + "path": "next2d", + "method": "GET" + }; + + const responseMock = () => + { + return Promise.resolve({ + "ok": false, + "status": 404, + "statusText": "Not Found", + "json": () => { + return Promise.resolve(null); + } + }); + }; + global.fetch = vi.fn().mockImplementation(responseMock); + + await expect(execute(object)).rejects.toThrow("HTTP error: 404 Not Found for next2d"); + }); + + it("should throw error when path or name is missing", async () => + { + const objectWithoutPath: IRequest = { + "type": "json", + "name": "TestName", + "path": "" + }; + + await expect(execute(objectWithoutPath)).rejects.toThrow( + "`path` and `name` must be set for json requests." + ); + + const objectWithoutName: IRequest = { + "type": "json", + "name": "", + "path": "test/path" + }; + + await expect(execute(objectWithoutName)).rejects.toThrow( + "`path` and `name` must be set for json requests." + ); + }); +}); diff --git a/src/infrastructure/repository/JsonRepository.ts b/src/infrastructure/repository/JsonRepository.ts new file mode 100644 index 0000000..cb806b3 --- /dev/null +++ b/src/infrastructure/repository/JsonRepository.ts @@ -0,0 +1,48 @@ +import type { IRequest } from "../../interface/IRequest"; +import { ResponseDTO } from "../dto/ResponseDTO"; +import { normalizeHttpMethod } from "../../shared/util/NormalizeHttpMethod"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; + +/** + * @description 指定先のJSONを非同期で取得 + * Asynchronously obtain JSON of the specified destination + * + * @param {IRequest} requestObject + * @return {Promise} + * @throws {Error} path/nameが未設定の場合、HTTPエラーの場合 + * @method + * @public + */ +export const execute = async (requestObject: IRequest): Promise => +{ + if (!requestObject.path || !requestObject.name) { + throw new Error("`path` and `name` must be set for json requests."); + } + + const cachedResponse = requestCacheCheckService(requestObject); + if (cachedResponse) { + return cachedResponse; + } + + const method = normalizeHttpMethod(requestObject.method); + const options: RequestInit = { method }; + + if (requestObject.body && (method === "POST" || method === "PUT")) { + options.body = JSON.stringify(requestObject.body); + } + + if (requestObject.headers) { + options.headers = requestObject.headers; + } + + const response = await fetch(requestObject.path, options); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status} ${response.statusText} for ${requestObject.path}`); + } + + const value = await response.json(); + + return requestResponseProcessService(requestObject, value); +}; diff --git a/src/infrastructure/service/RequestCacheCheckService.test.ts b/src/infrastructure/service/RequestCacheCheckService.test.ts new file mode 100644 index 0000000..50654a7 --- /dev/null +++ b/src/infrastructure/service/RequestCacheCheckService.test.ts @@ -0,0 +1,58 @@ +import { execute } from "./RequestCacheCheckService"; +import { cache } from "../../application/variable/Cache"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("RequestCacheCheckService Test", () => +{ + beforeEach(() => + { + cache.clear(); + }); + + it("should return null if cache is not enabled", async () => + { + const result = await execute({ + "type": "json", + "name": "test", + "cache": false + }); + + expect(result).toBeNull(); + }); + + it("should return null if name is not set", async () => + { + const result = await execute({ + "type": "json", + "cache": true + }); + + expect(result).toBeNull(); + }); + + it("should return null if cache does not contain the key", async () => + { + const result = await execute({ + "type": "json", + "name": "test", + "cache": true + }); + + expect(result).toBeNull(); + }); + + it("should return ResponseDTO if cache contains the key", async () => + { + cache.set("test", { "data": "cached" }); + + const result = await execute({ + "type": "json", + "name": "test", + "cache": true + }); + + expect(result).not.toBeNull(); + expect(result?.name).toBe("test"); + expect(result?.response).toEqual({ "data": "cached" }); + }); +}); diff --git a/src/infrastructure/service/RequestCacheCheckService.ts b/src/infrastructure/service/RequestCacheCheckService.ts new file mode 100644 index 0000000..89e9971 --- /dev/null +++ b/src/infrastructure/service/RequestCacheCheckService.ts @@ -0,0 +1,29 @@ +import type { IRequest } from "../../interface/IRequest"; +import { cache } from "../../application/variable/Cache"; +import { ResponseDTO } from "../dto/ResponseDTO"; + +/** + * @description キャッシュをチェックし、存在すればキャッシュデータを返却 + * Check cache and return cached data if exists + * + * @param {IRequest} requestObject + * @return {ResponseDTO | null} + * @method + * @public + */ +export const execute = (requestObject: IRequest): ResponseDTO | null => +{ + if (!requestObject.cache || !requestObject.name) { + return null; + } + + const name = requestObject.name; + + if (!cache.has(name)) { + return null; + } + + const value: unknown = cache.get(name); + + return new ResponseDTO(name, value, requestObject.callback); +}; diff --git a/src/infrastructure/service/RequestResponseProcessService.test.ts b/src/infrastructure/service/RequestResponseProcessService.test.ts new file mode 100644 index 0000000..16cabef --- /dev/null +++ b/src/infrastructure/service/RequestResponseProcessService.test.ts @@ -0,0 +1,47 @@ +import { execute } from "./RequestResponseProcessService"; +import { cache } from "../../application/variable/Cache"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("RequestResponseProcessService Test", () => +{ + beforeEach(() => + { + cache.clear(); + }); + + it("should return ResponseDTO with name and value", async () => + { + const result = await execute({ + "type": "json", + "name": "test" + }, { "data": "response" }); + + expect(result.name).toBe("test"); + expect(result.response).toEqual({ "data": "response" }); + }); + + it("should save to cache if cache is enabled", async () => + { + expect(cache.has("test")).toBe(false); + + await execute({ + "type": "json", + "name": "test", + "cache": true + }, { "data": "cached" }); + + expect(cache.has("test")).toBe(true); + expect(cache.get("test")).toEqual({ "data": "cached" }); + }); + + it("should not save to cache if cache is disabled", async () => + { + await execute({ + "type": "json", + "name": "test", + "cache": false + }, { "data": "not-cached" }); + + expect(cache.has("test")).toBe(false); + }); +}); diff --git a/src/infrastructure/service/RequestResponseProcessService.ts b/src/infrastructure/service/RequestResponseProcessService.ts new file mode 100644 index 0000000..188972e --- /dev/null +++ b/src/infrastructure/service/RequestResponseProcessService.ts @@ -0,0 +1,24 @@ +import type { IRequest } from "../../interface/IRequest"; +import { cache } from "../../application/variable/Cache"; +import { ResponseDTO } from "../dto/ResponseDTO"; + +/** + * @description レスポンスをキャッシュに保存してDTOを返却 + * Save response to cache and return DTO + * + * @param {IRequest} requestObject + * @param {T} value + * @return {ResponseDTO} + * @method + * @public + */ +export const execute = (requestObject: IRequest, value: T): ResponseDTO => +{ + const name = requestObject.name as string; + + if (requestObject.cache) { + cache.set(name, value); + } + + return new ResponseDTO(name, value, requestObject.callback); +}; diff --git a/src/infrastructure/Request/usecase/RequestUseCase.test.ts b/src/infrastructure/usecase/RequestUseCase.test.ts similarity index 88% rename from src/infrastructure/Request/usecase/RequestUseCase.test.ts rename to src/infrastructure/usecase/RequestUseCase.test.ts index ca08d02..69cebaf 100644 --- a/src/infrastructure/Request/usecase/RequestUseCase.test.ts +++ b/src/infrastructure/usecase/RequestUseCase.test.ts @@ -1,9 +1,9 @@ import { execute } from "./RequestUseCase"; -import { $setPackages, packages } from "../../../application/variable/Packages"; -import { cache } from "../../../application/variable/Cache"; -import { $setConfig } from "../../../application/variable/Config"; +import { $setPackages, packages } from "../../application/variable/Packages"; +import { cache } from "../../application/variable/Cache"; +import { $setConfig } from "../../application/variable/Config"; import { describe, expect, it } from "vitest"; -import type { IConfig } from "../../../interface/IConfig"; +import type { IConfig } from "../../interface/IConfig"; describe("RequestUseCase Test", () => { diff --git a/src/infrastructure/usecase/RequestUseCase.ts b/src/infrastructure/usecase/RequestUseCase.ts new file mode 100644 index 0000000..fa5be71 --- /dev/null +++ b/src/infrastructure/usecase/RequestUseCase.ts @@ -0,0 +1,43 @@ +import type { IRequest } from "../../interface/IRequest"; +import type { ResponseDTO } from "../dto/ResponseDTO"; +import { execute as routingRequestsParserService } from "../../application/service/RoutingRequestsParserService"; +import { repositoryMap } from "../variable/RepositoryMap"; + +/** + * @description ルーティング設定のリクエスト配列を取得 + * Get request array from routing settings + * + * @param {string} name + * @return {IRequest[]} + * @method + * @public + */ +export const getRequests = (name: string): IRequest[] => +{ + return routingRequestsParserService(name); +}; + +/** + * @description Routing設定で指定したタイプへリクエストを並列実行 + * Execute requests in parallel to the type specified in Routing settings + * + * @param {string} name + * @return {Promise} + * @method + * @public + */ +export const execute = async (name: string): Promise => +{ + const requests = routingRequestsParserService(name); + + const promises: Promise[] = []; + for (let idx = 0; idx < requests.length; ++idx) { + const requestObject = requests[idx]; + const repository = repositoryMap.get(requestObject.type); + if (repository) { + promises.push(repository(requestObject)); + } + } + + return Promise.all(promises); +}; diff --git a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts similarity index 53% rename from src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts rename to src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts index 9e5ab40..4a88e27 100644 --- a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts @@ -1,8 +1,7 @@ import { execute } from "./ResponseRemoveVariableUseCase"; -import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; -import { $setConfig } from "../../../application/variable/Config"; -import { IConfig } from "../../../interface/IConfig"; -import { response } from "../../Response/variable/Response"; +import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; +import type { IRequest } from "../../interface/IRequest"; +import { response } from "../variable/Response"; import { describe, expect, it } from "vitest"; describe("ResponseRemoveVariableUseCase", () => @@ -37,41 +36,25 @@ describe("ResponseRemoveVariableUseCase", () => response.set("test2", []); expect(response.size).toBe(2); - // mock - const config: IConfig = { - "platform": "web", - "spa": true, - "stage": { - "width": 240, - "height": 240, - "fps": 12, - "options": {} + // mock requests array + const requests: IRequest[] = [ + { + "type": "content", + "name": "test1" }, - "routing": { - "test": { - "requests": [ - { - "type": "content", - "name": "test1" - }, - { - "type": "json", - "name": "test2" - }, - { - "type": "content", - "name": "test3", - "cache": true - } - ] - } + { + "type": "json", + "name": "test2" + }, + { + "type": "content", + "name": "test3", + "cache": true } - }; - - $setConfig(config); + ]; // execute - execute("test"); + execute(requests); // test expect(response.size).toBe(0); @@ -80,4 +63,4 @@ describe("ResponseRemoveVariableUseCase", () => expect(loaderInfoMap.has("symbol_2")).toBe(true); expect(loaderInfoMap.has("symbol_3")).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts similarity index 69% rename from src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts rename to src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts index a37ba69..688c5d1 100644 --- a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts @@ -1,20 +1,19 @@ import type { DisplayObject } from "@next2d/display"; -import { execute as configParserRequestsPropertyService } from "../../../application/Config/service/ConfigParserRequestsPropertyService"; -import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; +import type { IRequest } from "../../interface/IRequest"; +import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; import { response } from "../variable/Response"; /** * @description レスポンスデータを削除、キャッシュ設定があれば削除しない * Remove response data, do not remove if cache setting is present * - * @param {string} name + * @param {IRequest[]} requests * @return {void} * @method * @public */ -export const execute = (name: string): void => +export const execute = (requests: IRequest[]): void => { - const requests = configParserRequestsPropertyService(name); for (let idx = 0; idx < requests.length; ++idx) { const object = requests[idx]; @@ -34,13 +33,13 @@ export const execute = (name: string): void => * キャッシュしないパッケージはインメモリから削除 * Remove non-cached packages from in-memory */ - const content = response.get(object.name) as D; + const content = response.get(object.name) as DisplayObject; const contentLoaderInfo = content.loaderInfo; if (contentLoaderInfo && contentLoaderInfo.data) { const symbols: Map = contentLoaderInfo.data.symbols; if (symbols.size) { - for (const name of symbols.keys()) { - loaderInfoMap.delete(name); + for (const symbolName of symbols.keys()) { + loaderInfoMap.delete(symbolName); } } } @@ -53,4 +52,4 @@ export const execute = (name: string): void => if (response.size) { response.clear(); } -}; \ No newline at end of file +}; diff --git a/src/infrastructure/variable/RepositoryMap.ts b/src/infrastructure/variable/RepositoryMap.ts new file mode 100644 index 0000000..c43a225 --- /dev/null +++ b/src/infrastructure/variable/RepositoryMap.ts @@ -0,0 +1,24 @@ +import type { IRequest } from "../../interface/IRequest"; +import type { ResponseDTO } from "../dto/ResponseDTO"; +import { execute as contentRepository } from "../repository/ContentRepository"; +import { execute as customRepository } from "../repository/CustomRepository"; +import { execute as jsonRepository } from "../repository/JsonRepository"; + +/** + * @description リポジトリ実行関数の型 + * Type for repository executor function + */ +type RepositoryExecutor = (request: IRequest) => Promise; + +/** + * @description リクエストタイプとリポジトリのマップ + * Map of request types and repositories + * + * @type {Map} + * @protected + */ +export const repositoryMap: Map = new Map([ + ["json", jsonRepository], + ["content", contentRepository], + ["custom", customRepository] +]); diff --git a/src/infrastructure/variable/Response.ts b/src/infrastructure/variable/Response.ts new file mode 100644 index 0000000..c9a685f --- /dev/null +++ b/src/infrastructure/variable/Response.ts @@ -0,0 +1,5 @@ +/** + * @type {Map} + * @protected + */ +export const response: Map = new Map(); diff --git a/src/interface/IAccessType.ts b/src/interface/IAccessType.ts new file mode 100644 index 0000000..9172556 --- /dev/null +++ b/src/interface/IAccessType.ts @@ -0,0 +1,5 @@ +/** + * @description カスタムリクエストのアクセス方法 + * Access method for custom request + */ +export type IAccessType = "static" | "instance" | (string & {}); \ No newline at end of file diff --git a/src/interface/ICallback.ts b/src/interface/ICallback.ts new file mode 100644 index 0000000..abed351 --- /dev/null +++ b/src/interface/ICallback.ts @@ -0,0 +1,17 @@ +/** + * @description Callbackクラスが実装すべきインターフェース + * Interface that Callback classes should implement + * + * @interface + */ +export interface ICallback { + /** + * @description コールバック処理を実行 + * Execute callback process + * + * @param {T} value + * @return {Promise | R} + * @method + */ + execute(value: T): Promise | R; +} diff --git a/src/interface/IConfig.ts b/src/interface/IConfig.ts index 72f65a4..466a77a 100644 --- a/src/interface/IConfig.ts +++ b/src/interface/IConfig.ts @@ -6,15 +6,53 @@ interface IBaseConfig { [key: string]: any } +/** + * @description アプリケーション設定のインターフェース + * Application configuration interface + * + * @interface + */ export interface IConfig extends IBaseConfig { + /** + * @description プラットフォーム識別子 + * Platform identifier + */ platform: string; + + /** + * @description ステージ設定 + * Stage configuration + */ stage: IStage; + + /** + * @description SPAモードの有効/無効 + * Enable/disable SPA mode + */ spa: boolean; + + /** + * @description デフォルトのトップページ + * Default top page + */ defaultTop?: string; + + /** + * @description 画面遷移時のコールバック設定 + * Callback configuration for view transitions + */ gotoView?: IGotoView; - routing?: { - [key: string]: IRouting - }; + + /** + * @description ルーティング設定 + * Routing configuration + */ + routing?: Record; + + /** + * @description ローディング設定 + * Loading configuration + */ loading?: { callback: string; }; diff --git a/src/interface/ILoading.ts b/src/interface/ILoading.ts index 5227f28..5c434f2 100644 --- a/src/interface/ILoading.ts +++ b/src/interface/ILoading.ts @@ -1,4 +1,19 @@ +/** + * @description ローディング処理のインターフェース + * Interface for loading operations + * + * @interface + */ export interface ILoading { - start: Function; - end: Function; + /** + * @description ローディング開始処理 + * Start loading process + */ + start: () => Promise | void; + + /** + * @description ローディング終了処理 + * End loading process + */ + end: () => Promise | void; } \ No newline at end of file diff --git a/src/interface/IPackages.ts b/src/interface/IPackages.ts index 75153ce..1cb91fa 100644 --- a/src/interface/IPackages.ts +++ b/src/interface/IPackages.ts @@ -1 +1,11 @@ -export type IPackages = Array> \ No newline at end of file +/** + * @description コンストラクタ型 + * Constructor type + */ +export type Constructor = new (...args: unknown[]) => T; + +/** + * @description パッケージリストの型定義 + * Type definition for package list + */ +export type IPackages = [string, Constructor][]; \ No newline at end of file diff --git a/src/interface/IRequest.ts b/src/interface/IRequest.ts index 44cedd3..4f0c540 100644 --- a/src/interface/IRequest.ts +++ b/src/interface/IRequest.ts @@ -1,14 +1,74 @@ import type { IRequestType } from "./IRequestType"; +import type { IAccessType } from "./IAccessType"; +/** + * @description HTTPメソッドの型 + * HTTP method type + */ +export type IHttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; + +/** + * @description リクエスト設定のインターフェース + * Request configuration interface + */ export interface IRequest { + /** + * @description リクエストタイプ + * Request type + */ type: IRequestType; + + /** + * @description リクエストパス + * Request path + */ path?: string; + + /** + * @description レスポンスのキャッシュキー名 + * Cache key name for response + */ name?: string; + + /** + * @description キャッシュの有効/無効 + * Enable/disable cache + */ cache?: boolean; - callback?: string | string[any]; + + /** + * @description レスポンス取得後のコールバッククラス名 + * Callback class name after response + */ + callback?: string | string[]; + + /** + * @description カスタムリクエスト用クラス名 + * Class name for custom request + */ class?: string; - access?: string; - method?: string; + + /** + * @description カスタムリクエストのアクセス方法 (static/instance) + * Access method for custom request + */ + access?: IAccessType; + + /** + * @description カスタムリクエストで呼び出すメソッド名 + * Method name to call for custom request + */ + method?: IHttpMethod | string; + + /** + * @description リクエストヘッダー + * Request headers + */ headers?: HeadersInit; - body?: any; + + /** + * @description リクエストボディ + * Request body + */ + body?: BodyInit | Record; } \ No newline at end of file diff --git a/src/interface/IRequestType.ts b/src/interface/IRequestType.ts index 936eecb..8d201dd 100644 --- a/src/interface/IRequestType.ts +++ b/src/interface/IRequestType.ts @@ -1 +1 @@ -export type IRequestType = "json" | "content" | "custom" | "cluster"; \ No newline at end of file +export type IRequestType = "json" | "content" | "custom" | "cluster" | (string & {}); \ No newline at end of file diff --git a/src/shared/util/NormalizeHttpMethod.test.ts b/src/shared/util/NormalizeHttpMethod.test.ts new file mode 100644 index 0000000..6f3d184 --- /dev/null +++ b/src/shared/util/NormalizeHttpMethod.test.ts @@ -0,0 +1,50 @@ +import { normalizeHttpMethod } from "./NormalizeHttpMethod"; +import { describe, expect, it } from "vitest"; + +describe("NormalizeHttpMethod Test", () => +{ + it("should return GET when method is undefined", () => + { + expect(normalizeHttpMethod()).toBe("GET"); + }); + + it("should return GET when method is empty", () => + { + expect(normalizeHttpMethod("")).toBe("GET"); + }); + + it("should normalize lowercase GET", () => + { + expect(normalizeHttpMethod("get")).toBe("GET"); + }); + + it("should normalize lowercase POST", () => + { + expect(normalizeHttpMethod("post")).toBe("POST"); + }); + + it("should normalize lowercase PUT", () => + { + expect(normalizeHttpMethod("put")).toBe("PUT"); + }); + + it("should normalize lowercase DELETE", () => + { + expect(normalizeHttpMethod("delete")).toBe("DELETE"); + }); + + it("should normalize lowercase HEAD", () => + { + expect(normalizeHttpMethod("head")).toBe("HEAD"); + }); + + it("should normalize lowercase OPTIONS", () => + { + expect(normalizeHttpMethod("options")).toBe("OPTIONS"); + }); + + it("should return GET for unknown method", () => + { + expect(normalizeHttpMethod("PATCH")).toBe("GET"); + }); +}); diff --git a/src/shared/util/NormalizeHttpMethod.ts b/src/shared/util/NormalizeHttpMethod.ts new file mode 100644 index 0000000..e6381a3 --- /dev/null +++ b/src/shared/util/NormalizeHttpMethod.ts @@ -0,0 +1,31 @@ +import type { IHttpMethod } from "../../interface/IRequest"; + +/** + * @description HTTPメソッドを正規化 + * Normalize HTTP method + * + * @param {string} [method] + * @return {IHttpMethod} + * @method + * @public + */ +export const normalizeHttpMethod = (method?: string): IHttpMethod => +{ + if (!method) { + return "GET"; + } + + const normalized = method.toUpperCase(); + switch (normalized) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": + return normalized; + + default: + return "GET"; + } +}; diff --git a/src/shared/util/ToCamelCase.test.ts b/src/shared/util/ToCamelCase.test.ts new file mode 100644 index 0000000..81d54ae --- /dev/null +++ b/src/shared/util/ToCamelCase.test.ts @@ -0,0 +1,45 @@ +import { toCamelCase } from "./ToCamelCase"; +import { describe, expect, it } from "vitest"; + +describe("ToCamelCase Test", () => +{ + it("should convert single word", () => + { + expect(toCamelCase("home")).toBe("Home"); + }); + + it("should convert slash separated path", () => + { + expect(toCamelCase("quest/list")).toBe("QuestList"); + }); + + it("should convert multi-level path", () => + { + expect(toCamelCase("game/list/page")).toBe("GameListPage"); + }); + + it("should convert hyphen separated words", () => + { + expect(toCamelCase("user-profile")).toBe("UserProfile"); + }); + + it("should convert underscore separated words", () => + { + expect(toCamelCase("user_settings")).toBe("UserSettings"); + }); + + it("should convert mixed separators", () => + { + expect(toCamelCase("game/user-profile_page")).toBe("GameUserProfilePage"); + }); + + it("should convert multiple hyphens", () => + { + expect(toCamelCase("my-awesome-component")).toBe("MyAwesomeComponent"); + }); + + it("should convert multiple underscores", () => + { + expect(toCamelCase("my_awesome_component")).toBe("MyAwesomeComponent"); + }); +}); diff --git a/src/shared/util/ToCamelCase.ts b/src/shared/util/ToCamelCase.ts new file mode 100644 index 0000000..e395aaa --- /dev/null +++ b/src/shared/util/ToCamelCase.ts @@ -0,0 +1,28 @@ +/** + * @description セパレータ用の正規表現(モジュールレベルで定義して再コンパイルを回避) + * Regex for separators (defined at module level to avoid recompilation) + * + * @type {RegExp} + * @private + */ +const SEPARATOR_REGEX = /-|\/|_/; + +/** + * @description キャメルケースに変換 + * Convert to CamelCase + * + * @param {string} name + * @return {string} + * @method + * @public + */ +export const toCamelCase = (name: string): string => +{ + const names = name.split(SEPARATOR_REGEX); + let result = ""; + for (let idx = 0; idx < names.length; ++idx) { + const word = names[idx]; + result += word.charAt(0).toUpperCase() + word.slice(1); + } + return result; +}; diff --git a/src/view/View.test.ts b/src/view/View.test.ts index a6dc762..0fb5f1e 100644 --- a/src/view/View.test.ts +++ b/src/view/View.test.ts @@ -1,10 +1,156 @@ import { View } from "./View"; +import { ViewModel } from "./ViewModel"; import { describe, expect, it } from "vitest"; +/** + * テスト用の具象ViewModelクラス + */ +class TestViewModel extends ViewModel +{ + async initialize(): Promise {} +} + +/** + * テスト用の具象Viewクラス + */ +class TestView extends View +{ + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} +} + describe("View Test", () => { - it("initialize call test", () => { - const view = new View(); + it("should create an instance", () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + expect(view).toBeInstanceOf(View); + }); + + it("should have initialize method", () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + expect(view.initialize).toBeDefined(); expect(typeof view.initialize).toBe("function"); }); -}); \ No newline at end of file + + it("should have onEnter method", () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + expect(view.onEnter).toBeDefined(); + expect(typeof view.onEnter).toBe("function"); + }); + + it("should have onExit method", () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + expect(view.onExit).toBeDefined(); + expect(typeof view.onExit).toBe("function"); + }); + + it("initialize should return Promise", async () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + const result = await view.initialize(); + expect(result).toBeUndefined(); + }); + + it("onEnter should return Promise", async () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + const result = await view.onEnter(); + expect(result).toBeUndefined(); + }); + + it("onExit should return Promise", async () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + const result = await view.onExit(); + expect(result).toBeUndefined(); + }); + + it("should be able to call lifecycle methods in sequence", async () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + await view.initialize(); + await view.onEnter(); + await view.onExit(); + expect(view).toBeInstanceOf(View); + }); + + it("should be extendable", () => + { + class CustomViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class CustomView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} + } + + const vm = new CustomViewModel(); + const customView = new CustomView(vm); + expect(customView).toBeInstanceOf(View); + expect(customView).toBeInstanceOf(CustomView); + }); + + it("extended class should override lifecycle methods", async () => + { + let initCalled = false; + let enterCalled = false; + let exitCalled = false; + + class CustomViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class CustomView extends View + { + async initialize(): Promise + { + initCalled = true; + } + + async onEnter(): Promise + { + enterCalled = true; + } + + async onExit(): Promise + { + exitCalled = true; + } + } + + const vm = new CustomViewModel(); + const customView = new CustomView(vm); + await customView.initialize(); + await customView.onEnter(); + await customView.onExit(); + + expect(initCalled).toBe(true); + expect(enterCalled).toBe(true); + expect(exitCalled).toBe(true); + }); + + it("should have vm property set via constructor", () => + { + const vm = new TestViewModel(); + const view = new TestView(vm); + expect(view["vm"]).toBe(vm); + }); +}); diff --git a/src/view/View.ts b/src/view/View.ts index ca1cbc1..0290537 100644 --- a/src/view/View.ts +++ b/src/view/View.ts @@ -1,32 +1,62 @@ import { Sprite } from "@next2d/display"; +import type { ViewModel } from "./ViewModel"; /** * @description Viewの親クラス、抽象クラスとして存在しています。 * It exists as a parent class of View and as an abstract class. * * @class - * @extends {MovieClip} + * @extends {Sprite} + * @abstract */ -export class View extends Sprite +export abstract class View extends Sprite { + /** + * @description ViewModelへの参照 + * Reference to ViewModel + * + * @type {VM} + * @protected + */ + protected readonly vm: VM; + /** * @constructor * @public */ - constructor () + constructor (vm: VM) { super(); - this.initialize(); + this.vm = vm; } /** * @description constructorが起動した後にコールされます。 * Called after the constructor is invoked. * - * @return {void} + * @return {Promise} * @method * @abstract */ - // eslint-disable-next-line no-empty-function - initialize (): void {} + abstract initialize (): Promise; + + /** + * @description Viewが表示された際にコールされます。 + * Called when the View is displayed. + * + * @return {Promise} + * @method + * @public + */ + abstract onEnter (): Promise; + + /** + * @description Viewが非表示になった際にコールされます。 + * Called when the View is hidden. + * + * @return {Promise} + * @method + * @public + */ + abstract onExit (): Promise; } diff --git a/src/view/ViewModel.test.ts b/src/view/ViewModel.test.ts index 6a4a130..62aadc9 100644 --- a/src/view/ViewModel.test.ts +++ b/src/view/ViewModel.test.ts @@ -1,20 +1,126 @@ -import { View } from "./View"; import { ViewModel } from "./ViewModel"; import { describe, expect, it } from "vitest"; +/** + * テスト用の具象ViewModelクラス + */ +class TestViewModel extends ViewModel +{ + async initialize(): Promise {} +} + describe("ViewModel Test", () => { - it("bind call test", async () => + it("should create an instance", () => + { + const viewModel = new TestViewModel(); + expect(viewModel).toBeInstanceOf(ViewModel); + }); + + it("should have initialize method", () => { - const view = new View(); - const viewModel = new ViewModel(); - expect(await viewModel.bind(view)).toBe(undefined); + const viewModel = new TestViewModel(); + expect(viewModel.initialize).toBeDefined(); + expect(typeof viewModel.initialize).toBe("function"); }); - it("unbind call test", async () => + it("initialize should return Promise", async () => { - const view = new View(); - const viewModel = new ViewModel(); - expect(await viewModel.unbind(view)).toBe(view); + const viewModel = new TestViewModel(); + const result = await viewModel.initialize(); + expect(result).toBeUndefined(); + }); + + it("should be able to call initialize multiple times", async () => + { + const viewModel = new TestViewModel(); + await viewModel.initialize(); + await viewModel.initialize(); + expect(viewModel).toBeInstanceOf(ViewModel); + }); + + it("should be extendable", () => + { + class CustomViewModel extends ViewModel + { + async initialize(): Promise + { + // Custom initialization + } + } + + const customViewModel = new CustomViewModel(); + expect(customViewModel).toBeInstanceOf(ViewModel); + expect(customViewModel).toBeInstanceOf(CustomViewModel); + }); + + it("extended class should override initialize method", async () => + { + let initCalled = false; + + class CustomViewModel extends ViewModel + { + async initialize(): Promise + { + initCalled = true; + } + } + + const customViewModel = new CustomViewModel(); + await customViewModel.initialize(); + + expect(initCalled).toBe(true); + }); + + it("extended class can have additional properties", () => + { + class CustomViewModel extends ViewModel + { + public data: string = "test"; + + async initialize(): Promise + { + this.data = "initialized"; + } + } + + const customViewModel = new CustomViewModel(); + expect(customViewModel.data).toBe("test"); + }); + + it("extended class can modify properties in initialize", async () => + { + class CustomViewModel extends ViewModel + { + public data: string = "test"; + + async initialize(): Promise + { + this.data = "initialized"; + } + } + + const customViewModel = new CustomViewModel(); + await customViewModel.initialize(); + expect(customViewModel.data).toBe("initialized"); + }); + + it("extended class can have async operations in initialize", async () => + { + class CustomViewModel extends ViewModel + { + public loaded: boolean = false; + + async initialize(): Promise + { + await new Promise(resolve => setTimeout(resolve, 10)); + this.loaded = true; + } + } + + const customViewModel = new CustomViewModel(); + expect(customViewModel.loaded).toBe(false); + await customViewModel.initialize(); + expect(customViewModel.loaded).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/view/ViewModel.ts b/src/view/ViewModel.ts index e63e51e..48d3a04 100644 --- a/src/view/ViewModel.ts +++ b/src/view/ViewModel.ts @@ -1,37 +1,19 @@ -import type { View } from "./View"; - /** * @description ViewModelの親クラス、抽象クラスとして存在しています。 * It exists as a parent class of ViewModel and as an abstract class. * * @class + * @abstract */ -export class ViewModel +export abstract class ViewModel { /** - * @description rootのSpriteにアタッチされたタイミングでコールされます。 - * Called at the timing when the root Sprite is attached. + * @description constructorが起動した後にコールされます。 + * Called after the constructor is invoked. * - * @param {View} view * @return {Promise} * @method * @abstract */ - // @ts-ignore - // eslint-disable-next-line unused-imports/no-unused-vars - async bind (view: View): Promise { return void 0 } - - /** - * @description 新しいViewクラスがアタッチされる前にコールされます。 - * Called before a new View class is attached. - * - * @param {View} view - * @return {Promise} - * @method - * @abstract - */ - async unbind (view: View): Promise - { - return view; - } + abstract initialize (): Promise; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 999270b..03ee100 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "rootDir": "./src", "outDir": "./dist/src", "types": [ "vitest/globals" diff --git a/vite.config.ts b/vite.config.ts index 27cd40f..291fefc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ "@vitest/web-worker", "vitest-webgl-canvas-mock" ], + "pool": "threads", "include": ["src/**/*.test.ts"] } }); \ No newline at end of file