From d0fbf04efde4bd5205fe25c21500cb93cdfa30fa Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 1 Nov 2025 09:21:28 +0900 Subject: [PATCH 01/28] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3c6442..a995ff4 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) From 77f132354de570a554313ffaa764b2c57658f2bc Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 15 Nov 2025 08:00:13 +0900 Subject: [PATCH 02/28] #155 update packages --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index b5d4752..5b10543 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/)", @@ -32,18 +32,18 @@ }, "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/js": "^9.39.1", + "@types/node": "^24.10.1", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", + "@vitest/web-worker": "^4.0.9", + "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.4.0", - "jsdom": "^27.1.0", + "globals": "^16.5.0", + "jsdom": "^27.2.0", "typescript": "^5.9.3", - "vite": "^7.1.12", - "vitest": "^4.0.6", + "vite": "^7.2.2", + "vitest": "^4.0.9", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { From bccd34762e0982749c5c399ce176ac452954649a Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 15 Nov 2025 08:00:35 +0900 Subject: [PATCH 03/28] =?UTF-8?q?#155=20View=E3=81=A8ViewModel=E3=81=AE?= =?UTF-8?q?=E4=BE=9D=E5=AD=98=E9=96=A2=E4=BF=82=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ContextToCamelCaseService.test.ts | 25 ++++ .../service/ContextToCamelCaseService.ts | 2 +- .../Context/service/ContextUnbindService.ts | 8 +- .../Context/usecase/ContextBindUseCase.ts | 20 +-- .../service/AddScreenCaptureService.ts | 4 +- src/index.ts | 2 +- src/view/View.test.ts | 115 +++++++++++++++++- src/view/View.ts | 29 ++++- src/view/ViewModel.test.ts | 114 +++++++++++++++-- src/view/ViewModel.ts | 26 +--- 10 files changed, 295 insertions(+), 50 deletions(-) diff --git a/src/application/Context/service/ContextToCamelCaseService.test.ts b/src/application/Context/service/ContextToCamelCaseService.test.ts index dd030d5..9252358 100644 --- a/src/application/Context/service/ContextToCamelCaseService.test.ts +++ b/src/application/Context/service/ContextToCamelCaseService.test.ts @@ -17,4 +17,29 @@ describe("ContextToCamelCaseService Test", () => { expect(execute("game/list/page")).toBe("GameListPage"); }); + + it("execute test case4 - hyphen separator", () => + { + expect(execute("user-profile")).toBe("UserProfile"); + }); + + it("execute test case5 - underscore separator", () => + { + expect(execute("user_settings")).toBe("UserSettings"); + }); + + it("execute test case6 - mixed separators", () => + { + expect(execute("game/user-profile_page")).toBe("GameUserProfilePage"); + }); + + it("execute test case7 - multiple hyphens", () => + { + expect(execute("my-awesome-component")).toBe("MyAwesomeComponent"); + }); + + it("execute test case8 - multiple underscores", () => + { + expect(execute("my_awesome_component")).toBe("MyAwesomeComponent"); + }); }); \ No newline at end of file diff --git a/src/application/Context/service/ContextToCamelCaseService.ts b/src/application/Context/service/ContextToCamelCaseService.ts index fbc423f..b5234c3 100644 --- a/src/application/Context/service/ContextToCamelCaseService.ts +++ b/src/application/Context/service/ContextToCamelCaseService.ts @@ -9,7 +9,7 @@ */ export const execute = (name: string): string => { - const names: string[] = name.split("/"); + const names: string[] = name.split(/-|\/|_/); let viewName: string = ""; for (let idx: number = 0; names.length > idx; ++idx) { diff --git a/src/application/Context/service/ContextUnbindService.ts b/src/application/Context/service/ContextUnbindService.ts index 9b21385..bff3667 100644 --- a/src/application/Context/service/ContextUnbindService.ts +++ b/src/application/Context/service/ContextUnbindService.ts @@ -15,12 +15,16 @@ export const execute = async (context: Context): Promise => return ; } - await context.viewModel.unbind(context.view); - const root = context.root; if (!root) { return ; } root.removeChild(context.view); + + /** + * ViewのonExitをコール + * Call View's onExit + */ + await context.view.onExit(); }; \ No newline at end of file diff --git a/src/application/Context/usecase/ContextBindUseCase.ts b/src/application/Context/usecase/ContextBindUseCase.ts index 942366b..d659494 100644 --- a/src/application/Context/usecase/ContextBindUseCase.ts +++ b/src/application/Context/usecase/ContextBindUseCase.ts @@ -27,20 +27,16 @@ export const execute = async (context: Context, name: string): Promise => } /** - * 遷移先のViewとViewModelを準備 - * Prepare the destination View and ViewModel + * 遷移先のViewとViewModelを起動、初期化処理を実行 + * Start the destination View and ViewModel, and execute the initialization process */ const ViewModelClass: any = packages.get(viewModelName) as unknown as ViewModel; context.viewModel = (new ViewModelClass() as ViewModel); + await context.viewModel.initialize(); 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); + context.view = (new ViewClass(context.viewModel) as View); + await context.view.initialize(); /** * rootの子要素を全て削除 @@ -57,5 +53,11 @@ export const execute = async (context: Context, name: string): Promise => */ root.addChildAt(context.view, 0); + /** + * 画面表示時の処理を実行 + * Execute processing when the screen is displayed + */ + await context.view.onEnter(); + return context.view; }; \ No newline at end of file diff --git a/src/domain/screen/Capture/service/AddScreenCaptureService.ts b/src/domain/screen/Capture/service/AddScreenCaptureService.ts index b21f341..74970ff 100644 --- a/src/domain/screen/Capture/service/AddScreenCaptureService.ts +++ b/src/domain/screen/Capture/service/AddScreenCaptureService.ts @@ -61,7 +61,9 @@ export const execute = async (): Promise => scale, 0, 0, scale, -rectangle.x * scale, -rectangle.y * scale - ) + ), + "bgColor": config.stage.options?.bgColor || null, + "bgAlpha": config.stage.options?.bgColor !== "" ? 1 : 0 }); const bitmapData = new BitmapData(canvas.width, canvas.height); 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/view/View.test.ts b/src/view/View.test.ts index a6dc762..647c8e2 100644 --- a/src/view/View.test.ts +++ b/src/view/View.test.ts @@ -3,8 +3,119 @@ import { describe, expect, it } from "vitest"; describe("View Test", () => { - it("initialize call test", () => { + it("should create an instance", () => + { const view = new View(); + expect(view).toBeInstanceOf(View); + }); + + it("should have initialize method", () => + { + const view = new View(); + expect(view.initialize).toBeDefined(); expect(typeof view.initialize).toBe("function"); }); -}); \ No newline at end of file + + it("should have onEnter method", () => + { + const view = new View(); + expect(view.onEnter).toBeDefined(); + expect(typeof view.onEnter).toBe("function"); + }); + + it("should have onExit method", () => + { + const view = new View(); + expect(view.onExit).toBeDefined(); + expect(typeof view.onExit).toBe("function"); + }); + + it("initialize should return Promise", async () => + { + const view = new View(); + const result = await view.initialize(); + expect(result).toBeUndefined(); + }); + + it("onEnter should return Promise", async () => + { + const view = new View(); + const result = await view.onEnter(); + expect(result).toBeUndefined(); + }); + + it("onExit should return Promise", async () => + { + const view = new View(); + const result = await view.onExit(); + expect(result).toBeUndefined(); + }); + + it("should be able to call lifecycle methods in sequence", async () => + { + const view = new View(); + await view.initialize(); + await view.onEnter(); + await view.onExit(); + expect(view).toBeInstanceOf(View); + }); + + it("should be extendable", () => + { + class CustomView extends View + { + async initialize(): Promise + { + // Custom initialization + } + + async onEnter(): Promise + { + // Custom onEnter + } + + async onExit(): Promise + { + // Custom onExit + } + } + + const customView = new CustomView(); + 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 CustomView extends View + { + async initialize(): Promise + { + initCalled = true; + } + + async onEnter(): Promise + { + enterCalled = true; + } + + async onExit(): Promise + { + exitCalled = true; + } + } + + const customView = new CustomView(); + await customView.initialize(); + await customView.onEnter(); + await customView.onExit(); + + expect(initCalled).toBe(true); + expect(enterCalled).toBe(true); + expect(exitCalled).toBe(true); + }); +}); diff --git a/src/view/View.ts b/src/view/View.ts index ca1cbc1..25d4ed2 100644 --- a/src/view/View.ts +++ b/src/view/View.ts @@ -5,7 +5,7 @@ import { Sprite } from "@next2d/display"; * It exists as a parent class of View and as an abstract class. * * @class - * @extends {MovieClip} + * @extends {Sprite} */ export class View extends Sprite { @@ -16,17 +16,38 @@ export class View extends Sprite constructor () { super(); - this.initialize(); } /** * @description constructorが起動した後にコールされます。 * Called after the constructor is invoked. * - * @return {void} + * @return {Promise} * @method * @abstract */ // eslint-disable-next-line no-empty-function - initialize (): void {} + async initialize (): Promise {} + + /** + * @description Viewが表示された際にコールされます。 + * Called when the View is displayed. + * + * @return {Promise} + * @method + * @public + */ + // eslint-disable-next-line no-empty-function + async onEnter (): Promise {} + + /** + * @description Viewが非表示になった際にコールされます。 + * Called when the View is hidden. + * + * @return {Promise} + * @method + * @public + */ + // eslint-disable-next-line no-empty-function + async onExit (): Promise {} } diff --git a/src/view/ViewModel.test.ts b/src/view/ViewModel.test.ts index 6a4a130..31426c9 100644 --- a/src/view/ViewModel.test.ts +++ b/src/view/ViewModel.test.ts @@ -1,20 +1,118 @@ -import { View } from "./View"; import { ViewModel } from "./ViewModel"; import { describe, expect, it } from "vitest"; describe("ViewModel Test", () => { - it("bind call test", async () => + it("should create an instance", () => { - const view = new View(); const viewModel = new ViewModel(); - expect(await viewModel.bind(view)).toBe(undefined); + expect(viewModel).toBeInstanceOf(ViewModel); }); - it("unbind call test", async () => + it("should have initialize method", () => { - const view = new View(); const viewModel = new ViewModel(); - expect(await viewModel.unbind(view)).toBe(view); + expect(viewModel.initialize).toBeDefined(); + expect(typeof viewModel.initialize).toBe("function"); }); -}); \ No newline at end of file + + it("initialize should return Promise", async () => + { + const viewModel = new ViewModel(); + const result = await viewModel.initialize(); + expect(result).toBeUndefined(); + }); + + it("should be able to call initialize multiple times", async () => + { + const viewModel = new ViewModel(); + 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); + }); +}); diff --git a/src/view/ViewModel.ts b/src/view/ViewModel.ts index e63e51e..5abc6df 100644 --- a/src/view/ViewModel.ts +++ b/src/view/ViewModel.ts @@ -1,5 +1,3 @@ -import type { View } from "./View"; - /** * @description ViewModelの親クラス、抽象クラスとして存在しています。 * It exists as a parent class of ViewModel and as an abstract class. @@ -9,29 +7,13 @@ import type { View } from "./View"; export 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; - } + // eslint-disable-next-line no-empty-function + async initialize (): Promise {} } \ No newline at end of file From f4425145a2ad7d61712edb51b180ca64b501d689 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 25 Nov 2025 23:12:10 +0900 Subject: [PATCH 04/28] #155 update packages --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5b10543..6ece1e0 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,16 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.46.4", - "@typescript-eslint/parser": "^8.46.4", - "@vitest/web-worker": "^4.0.9", + "@typescript-eslint/eslint-plugin": "^8.48.0", + "@typescript-eslint/parser": "^8.48.0", + "@vitest/web-worker": "^4.0.14", "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^16.5.0", "jsdom": "^27.2.0", "typescript": "^5.9.3", - "vite": "^7.2.2", - "vitest": "^4.0.9", + "vite": "^7.2.4", + "vitest": "^4.0.14", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { From 897779a258b529417b63250c3a10b9bc85ef0249 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 2 Dec 2025 08:45:44 +0900 Subject: [PATCH 05/28] #155 update github actions --- .github/workflows/integration.yml | 19 ++- .github/workflows/lint.yml | 16 +- .github/workflows/publish.yml | 7 +- Flowchart.xml | 233 ++++++++++++++++++++++++++++++ README.md | 60 +++++++- 5 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 Flowchart.xml 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..0ab05ff 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,11 +12,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: "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 diff --git a/Flowchart.xml b/Flowchart.xml new file mode 100644 index 0000000..74262cf --- /dev/null +++ b/Flowchart.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index a995ff4..6381f78 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,64 @@ cd app-name npm start ``` -## Flowchart -![Flowchart](./Framework_Flowchart.svg) +## Flowchart + +```mermaid +graph TD + User([User]) -->|Request| GotoView[gotoView Path] + + GotoView --> LoadingCheck{use loading?
Default: true} + + LoadingCheck -->|YES| LoadingStart[Start Loading] + LoadingCheck -->|NO| OnExit + LoadingStart --> OnExit + + OnExit[Previous View: onExit] --> RemoveResponse[Remove Previous Response Data] + + RemoveResponse --> 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] --> ViewModelInit[ViewModel: initialize] + + ViewModelInit --> ViewInit[View: initialize] + ViewInit --> AddToStage[Add View to Stage] + AddToStage --> OnEnter[View: onEnter] + + OnEnter --> CallbackCheck{use callback?
Default: empty} + + CallbackCheck -->|YES| CallbackStart[Start Callback] + CallbackCheck -->|NO| LoadingEndCheck + CallbackStart --> LoadingEndCheck + + LoadingEndCheck{use loading?
Default: true} + + LoadingEndCheck -->|YES| LoadingEnd[End Loading] + LoadingEndCheck -->|NO| StartDrawing + LoadingEnd --> 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. From da66bec2dbf28f27dae6e9264dd24abb7f091368 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 2 Dec 2025 08:46:59 +0900 Subject: [PATCH 06/28] =?UTF-8?q?#155=20=E7=94=BB=E5=83=8F=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Flowchart.xml | 233 ---------------------------------------- Framework_Flowchart.svg | 4 - 2 files changed, 237 deletions(-) delete mode 100644 Flowchart.xml delete mode 100644 Framework_Flowchart.svg diff --git a/Flowchart.xml b/Flowchart.xml deleted file mode 100644 index 74262cf..0000000 --- a/Flowchart.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 From 1f087f049986c3d8534085e64d77cd3acd387964 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 2 Dec 2025 08:57:37 +0900 Subject: [PATCH 07/28] #155 update packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6ece1e0..ec2afe0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "url": "git+https://github.com/Next2D/Framework.git" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "@typescript-eslint/eslint-plugin": "^8.48.0", @@ -42,7 +42,7 @@ "globals": "^16.5.0", "jsdom": "^27.2.0", "typescript": "^5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.6", "vitest": "^4.0.14", "vitest-webgl-canvas-mock": "^1.1.0" }, From 6dec011403ddc2f20e3e983118348425c61178f5 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 2 Dec 2025 22:58:26 +0900 Subject: [PATCH 08/28] =?UTF-8?q?#155=20onExit=E3=82=92removeChild?= =?UTF-8?q?=E3=81=AE=E5=89=8D=E3=81=AB=E5=AE=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- .../Context/service/ContextUnbindService.ts | 17 ++++++++++------- vite.config.ts | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6381f78..6c6a3ce 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,8 @@ graph TD LoadingCheck -->|NO| OnExit LoadingStart --> OnExit - OnExit[Previous View: onExit] --> RemoveResponse[Remove Previous Response Data] + OnExit[Previous View: onExit] --> RemoveViewFromStage[Remove Previous View from Stage] + RemoveViewFromStage --> RemoveResponse[Remove Previous Response Data] RemoveResponse --> RequestType[Request Type] diff --git a/src/application/Context/service/ContextUnbindService.ts b/src/application/Context/service/ContextUnbindService.ts index bff3667..4f799f2 100644 --- a/src/application/Context/service/ContextUnbindService.ts +++ b/src/application/Context/service/ContextUnbindService.ts @@ -15,16 +15,19 @@ export const execute = async (context: Context): Promise => return ; } - const root = context.root; - if (!root) { - return ; - } - - root.removeChild(context.view); - /** * 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); }; \ No newline at end of file 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 From c75cd663a0c27adc9ee2bb9a63c45ac2380e9bf1 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 2 Dec 2025 22:58:53 +0900 Subject: [PATCH 09/28] #155 update packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ec2afe0..1fed544 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.48.0", - "@typescript-eslint/parser": "^8.48.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vitest/web-worker": "^4.0.14", "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", From 47367712793cf89b8dc3f04a14a3fd9463b911c3 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 22:07:17 +0900 Subject: [PATCH 10/28] =?UTF-8?q?#155=20=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationInitializeService.ts | 9 +- ...ApplicationParseQueryStringService.test.ts | 35 +++++++ .../ApplicationParseQueryStringService.ts | 19 ++++ .../ApplicationQueryStringParserService.ts | 28 ++---- .../usecase/ApplicationGotoViewUseCase.ts | 16 ++-- .../Application/variable/PopstateQueue.ts | 22 +++++ .../ConfigParserRequestsPropertyService.ts | 11 +-- .../service/ContextToCamelCaseService.ts | 15 +-- .../service/ContextUnbindService.test.ts | 72 ++++++++++++-- .../usecase/ContextBindUseCase.test.ts | 89 +++++++++++++++-- .../callback/service/CallbackService.ts | 4 +- .../Loading/service/LoadingEndService.ts | 28 +----- .../service/LoadingGetInstanceService.test.ts | 96 +++++++++++++++++++ .../service/LoadingGetInstanceService.ts | 41 ++++++++ .../Loading/service/LoadingStartService.ts | 28 +----- .../service/AddScreenCaptureService.ts | 22 ++--- src/domain/screen/Capture/variable/CacheX.ts | 19 ++++ src/domain/screen/Capture/variable/CacheY.ts | 19 ++++ .../repository/RequestContentRepository.ts | 36 ++----- .../repository/RequestCustomRepository.ts | 36 ++----- .../repository/RequestJsonRepository.ts | 35 ++----- .../service/RequestCacheCheckService.test.ts | 58 +++++++++++ .../service/RequestCacheCheckService.ts | 34 +++++++ .../RequestResponseProcessService.test.ts | 47 +++++++++ .../service/RequestResponseProcessService.ts | 29 ++++++ .../Request/usecase/RequestUseCase.ts | 48 ++-------- .../Request/variable/RepositoryMap.ts | 16 ++++ 27 files changed, 646 insertions(+), 266 deletions(-) create mode 100644 src/application/Application/service/ApplicationParseQueryStringService.test.ts create mode 100644 src/application/Application/service/ApplicationParseQueryStringService.ts create mode 100644 src/application/Application/variable/PopstateQueue.ts create mode 100644 src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts create mode 100644 src/domain/loading/Loading/service/LoadingGetInstanceService.ts create mode 100644 src/domain/screen/Capture/variable/CacheX.ts create mode 100644 src/domain/screen/Capture/variable/CacheY.ts create mode 100644 src/infrastructure/Request/service/RequestCacheCheckService.test.ts create mode 100644 src/infrastructure/Request/service/RequestCacheCheckService.ts create mode 100644 src/infrastructure/Request/service/RequestResponseProcessService.test.ts create mode 100644 src/infrastructure/Request/service/RequestResponseProcessService.ts create mode 100644 src/infrastructure/Request/variable/RepositoryMap.ts diff --git a/src/application/Application/service/ApplicationInitializeService.ts b/src/application/Application/service/ApplicationInitializeService.ts index 7d34b81..79220ab 100644 --- a/src/application/Application/service/ApplicationInitializeService.ts +++ b/src/application/Application/service/ApplicationInitializeService.ts @@ -3,12 +3,7 @@ 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 { popstateQueue, setPopstateQueue } from "../variable/PopstateQueue"; /** * @description アプリケーションの初期化処理を実行します @@ -38,7 +33,7 @@ export const execute = ( window.addEventListener("popstate", async (): Promise => { application.popstate = true; - $popstateQueue = $popstateQueue.then(() => application.gotoView()); + setPopstateQueue(popstateQueue.then(() => application.gotoView())); }); } diff --git a/src/application/Application/service/ApplicationParseQueryStringService.test.ts b/src/application/Application/service/ApplicationParseQueryStringService.test.ts new file mode 100644 index 0000000..c58de20 --- /dev/null +++ b/src/application/Application/service/ApplicationParseQueryStringService.test.ts @@ -0,0 +1,35 @@ +import { execute } from "./ApplicationParseQueryStringService"; +import { query } from "../../variable/Query"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("ApplicationParseQueryStringService Test", () => +{ + beforeEach(() => + { + query.clear(); + }); + + it("should parse query string without leading ?", () => + { + execute("key1=value1&key2=value2"); + + expect(query.get("key1")).toBe("value1"); + expect(query.get("key2")).toBe("value2"); + }); + + it("should parse query string with leading ?", () => + { + execute("?key1=value1&key2=value2"); + + expect(query.get("key1")).toBe("value1"); + expect(query.get("key2")).toBe("value2"); + }); + + it("should parse single parameter", () => + { + execute("single=param"); + + expect(query.get("single")).toBe("param"); + expect(query.size).toBe(1); + }); +}); diff --git a/src/application/Application/service/ApplicationParseQueryStringService.ts b/src/application/Application/service/ApplicationParseQueryStringService.ts new file mode 100644 index 0000000..2d51410 --- /dev/null +++ b/src/application/Application/service/ApplicationParseQueryStringService.ts @@ -0,0 +1,19 @@ +import { query } from "../../variable/Query"; + +/** + * @description QueryStringをパースしてqueryマップに登録 + * Parse QueryString and register to query map + * + * @param {string} queryString + * @return {void} + * @method + * @protected + */ +export const execute = (queryString: string): void => +{ + const parameters = queryString.slice(queryString.startsWith("?") ? 1 : 0).split("&"); + for (const parameter of parameters) { + const [key, value] = parameter.split("="); + query.set(key, value); + } +}; diff --git a/src/application/Application/service/ApplicationQueryStringParserService.ts b/src/application/Application/service/ApplicationQueryStringParserService.ts index 265ae6e..304cbf7 100644 --- a/src/application/Application/service/ApplicationQueryStringParserService.ts +++ b/src/application/Application/service/ApplicationQueryStringParserService.ts @@ -1,6 +1,7 @@ import type { IQueryObject } from "../../../interface/IQueryObject"; import { $getConfig } from "../../variable/Config"; import { query } from "../../variable/Query"; +import { execute as applicationParseQueryStringService } from "./ApplicationParseQueryStringService"; /** * @description 指定されたQueryStringか、URLのQueryStringをquery mapに登録 @@ -28,11 +29,7 @@ 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]); - } + applicationParseQueryStringService(queryString); } const config = $getConfig(); @@ -61,25 +58,18 @@ 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]); - } + if (name.includes("?")) { + const [baseName, qs] = name.split("?"); + name = baseName; + queryString = `?${qs}`; + applicationParseQueryStringService(qs); } - if (name.slice(0, 1) === ".") { + if (name.startsWith(".")) { name = name.split("/").slice(1).join("/") || defaultTop; } - if (name.indexOf("@") > -1) { + if (name.includes("@")) { name = name.replace("@", ""); } diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts index cf1a7d3..4cfcb22 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts @@ -24,7 +24,9 @@ import { execute as callbackService } from "../../../domain/callback/service/Cal 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 @@ -86,17 +88,13 @@ export const execute = async (application: Application, name: string = ""): Prom * レスポンス情報をマップに登録 * Response information is registered on the map */ - for (let idx = 0; idx < responses.length; ++idx) { - - const object = responses[idx]; - if (!object.name) { - continue; + for (const object of responses) { + if (object.name) { + response.set(object.name, object.response); } - - response.set(object.name, object.response); } - if (config.loading) { + if (hasLoading) { /** * ローディング表示を終了 * End loading display diff --git a/src/application/Application/variable/PopstateQueue.ts b/src/application/Application/variable/PopstateQueue.ts new file mode 100644 index 0000000..903266f --- /dev/null +++ b/src/application/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/Config/service/ConfigParserRequestsPropertyService.ts b/src/application/Config/service/ConfigParserRequestsPropertyService.ts index aa42ffc..b2a267d 100644 --- a/src/application/Config/service/ConfigParserRequestsPropertyService.ts +++ b/src/application/Config/service/ConfigParserRequestsPropertyService.ts @@ -15,21 +15,18 @@ 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 request: IRequest = routing.requests[idx]; + const requests: IRequest[] = []; + for (const request of routing.requests) { if (request.type !== "cluster") { requests.push(request); diff --git a/src/application/Context/service/ContextToCamelCaseService.ts b/src/application/Context/service/ContextToCamelCaseService.ts index b5234c3..ee835f5 100644 --- a/src/application/Context/service/ContextToCamelCaseService.ts +++ b/src/application/Context/service/ContextToCamelCaseService.ts @@ -9,15 +9,8 @@ */ 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; + return name + .split(/-|\/|_/) + .map((word: string): string => word.charAt(0).toUpperCase() + word.slice(1)) + .join(""); }; \ No newline at end of file diff --git a/src/application/Context/service/ContextUnbindService.test.ts b/src/application/Context/service/ContextUnbindService.test.ts index 9d4cda7..28357e8 100644 --- a/src/application/Context/service/ContextUnbindService.test.ts +++ b/src/application/Context/service/ContextUnbindService.test.ts @@ -1,10 +1,10 @@ -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 { ViewModel } from "../../../view/ViewModel"; import { describe, expect, it } from "vitest"; describe("ContextUnbindService Test", () => @@ -26,22 +26,74 @@ describe("ContextUnbindService Test", () => const context = new Context(root); $setContext(context); - let state = "none"; - context.view = new View(); - root.addChild(context.view); - context.viewModel = { - "unbind": (view: View) => + let onExitCalled = false; + + class TestView extends View + { + async onExit () { - state = "unbind"; + onExitCalled = true; } - } as ViewModel; + } + + context.view = new TestView(); + root.addChild(context.view); + context.viewModel = new ViewModel(); - expect(state).toBe("none"); + expect(onExitCalled).toBe(false); expect(root.numChildren).toBe(1); await execute(context); - expect(state).toBe("unbind"); + 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); + + context.view = null; + context.viewModel = new ViewModel(); + + await execute(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); + + context.view = new View(); + context.viewModel = null; + + await execute(context); + expect(root.numChildren).toBe(0); }); }); \ No newline at end of file diff --git a/src/application/Context/usecase/ContextBindUseCase.test.ts b/src/application/Context/usecase/ContextBindUseCase.test.ts index 162daf3..b85b6a5 100644 --- a/src/application/Context/usecase/ContextBindUseCase.test.ts +++ b/src/application/Context/usecase/ContextBindUseCase.test.ts @@ -23,29 +23,106 @@ describe("ContextBindUseCase Test", () => } }); - let state = "none"; + let viewModelInitialized = false; + let viewInitialized = false; + let viewEntered = false; + class TestViewModel extends ViewModel { - async bind () + async initialize () + { + viewModelInitialized = true; + } + } + + class TestView extends View + { + async initialize () + { + viewInitialized = true; + } + + async onEnter () { - state = "bind"; + viewEntered = true; } } packages.clear(); - packages.set("TestView", View); + packages.set("TestView", TestView); packages.set("TestViewModel", TestViewModel); const root = new MovieClip(); const context = new Context(root); $setContext(context); - expect(state).toBe("none"); + expect(viewModelInitialized).toBe(false); + expect(viewInitialized).toBe(false); + expect(viewEntered).toBe(false); expect(root.numChildren).toBe(0); await execute(context, "test"); - expect(state).toBe("bind"); + expect(viewModelInitialized).toBe(true); + expect(viewInitialized).toBe(true); + expect(viewEntered).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(execute(context, "notfound")).rejects.toThrow("not found view or viewMode."); + }); + + 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 {} + class TestView extends View {} + + 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 execute(context, "test"); + expect(root.numChildren).toBe(1); + expect(context.view).toBeInstanceOf(TestView); }); }); \ No newline at end of file diff --git a/src/domain/callback/service/CallbackService.ts b/src/domain/callback/service/CallbackService.ts index 4cafd77..0673a5d 100644 --- a/src/domain/callback/service/CallbackService.ts +++ b/src/domain/callback/service/CallbackService.ts @@ -23,9 +23,7 @@ export const execute = async ( ? [callback] : callback; - for (let idx = 0; idx < callbacks.length; ++idx) { - - const name = callbacks[idx]; + for (const name of callbacks) { if (!packages.has(name)) { continue; } diff --git a/src/domain/loading/Loading/service/LoadingEndService.ts b/src/domain/loading/Loading/service/LoadingEndService.ts index 921e02a..5892a53 100644 --- a/src/domain/loading/Loading/service/LoadingEndService.ts +++ b/src/domain/loading/Loading/service/LoadingEndService.ts @@ -1,10 +1,4 @@ -import { DefaultLoader } from "../../DefaultLoader"; -import { $getConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; +import { execute as loadingGetInstanceService } from "./LoadingGetInstanceService"; /** * @description ローダーのアニメーションを終了 @@ -16,25 +10,9 @@ import { */ 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(); + const instance = loadingGetInstanceService(); if (!instance) { - - const LoaderClass: any = packages.has(name) - ? packages.get(name) - : DefaultLoader; - - instance = new LoaderClass(); - $setInstance(instance); + return ; } await instance.end(); diff --git a/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts b/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts new file mode 100644 index 0000000..fbe3cd9 --- /dev/null +++ b/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts @@ -0,0 +1,96 @@ +import { execute } from "./LoadingGetInstanceService"; +import { $setConfig } from "../../../../application/variable/Config"; +import { $setPackages } from "../../../../application/variable/Packages"; +import { $setInstance } from "../../Loading"; +import { DefaultLoader } from "../../DefaultLoader"; +import { describe, expect, it, beforeEach } from "vitest"; + +describe("LoadingGetInstanceService Test", () => +{ + beforeEach(() => + { + $setInstance(null as any); + }); + + it("should return null if config has no loading", () => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + + const result = execute(); + + 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 = execute(); + + 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 = execute(); + + 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 = execute(); + const result2 = execute(); + + expect(result1).toBe(result2); + }); +}); diff --git a/src/domain/loading/Loading/service/LoadingGetInstanceService.ts b/src/domain/loading/Loading/service/LoadingGetInstanceService.ts new file mode 100644 index 0000000..6be3508 --- /dev/null +++ b/src/domain/loading/Loading/service/LoadingGetInstanceService.ts @@ -0,0 +1,41 @@ +import type { ILoading } from "../../../../interface/ILoading"; +import { DefaultLoader } from "../../DefaultLoader"; +import { $getConfig } from "../../../../application/variable/Config"; +import { packages } from "../../../../application/variable/Packages"; +import { + $getInstance, + $setInstance +} from "../../Loading"; + +/** + * @description ローダーのインスタンスを取得または作成 + * Get or create loader instance + * + * @return {ILoading | null} + * @method + * @protected + */ +export const execute = (): 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: any = packages.has(name) + ? packages.get(name) + : DefaultLoader; + + instance = new LoaderClass(); + $setInstance(instance); + } + + return instance; +}; diff --git a/src/domain/loading/Loading/service/LoadingStartService.ts b/src/domain/loading/Loading/service/LoadingStartService.ts index fab5f17..2e71c02 100644 --- a/src/domain/loading/Loading/service/LoadingStartService.ts +++ b/src/domain/loading/Loading/service/LoadingStartService.ts @@ -1,10 +1,4 @@ -import { DefaultLoader } from "../../DefaultLoader"; -import { $getConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; +import { execute as loadingGetInstanceService } from "./LoadingGetInstanceService"; /** * @description ローダーのアニメーションを実行 @@ -16,25 +10,9 @@ import { */ 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(); + const instance = loadingGetInstanceService(); if (!instance) { - - const LoaderClass: any = packages.has(name) - ? packages.get(name) - : DefaultLoader; - - instance = new LoaderClass(); - $setInstance(instance); + return ; } await instance.start(); diff --git a/src/domain/screen/Capture/service/AddScreenCaptureService.ts b/src/domain/screen/Capture/service/AddScreenCaptureService.ts index 74970ff..54bd28e 100644 --- a/src/domain/screen/Capture/service/AddScreenCaptureService.ts +++ b/src/domain/screen/Capture/service/AddScreenCaptureService.ts @@ -2,24 +2,14 @@ import { $getConfig } from "../../../../application/variable/Config"; import { $getContext } from "../../../../application/variable/Context"; import { Matrix } from "@next2d/geom"; import { shape } from "../../Capture"; +import { cacheX, setCacheX } from "../variable/CacheX"; +import { cacheY, setCacheY } from "../variable/CacheY"; 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 @@ -92,14 +82,14 @@ export const execute = async (): Promise => .endFill(); } - if (tx && $cacheX !== tx) { - $cacheX = tx; + if (tx && cacheX !== tx) { + setCacheX(tx); shape.width = stage.rendererWidth / scale; shape.x = -tx / scale; } - if (ty && $cacheY !== ty) { - $cacheY = ty; + if (ty && cacheY !== ty) { + setCacheY(ty); shape.height = stage.rendererHeight / scale; shape.y = -ty / scale; } diff --git a/src/domain/screen/Capture/variable/CacheX.ts b/src/domain/screen/Capture/variable/CacheX.ts new file mode 100644 index 0000000..9939873 --- /dev/null +++ b/src/domain/screen/Capture/variable/CacheX.ts @@ -0,0 +1,19 @@ +/** + * @type {number} + * @protected + */ +export let cacheX: number = 0; + +/** + * @description キャッシュXを設定 + * Set cache X + * + * @param {number} value + * @return {void} + * @method + * @protected + */ +export const setCacheX = (value: number): void => +{ + cacheX = value; +}; diff --git a/src/domain/screen/Capture/variable/CacheY.ts b/src/domain/screen/Capture/variable/CacheY.ts new file mode 100644 index 0000000..bb688a4 --- /dev/null +++ b/src/domain/screen/Capture/variable/CacheY.ts @@ -0,0 +1,19 @@ +/** + * @type {number} + * @protected + */ +export let cacheY: number = 0; + +/** + * @description キャッシュYを設定 + * Set cache Y + * + * @param {number} value + * @return {void} + * @method + * @protected + */ +export const setCacheY = (value: number): void => +{ + cacheY = value; +}; diff --git a/src/infrastructure/Request/repository/RequestContentRepository.ts b/src/infrastructure/Request/repository/RequestContentRepository.ts index 388e441..8760936 100644 --- a/src/infrastructure/Request/repository/RequestContentRepository.ts +++ b/src/infrastructure/Request/repository/RequestContentRepository.ts @@ -1,10 +1,9 @@ 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"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; /** * @description 指定先のJSONを非同期で取得 @@ -21,24 +20,9 @@ export const execute = async (request_object: IRequest): Promise => 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 cachedResponse = await requestCacheCheckService(request_object); + if (cachedResponse) { + return cachedResponse; } const urlRequest = new URLRequest(request_object.path); @@ -95,13 +79,5 @@ export const execute = async (request_object: IRequest): Promise => } } - 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); + return requestResponseProcessService(request_object, content); }; \ No newline at end of file diff --git a/src/infrastructure/Request/repository/RequestCustomRepository.ts b/src/infrastructure/Request/repository/RequestCustomRepository.ts index 9fb5544..535e193 100644 --- a/src/infrastructure/Request/repository/RequestCustomRepository.ts +++ b/src/infrastructure/Request/repository/RequestCustomRepository.ts @@ -1,8 +1,7 @@ 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"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; /** * @description 指定先の外部データを非同期で取得 @@ -23,24 +22,9 @@ export const execute = async (request_object: IRequest): Promise => 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 cachedResponse = await requestCacheCheckService(request_object); + if (cachedResponse) { + return cachedResponse; } const className = request_object.class; @@ -53,13 +37,5 @@ export const execute = async (request_object: IRequest): Promise => ? 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); + return requestResponseProcessService(request_object, value); }; \ No newline at end of file diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.ts b/src/infrastructure/Request/repository/RequestJsonRepository.ts index 9c30745..6afda08 100644 --- a/src/infrastructure/Request/repository/RequestJsonRepository.ts +++ b/src/infrastructure/Request/repository/RequestJsonRepository.ts @@ -1,7 +1,6 @@ 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"; +import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; +import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; /** * @description 指定先のJSONを非同期で取得 @@ -18,23 +17,9 @@ export const execute = async (request_object: IRequest): Promise => 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 cachedResponse = await requestCacheCheckService(request_object); + if (cachedResponse) { + return cachedResponse; } const options: RequestInit = {}; @@ -59,13 +44,5 @@ export const execute = async (request_object: IRequest): Promise => 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); + return requestResponseProcessService(request_object, value); }; \ No newline at end of file diff --git a/src/infrastructure/Request/service/RequestCacheCheckService.test.ts b/src/infrastructure/Request/service/RequestCacheCheckService.test.ts new file mode 100644 index 0000000..50e4b55 --- /dev/null +++ b/src/infrastructure/Request/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/Request/service/RequestCacheCheckService.ts b/src/infrastructure/Request/service/RequestCacheCheckService.ts new file mode 100644 index 0000000..861889b --- /dev/null +++ b/src/infrastructure/Request/service/RequestCacheCheckService.ts @@ -0,0 +1,34 @@ +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 キャッシュをチェックし、存在すればキャッシュデータを返却 + * Check cache and return cached data if exists + * + * @param {IRequest} request_object + * @return {Promise} + * @method + * @public + */ +export const execute = async (request_object: IRequest): Promise => +{ + if (!request_object.cache || !request_object.name) { + return null; + } + + const name = request_object.name; + + if (!cache.size || !cache.has(name)) { + return null; + } + + const value: any = cache.get(name); + + if (request_object.callback) { + await callbackService(request_object.callback, value); + } + + return new ResponseDTO(name, value); +}; diff --git a/src/infrastructure/Request/service/RequestResponseProcessService.test.ts b/src/infrastructure/Request/service/RequestResponseProcessService.test.ts new file mode 100644 index 0000000..86be7f4 --- /dev/null +++ b/src/infrastructure/Request/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/Request/service/RequestResponseProcessService.ts b/src/infrastructure/Request/service/RequestResponseProcessService.ts new file mode 100644 index 0000000..ba22045 --- /dev/null +++ b/src/infrastructure/Request/service/RequestResponseProcessService.ts @@ -0,0 +1,29 @@ +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 レスポンスをキャッシュに保存し、コールバックを実行してDTOを返却 + * Save response to cache, execute callback and return DTO + * + * @param {IRequest} request_object + * @param {any} value + * @return {Promise} + * @method + * @public + */ +export const execute = async (request_object: IRequest, value: any): Promise => +{ + const name = request_object.name as string; + + if (request_object.cache) { + cache.set(name, value); + } + + if (request_object.callback) { + await callbackService(request_object.callback, value); + } + + return new ResponseDTO(name, value); +}; diff --git a/src/infrastructure/Request/usecase/RequestUseCase.ts b/src/infrastructure/Request/usecase/RequestUseCase.ts index ddd2978..ee6d85b 100644 --- a/src/infrastructure/Request/usecase/RequestUseCase.ts +++ b/src/infrastructure/Request/usecase/RequestUseCase.ts @@ -1,8 +1,6 @@ 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"; +import { repositoryMap } from "../variable/RepositoryMap"; /** * @description Routing設定で指定したタイプへリクエストを実行 @@ -17,44 +15,16 @@ 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; + for (const requestObject of requests) { + const repository = repositoryMap.get(requestObject.type); + if (!repository) { + continue; + } + const response = await repository(requestObject); + if (response) { + responses.push(response); } } diff --git a/src/infrastructure/Request/variable/RepositoryMap.ts b/src/infrastructure/Request/variable/RepositoryMap.ts new file mode 100644 index 0000000..c119bf5 --- /dev/null +++ b/src/infrastructure/Request/variable/RepositoryMap.ts @@ -0,0 +1,16 @@ +import { execute as requestContentRepository } from "../repository/RequestContentRepository"; +import { execute as requestCustomRepository } from "../repository/RequestCustomRepository"; +import { execute as requestJsonRepository } from "../repository/RequestJsonRepository"; + +/** + * @description リクエストタイプとリポジトリのマップ + * Map of request types and repositories + * + * @type {Map} + * @protected + */ +export const repositoryMap: Map = new Map([ + ["json", requestJsonRepository], + ["content", requestContentRepository], + ["custom", requestCustomRepository] +]); From 5df86957814e7e2128bb675149562b19fb806041 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 22:11:01 +0900 Subject: [PATCH 11/28] #155 update README --- README.md | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c6a3ce..8b5dc3e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Next2D Frameworkは、クリーンアーキテクチャー、ドメイン駆動 [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. -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. +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. @@ -33,6 +33,36 @@ Next2D框架是根据简洁架构、领域驱动开发、测试驱动开发和MV 它可以通过URL(SPA)实现场景管理,这在传统的Canvas/WebGL应用程序中是很难实现的,并且可以为每个场景进行UI开发和屏幕检查。 该系统能够实现高效的UI构建和维护。 此外,对测试驱动开发的强调支持高质量代码的开发,同时在各个层面进行测试,包括单元测试、集成测试和UI测试。 + +## Architecture + +``` +src/ +├── application/ # Application Layer +│ ├── Application.ts # Main application class +│ ├── Context.ts # View/ViewModel context management +│ ├── Application/ # Application services and use cases +│ ├── Config/ # Configuration services +│ ├── Context/ # Context services and use cases +│ ├── content/ # Content classes (MovieClip, Shape, TextField, Video) +│ └── variable/ # Application state (Config, Context, Cache, Packages, Query) +├── domain/ # Domain Layer +│ ├── callback/ # Callback services +│ ├── loading/ # Loading animation (DefaultLoader, Loading services) +│ └── screen/ # Screen capture services +├── infrastructure/ # Infrastructure Layer +│ ├── Request/ # HTTP request handling (JSON, Content, Custom) +│ └── Response/ # Response data management +├── interface/ # TypeScript interfaces +│ ├── IConfig.ts # Configuration interface +│ ├── IRequest.ts # Request interface +│ ├── IRouting.ts # Routing interface +│ └── ... +└── view/ # View Layer + ├── View.ts # Base View class + └── ViewModel.ts # Base ViewModel class +``` + ## Support [日本語] @@ -69,6 +99,112 @@ cd app-name npm start ``` +## API Reference + +### Application + +| Method | Description | +|--------|-------------| +| `initialize(config, packages)` | Initialize the application with config and packages | +| `run()` | Launch the Next2D application | +| `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 | +| `bind(name)` | Attach View to Stage | +| `unbind()` | Detach View from 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 From d5a9082a630162d6284ceafd544f9078f30c6482 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 06:48:58 +0900 Subject: [PATCH 12/28] =?UTF-8?q?#155=20=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 -- .../ApplicationParseQueryStringService.ts | 9 +-- .../ApplicationQueryStringParserService.ts | 14 ++--- .../usecase/ApplicationGotoViewUseCase.ts | 4 +- .../ConfigParserRequestsPropertyService.ts | 4 +- .../service/ContextToCamelCaseService.ts | 11 ++-- .../callback/service/CallbackService.ts | 4 +- .../helper/GetOrCreateJobHelper.ts | 36 ++++++++++++ .../service/DefaultLoaderStartService.ts | 56 ++----------------- .../repository/RequestContentRepository.ts | 27 ++------- .../repository/RequestJsonRepository.ts | 17 ++---- .../service/RequestNormalizeMethodService.ts | 29 ++++++++++ .../Request/usecase/RequestUseCase.ts | 4 +- .../usecase/ResponseRemoveVariableUseCase.ts | 4 +- 14 files changed, 111 insertions(+), 112 deletions(-) create mode 100644 src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts create mode 100644 src/infrastructure/Request/service/RequestNormalizeMethodService.ts diff --git a/README.md b/README.md index 8b5dc3e..81a1d28 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,6 @@ npm start | Method | Description | |--------|-------------| -| `initialize(config, packages)` | Initialize the application with config and packages | -| `run()` | Launch the Next2D application | | `gotoView(name?)` | Navigate to a View. If no argument, parses URL | | `getContext()` | Get the current Context | | `getResponse()` | Get the response data Map | @@ -133,8 +131,6 @@ npm start | `view` | Current View instance | | `viewModel` | Current ViewModel instance | | `root` | Root Sprite on Stage | -| `bind(name)` | Attach View to Stage | -| `unbind()` | Detach View from Stage | ### Exported Classes diff --git a/src/application/Application/service/ApplicationParseQueryStringService.ts b/src/application/Application/service/ApplicationParseQueryStringService.ts index 2d51410..2b2784b 100644 --- a/src/application/Application/service/ApplicationParseQueryStringService.ts +++ b/src/application/Application/service/ApplicationParseQueryStringService.ts @@ -11,9 +11,10 @@ import { query } from "../../variable/Query"; */ export const execute = (queryString: string): void => { - const parameters = queryString.slice(queryString.startsWith("?") ? 1 : 0).split("&"); - for (const parameter of parameters) { - const [key, value] = parameter.split("="); - query.set(key, value); + const startIndex = queryString.charAt(0) === "?" ? 1 : 0; + const parameters = queryString.slice(startIndex).split("&"); + for (let idx = 0; idx < parameters.length; ++idx) { + const pair = parameters[idx].split("="); + query.set(pair[0], pair[1]); } }; diff --git a/src/application/Application/service/ApplicationQueryStringParserService.ts b/src/application/Application/service/ApplicationQueryStringParserService.ts index 304cbf7..8aa043c 100644 --- a/src/application/Application/service/ApplicationQueryStringParserService.ts +++ b/src/application/Application/service/ApplicationQueryStringParserService.ts @@ -58,18 +58,18 @@ export const execute = (name: string = ""): IQueryObject => * 任意で設定したQueryStringを分解 * Decompose an arbitrarily set QueryString */ - if (name.includes("?")) { - const [baseName, qs] = name.split("?"); - name = baseName; - queryString = `?${qs}`; - applicationParseQueryStringService(qs); + if (name.indexOf("?") > -1) { + const idx = name.indexOf("?"); + queryString = name.slice(idx); + applicationParseQueryStringService(name.slice(idx + 1)); + name = name.slice(0, idx); } - if (name.startsWith(".")) { + if (name.charAt(0) === ".") { name = name.split("/").slice(1).join("/") || defaultTop; } - if (name.includes("@")) { + if (name.indexOf("@") > -1) { name = name.replace("@", ""); } diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts index 4cfcb22..c3d0fa4 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts @@ -88,7 +88,9 @@ export const execute = async (application: Application, name: string = ""): Prom * レスポンス情報をマップに登録 * Response information is registered on the map */ - for (const object of responses) { + for (let idx = 0; idx < responses.length; ++idx) { + + const object = responses[idx]; if (object.name) { response.set(object.name, object.response); } diff --git a/src/application/Config/service/ConfigParserRequestsPropertyService.ts b/src/application/Config/service/ConfigParserRequestsPropertyService.ts index b2a267d..45652f5 100644 --- a/src/application/Config/service/ConfigParserRequestsPropertyService.ts +++ b/src/application/Config/service/ConfigParserRequestsPropertyService.ts @@ -26,7 +26,9 @@ export const execute = (name: string): IRequest[] => } const requests: IRequest[] = []; - for (const request of routing.requests) { + for (let idx = 0; idx < routing.requests.length; ++idx) { + + const request = routing.requests[idx]; if (request.type !== "cluster") { requests.push(request); diff --git a/src/application/Context/service/ContextToCamelCaseService.ts b/src/application/Context/service/ContextToCamelCaseService.ts index ee835f5..634a156 100644 --- a/src/application/Context/service/ContextToCamelCaseService.ts +++ b/src/application/Context/service/ContextToCamelCaseService.ts @@ -9,8 +9,11 @@ */ export const execute = (name: string): string => { - return name - .split(/-|\/|_/) - .map((word: string): string => word.charAt(0).toUpperCase() + word.slice(1)) - .join(""); + const names = name.split(/-|\/|_/); + let result = ""; + for (let idx = 0; idx < names.length; ++idx) { + const word = names[idx]; + result += word.charAt(0).toUpperCase() + word.slice(1); + } + return result; }; \ No newline at end of file diff --git a/src/domain/callback/service/CallbackService.ts b/src/domain/callback/service/CallbackService.ts index 0673a5d..4cafd77 100644 --- a/src/domain/callback/service/CallbackService.ts +++ b/src/domain/callback/service/CallbackService.ts @@ -23,7 +23,9 @@ export const execute = async ( ? [callback] : callback; - for (const name of callbacks) { + for (let idx = 0; idx < callbacks.length; ++idx) { + + const name = callbacks[idx]; if (!packages.has(name)) { continue; } diff --git a/src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts b/src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts new file mode 100644 index 0000000..b2a9504 --- /dev/null +++ b/src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts @@ -0,0 +1,36 @@ +import type { Shape } from "@next2d/display"; +import type { Job } from "@next2d/ui"; +import { + Tween, + Easing +} from "@next2d/ui"; + +/** + * @description Tweenジョブを取得または作成 + * Get or create Tween job + * + * @param {Shape} shape + * @param {string} jobName + * @return {Job} + * @method + * @protected + */ +export const execute = (shape: Shape, jobName: string): Job => +{ + if (shape.hasLocalVariable(jobName)) { + const job = shape.getLocalVariable(jobName) as Job; + job.stop(); + return job; + } + + const job = 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(jobName, job); + return job; +}; diff --git a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts b/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts index 5cfe2c8..187782a 100644 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts +++ b/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts @@ -1,12 +1,8 @@ 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"; +import { execute as getOrCreateJobHelper } from "../helper/GetOrCreateJobHelper"; /** * @description ローダーのアニメーションを実行 @@ -29,6 +25,7 @@ export const execute = (default_loader: DefaultLoader): void => 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); @@ -44,53 +41,8 @@ export const execute = (default_loader: DefaultLoader): void => 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); - } + const reduceJob = getOrCreateJobHelper(shape, "reduceJob"); + const expandJob = getOrCreateJobHelper(shape, "expandJob"); reduceJob.nextJob = expandJob; expandJob.nextJob = reduceJob; diff --git a/src/infrastructure/Request/repository/RequestContentRepository.ts b/src/infrastructure/Request/repository/RequestContentRepository.ts index 8760936..e943c8e 100644 --- a/src/infrastructure/Request/repository/RequestContentRepository.ts +++ b/src/infrastructure/Request/repository/RequestContentRepository.ts @@ -4,6 +4,7 @@ import { URLRequest } from "@next2d/net"; import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; +import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; /** * @description 指定先のJSONを非同期で取得 @@ -26,27 +27,7 @@ export const execute = async (request_object: IRequest): Promise => } 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; - - } + urlRequest.method = requestNormalizeMethodService(request_object.method); if (request_object.headers) { for (const [name, value] of Object.entries(request_object.headers)) { @@ -73,8 +54,8 @@ export const execute = async (request_object: IRequest): Promise => if (loaderInfo.data) { const symbols: Map = loaderInfo.data.symbols; if (symbols.size) { - for (const name of symbols.keys()) { - loaderInfoMap.set(name, loaderInfo); + for (const symbolName of symbols.keys()) { + loaderInfoMap.set(symbolName, loaderInfo); } } } diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.ts b/src/infrastructure/Request/repository/RequestJsonRepository.ts index 6afda08..d46a533 100644 --- a/src/infrastructure/Request/repository/RequestJsonRepository.ts +++ b/src/infrastructure/Request/repository/RequestJsonRepository.ts @@ -1,6 +1,7 @@ import type { IRequest } from "../../../interface/IRequest"; import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; +import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; /** * @description 指定先のJSONを非同期で取得 @@ -22,19 +23,11 @@ export const execute = async (request_object: IRequest): Promise => return cachedResponse; } - const options: RequestInit = {}; + const method = requestNormalizeMethodService(request_object.method); + const options: RequestInit = { method }; - 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.body && (method === "POST" || method === "PUT")) { + options.body = JSON.stringify(request_object.body); } if (request_object.headers) { diff --git a/src/infrastructure/Request/service/RequestNormalizeMethodService.ts b/src/infrastructure/Request/service/RequestNormalizeMethodService.ts new file mode 100644 index 0000000..0eb7a92 --- /dev/null +++ b/src/infrastructure/Request/service/RequestNormalizeMethodService.ts @@ -0,0 +1,29 @@ +/** + * @description HTTPメソッドを正規化 + * Normalize HTTP method + * + * @param {string} [method] + * @return {string} + * @method + * @protected + */ +export const execute = (method?: string): string => +{ + 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/infrastructure/Request/usecase/RequestUseCase.ts b/src/infrastructure/Request/usecase/RequestUseCase.ts index ee6d85b..86bc173 100644 --- a/src/infrastructure/Request/usecase/RequestUseCase.ts +++ b/src/infrastructure/Request/usecase/RequestUseCase.ts @@ -16,7 +16,9 @@ export const execute = async (name: string): Promise => const responses: ResponseDTO[] = []; const requests = configParserRequestsPropertyService(name); - for (const requestObject of requests) { + for (let idx = 0; idx < requests.length; ++idx) { + + const requestObject = requests[idx]; const repository = repositoryMap.get(requestObject.type); if (!repository) { continue; diff --git a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts b/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts index a37ba69..7a546fe 100644 --- a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts +++ b/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts @@ -39,8 +39,8 @@ export const execute = (name: string): void => 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); } } } From 579792d90bd56fbb40edf510072137b9bf815e5d Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 07:00:49 +0900 Subject: [PATCH 13/28] =?UTF-8?q?#155=20context=E3=81=AEbind/unbind?= =?UTF-8?q?=E3=81=AF=E5=86=85=E9=83=A8=E3=81=A7=E3=81=AE=E3=81=BF=E5=88=A9?= =?UTF-8?q?=E7=94=A8=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicationGotoViewUseCase.test.ts | 14 ++++++--- .../usecase/ApplicationGotoViewUseCase.ts | 6 ++-- src/application/Context.ts | 29 ------------------- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts index bab84bb..f668625 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts +++ b/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts @@ -38,6 +38,14 @@ vi.mock("../../../domain/callback/service/CallbackService", () => ({ execute: vi.fn().mockResolvedValue(undefined) })); +vi.mock("../../Context/usecase/ContextBindUseCase", () => ({ + execute: vi.fn().mockResolvedValue(null) +})); + +vi.mock("../../Context/service/ContextUnbindService", () => ({ + execute: vi.fn().mockResolvedValue(undefined) +})); + describe("ApplicationGotoViewUseCase Test", () => { let mockApplication: any; @@ -55,8 +63,6 @@ describe("ApplicationGotoViewUseCase Test", () => root = new MovieClip(); mockContext = new Context(root); - mockContext.unbind = vi.fn().mockResolvedValue(undefined); - mockContext.bind = vi.fn().mockResolvedValue(null); $setContext(mockContext); $setConfig({ @@ -84,6 +90,7 @@ describe("ApplicationGotoViewUseCase Test", () => { const { execute: applicationQueryStringParserService } = await import("../service/ApplicationQueryStringParserService"); const { execute: requestUseCase } = await import("../../../infrastructure/Request/usecase/RequestUseCase"); + const { execute: contextBindUseCase } = await import("../../Context/usecase/ContextBindUseCase"); vi.mocked(applicationQueryStringParserService).mockReturnValue({ name: "home", @@ -94,9 +101,8 @@ describe("ApplicationGotoViewUseCase Test", () => await execute(mockApplication, "home"); - expect(mockContext.unbind).toHaveBeenCalled(); expect(mockApplication.currentName).toBe("home"); - expect(mockContext.bind).toHaveBeenCalledWith("home"); + expect(contextBindUseCase).toHaveBeenCalledWith(mockContext, "home"); }); it("execute test case2: navigation with response data", async () => diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts index c3d0fa4..8624523 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/Application/usecase/ApplicationGotoViewUseCase.ts @@ -10,6 +10,8 @@ import { execute as responseRemoveVariableUseCase } from "../../../infrastructur 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 { execute as contextUnbindService } from "../../Context/service/ContextUnbindService"; +import { execute as contextBindUseCase } from "../../Context/usecase/ContextBindUseCase"; /** * @description 指定されたパス、もしくはURLのクラスを起動 @@ -45,7 +47,7 @@ export const execute = async (application: Application, name: string = ""): Prom * Unbind the View and ViewModel of the current screen */ const context = $getContext(); - await context.unbind(); + await contextUnbindService(context); /** * 前の画面で取得したレスポンスデータを初期化 @@ -114,7 +116,7 @@ export const execute = async (application: Application, name: string = ""): Prom * ViewとViewModelを起動 * Start View and ViewModel */ - const view = await context.bind(application.currentName); + const view = await contextBindUseCase(context, application.currentName); /** * コールバック設定があれば実行 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 From 587d83459f4ba559ec86270b7646c733d8ec2487 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 15:03:13 +0900 Subject: [PATCH 14/28] =?UTF-8?q?#155=20=E5=9E=8B=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E3=80=81=E3=82=A2=E3=83=BC=E3=82=AD=E3=83=86=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=B4=A9=E5=A3=8A=E9=83=A8=E5=88=86=E3=82=92=E6=94=B9?= =?UTF-8?q?=E4=BF=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/ContextBindUseCase.test.ts | 18 +++- .../Context/usecase/ContextBindUseCase.ts | 9 +- src/application/variable/Packages.ts | 10 ++- .../callback/service/CallbackService.ts | 14 +-- .../service/LoadingGetInstanceService.ts | 5 +- .../repository/RequestContentRepository.ts | 9 +- .../repository/RequestCustomRepository.ts | 20 +++-- .../repository/RequestJsonRepository.ts | 5 +- .../service/RequestCacheCheckService.ts | 2 +- .../service/RequestNormalizeMethodService.ts | 4 +- .../service/RequestResponseProcessService.ts | 4 +- .../Response/dto/ResponseDTO.ts | 10 +-- src/interface/ICallback.ts | 17 ++++ src/interface/IConfig.ts | 50 +++++++++-- src/interface/IPackages.ts | 12 ++- src/interface/IRequest.ts | 73 +++++++++++++++- src/interface/IURLRequestMethod.ts | 1 + src/view/View.test.ts | 85 +++++++++++++------ src/view/View.ts | 25 ++++-- src/view/ViewModel.test.ts | 16 +++- src/view/ViewModel.ts | 6 +- 21 files changed, 303 insertions(+), 92 deletions(-) create mode 100644 src/interface/ICallback.ts create mode 100644 src/interface/IURLRequestMethod.ts diff --git a/src/application/Context/usecase/ContextBindUseCase.test.ts b/src/application/Context/usecase/ContextBindUseCase.test.ts index b85b6a5..6d85768 100644 --- a/src/application/Context/usecase/ContextBindUseCase.test.ts +++ b/src/application/Context/usecase/ContextBindUseCase.test.ts @@ -35,7 +35,7 @@ describe("ContextBindUseCase Test", () => } } - class TestView extends View + class TestView extends View { async initialize () { @@ -46,6 +46,10 @@ describe("ContextBindUseCase Test", () => { viewEntered = true; } + + async onExit () + { + } } packages.clear(); @@ -104,8 +108,16 @@ describe("ContextBindUseCase Test", () => } }); - class TestViewModel extends ViewModel {} - class TestView extends View {} + 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); diff --git a/src/application/Context/usecase/ContextBindUseCase.ts b/src/application/Context/usecase/ContextBindUseCase.ts index d659494..3e44022 100644 --- a/src/application/Context/usecase/ContextBindUseCase.ts +++ b/src/application/Context/usecase/ContextBindUseCase.ts @@ -1,6 +1,7 @@ import type { Context } from "../../Context"; import type { View } from "../../../view/View"; import type { ViewModel } from "../../../view/ViewModel"; +import type { Constructor } from "../../../interface/IPackages"; import { packages } from "../../variable/Packages"; import { execute as contextToCamelCaseService } from "../service/ContextToCamelCaseService"; @@ -30,12 +31,12 @@ export const execute = async (context: Context, name: string): Promise => * 遷移先のViewとViewModelを起動、初期化処理を実行 * Start the destination View and ViewModel, and execute the initialization process */ - const ViewModelClass: any = packages.get(viewModelName) as unknown as ViewModel; - context.viewModel = (new ViewModelClass() as ViewModel); + const ViewModelClass = packages.get(viewModelName) as Constructor; + context.viewModel = new ViewModelClass(); await context.viewModel.initialize(); - const ViewClass: any = packages.get(viewName) as unknown as View; - context.view = (new ViewClass(context.viewModel) as View); + const ViewClass = packages.get(viewName) as Constructor; + context.view = new ViewClass(context.viewModel); await context.view.initialize(); /** 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/domain/callback/service/CallbackService.ts b/src/domain/callback/service/CallbackService.ts index 4cafd77..8512349 100644 --- a/src/domain/callback/service/CallbackService.ts +++ b/src/domain/callback/service/CallbackService.ts @@ -1,3 +1,5 @@ +import type { ICallback } from "../../../interface/ICallback"; +import type { Constructor } from "../../../interface/IPackages"; import { packages } from "../../../application/variable/Packages"; /** @@ -5,18 +7,18 @@ import { packages } from "../../../application/variable/Packages"; * Execute 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/domain/loading/Loading/service/LoadingGetInstanceService.ts b/src/domain/loading/Loading/service/LoadingGetInstanceService.ts index 6be3508..e7e7285 100644 --- a/src/domain/loading/Loading/service/LoadingGetInstanceService.ts +++ b/src/domain/loading/Loading/service/LoadingGetInstanceService.ts @@ -1,4 +1,5 @@ import type { ILoading } from "../../../../interface/ILoading"; +import type { Constructor } from "../../../../interface/IPackages"; import { DefaultLoader } from "../../DefaultLoader"; import { $getConfig } from "../../../../application/variable/Config"; import { packages } from "../../../../application/variable/Packages"; @@ -29,8 +30,8 @@ export const execute = (): ILoading | null => let instance = $getInstance(); if (!instance) { - const LoaderClass: any = packages.has(name) - ? packages.get(name) + const LoaderClass = packages.has(name) + ? packages.get(name) as Constructor : DefaultLoader; instance = new LoaderClass(); diff --git a/src/infrastructure/Request/repository/RequestContentRepository.ts b/src/infrastructure/Request/repository/RequestContentRepository.ts index e943c8e..e156665 100644 --- a/src/infrastructure/Request/repository/RequestContentRepository.ts +++ b/src/infrastructure/Request/repository/RequestContentRepository.ts @@ -1,21 +1,22 @@ import type { IRequest } from "../../../interface/IRequest"; import { Loader } from "@next2d/display"; import { URLRequest } from "@next2d/net"; +import { ResponseDTO } from "../../Response/dto/ResponseDTO"; import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; /** - * @description 指定先のJSONを非同期で取得 - * Asynchronously obtain JSON of the specified destination + * @description 指定先のコンテンツを非同期で取得 + * Asynchronously obtain content of the specified destination * * @param {IRequest} request_object - * @return {Promise} + * @return {Promise} * @method * @public */ -export const execute = async (request_object: IRequest): Promise => +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."); diff --git a/src/infrastructure/Request/repository/RequestCustomRepository.ts b/src/infrastructure/Request/repository/RequestCustomRepository.ts index 535e193..319802e 100644 --- a/src/infrastructure/Request/repository/RequestCustomRepository.ts +++ b/src/infrastructure/Request/repository/RequestCustomRepository.ts @@ -1,18 +1,28 @@ import type { IRequest } from "../../../interface/IRequest"; +import type { Constructor } from "../../../interface/IPackages"; +import { ResponseDTO } from "../../Response/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} + * @return {Promise} * @method * @public */ -export const execute = async (request_object: IRequest): Promise => +export const execute = async (request_object: IRequest): Promise => { if (!request_object.class || !request_object.access @@ -32,10 +42,10 @@ export const execute = async (request_object: IRequest): Promise => throw new Error("package not found."); } - const CallbackClass: any = packages.get(className); + const CustomClass = packages.get(className) as Constructor & ICustomClass; const value = request_object.access === "static" - ? await CallbackClass[request_object.method]() - : await new CallbackClass()[request_object.method](); + ? await CustomClass[request_object.method]() + : await new CustomClass()[request_object.method](); return requestResponseProcessService(request_object, value); }; \ No newline at end of file diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.ts b/src/infrastructure/Request/repository/RequestJsonRepository.ts index d46a533..2ca6aa2 100644 --- a/src/infrastructure/Request/repository/RequestJsonRepository.ts +++ b/src/infrastructure/Request/repository/RequestJsonRepository.ts @@ -1,4 +1,5 @@ import type { IRequest } from "../../../interface/IRequest"; +import { ResponseDTO } from "../../Response/dto/ResponseDTO"; import { execute as requestCacheCheckService } from "../service/RequestCacheCheckService"; import { execute as requestResponseProcessService } from "../service/RequestResponseProcessService"; import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; @@ -8,11 +9,11 @@ import { execute as requestNormalizeMethodService } from "../service/RequestNorm * Asynchronously obtain JSON of the specified destination * * @param {IRequest} request_object - * @return {Promise} + * @return {Promise} * @method * @public */ -export const execute = async (request_object: IRequest): Promise => +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."); diff --git a/src/infrastructure/Request/service/RequestCacheCheckService.ts b/src/infrastructure/Request/service/RequestCacheCheckService.ts index 861889b..b5cff71 100644 --- a/src/infrastructure/Request/service/RequestCacheCheckService.ts +++ b/src/infrastructure/Request/service/RequestCacheCheckService.ts @@ -24,7 +24,7 @@ export const execute = async (request_object: IRequest): Promise +export const execute = (method?: string): IURLRequestMethod => { if (!method) { return "GET"; diff --git a/src/infrastructure/Request/service/RequestResponseProcessService.ts b/src/infrastructure/Request/service/RequestResponseProcessService.ts index ba22045..06cac3a 100644 --- a/src/infrastructure/Request/service/RequestResponseProcessService.ts +++ b/src/infrastructure/Request/service/RequestResponseProcessService.ts @@ -8,12 +8,12 @@ import { execute as callbackService } from "../../../domain/callback/service/Cal * Save response to cache, execute callback and return DTO * * @param {IRequest} request_object - * @param {any} value + * @param {unknown} value * @return {Promise} * @method * @public */ -export const execute = async (request_object: IRequest, value: any): Promise => +export const execute = async (request_object: IRequest, value: T): Promise> => { const name = request_object.name as string; diff --git a/src/infrastructure/Response/dto/ResponseDTO.ts b/src/infrastructure/Response/dto/ResponseDTO.ts index a625e67..9c0ce22 100644 --- a/src/infrastructure/Response/dto/ResponseDTO.ts +++ b/src/infrastructure/Response/dto/ResponseDTO.ts @@ -4,7 +4,7 @@ * * @class */ -export class ResponseDTO +export class ResponseDTO { /** * @description キャッシュのキー名 @@ -21,20 +21,20 @@ export class ResponseDTO * @description レスポンスデータ * response data * - * @return {*} + * @return {T} * @default null * @readonly * @public */ - public readonly response: any; + public readonly response: T; /** * @param {string} [name=""] - * @param {*} [response=null] + * @param {T} [response] * @constructor * @public */ - constructor (name: string = "", response: any = null) + constructor (name: string = "", response: T = null as T) { this.name = name; this.response = response; 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..6f55294 100644 --- a/src/interface/IConfig.ts +++ b/src/interface/IConfig.ts @@ -2,19 +2,53 @@ import type { IStage } from "./IStage"; import type { IRouting } from "./IRouting"; import type { IGotoView } from "./IGotoView"; -interface IBaseConfig { - [key: string]: any -} - -export interface IConfig extends IBaseConfig { +/** + * @description アプリケーション設定のインターフェース + * Application configuration interface + * + * @interface + */ +export interface IConfig { + /** + * @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/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..7715aaf 100644 --- a/src/interface/IRequest.ts +++ b/src/interface/IRequest.ts @@ -1,14 +1,79 @@ import type { IRequestType } from "./IRequestType"; +/** + * @description HTTPメソッドの型 + * HTTP method type + */ +export type IHttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; + +/** + * @description カスタムリクエストのアクセス方法 + * Access method for custom request + */ +export type IAccessType = "static" | "instance"; + +/** + * @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/IURLRequestMethod.ts b/src/interface/IURLRequestMethod.ts new file mode 100644 index 0000000..202e485 --- /dev/null +++ b/src/interface/IURLRequestMethod.ts @@ -0,0 +1 @@ +export type IURLRequestMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "POST" | "PUT"; \ No newline at end of file diff --git a/src/view/View.test.ts b/src/view/View.test.ts index 647c8e2..0fb5f1e 100644 --- a/src/view/View.test.ts +++ b/src/view/View.test.ts @@ -1,59 +1,86 @@ 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("should create an instance", () => { - const view = new View(); + const vm = new TestViewModel(); + const view = new TestView(vm); expect(view).toBeInstanceOf(View); }); it("should have initialize method", () => { - const view = new View(); + const vm = new TestViewModel(); + const view = new TestView(vm); expect(view.initialize).toBeDefined(); expect(typeof view.initialize).toBe("function"); }); it("should have onEnter method", () => { - const view = new View(); + 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 view = new View(); + 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 view = new View(); + const vm = new TestViewModel(); + const view = new TestView(vm); const result = await view.initialize(); expect(result).toBeUndefined(); }); it("onEnter should return Promise", async () => { - const view = new View(); + const vm = new TestViewModel(); + const view = new TestView(vm); const result = await view.onEnter(); expect(result).toBeUndefined(); }); it("onExit should return Promise", async () => { - const view = new View(); + 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 view = new View(); + const vm = new TestViewModel(); + const view = new TestView(vm); await view.initialize(); await view.onEnter(); await view.onExit(); @@ -62,25 +89,20 @@ describe("View Test", () => it("should be extendable", () => { - class CustomView extends View + class CustomViewModel extends ViewModel { - async initialize(): Promise - { - // Custom initialization - } - - async onEnter(): Promise - { - // Custom onEnter - } + async initialize(): Promise {} + } - async onExit(): Promise - { - // Custom onExit - } + class CustomView extends View + { + async initialize(): Promise {} + async onEnter(): Promise {} + async onExit(): Promise {} } - const customView = new CustomView(); + const vm = new CustomViewModel(); + const customView = new CustomView(vm); expect(customView).toBeInstanceOf(View); expect(customView).toBeInstanceOf(CustomView); }); @@ -91,7 +113,12 @@ describe("View Test", () => let enterCalled = false; let exitCalled = false; - class CustomView extends View + class CustomViewModel extends ViewModel + { + async initialize(): Promise {} + } + + class CustomView extends View { async initialize(): Promise { @@ -109,7 +136,8 @@ describe("View Test", () => } } - const customView = new CustomView(); + const vm = new CustomViewModel(); + const customView = new CustomView(vm); await customView.initialize(); await customView.onEnter(); await customView.onExit(); @@ -118,4 +146,11 @@ describe("View Test", () => 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 25d4ed2..0290537 100644 --- a/src/view/View.ts +++ b/src/view/View.ts @@ -1,4 +1,5 @@ import { Sprite } from "@next2d/display"; +import type { ViewModel } from "./ViewModel"; /** * @description Viewの親クラス、抽象クラスとして存在しています。 @@ -6,16 +7,27 @@ import { Sprite } from "@next2d/display"; * * @class * @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.vm = vm; } /** @@ -26,8 +38,7 @@ export class View extends Sprite * @method * @abstract */ - // eslint-disable-next-line no-empty-function - async initialize (): Promise {} + abstract initialize (): Promise; /** * @description Viewが表示された際にコールされます。 @@ -37,8 +48,7 @@ export class View extends Sprite * @method * @public */ - // eslint-disable-next-line no-empty-function - async onEnter (): Promise {} + abstract onEnter (): Promise; /** * @description Viewが非表示になった際にコールされます。 @@ -48,6 +58,5 @@ export class View extends Sprite * @method * @public */ - // eslint-disable-next-line no-empty-function - async onExit (): Promise {} + abstract onExit (): Promise; } diff --git a/src/view/ViewModel.test.ts b/src/view/ViewModel.test.ts index 31426c9..62aadc9 100644 --- a/src/view/ViewModel.test.ts +++ b/src/view/ViewModel.test.ts @@ -1,31 +1,39 @@ import { ViewModel } from "./ViewModel"; import { describe, expect, it } from "vitest"; +/** + * テスト用の具象ViewModelクラス + */ +class TestViewModel extends ViewModel +{ + async initialize(): Promise {} +} + describe("ViewModel Test", () => { it("should create an instance", () => { - const viewModel = new ViewModel(); + const viewModel = new TestViewModel(); expect(viewModel).toBeInstanceOf(ViewModel); }); it("should have initialize method", () => { - const viewModel = new ViewModel(); + const viewModel = new TestViewModel(); expect(viewModel.initialize).toBeDefined(); expect(typeof viewModel.initialize).toBe("function"); }); it("initialize should return Promise", async () => { - const viewModel = new ViewModel(); + 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 ViewModel(); + const viewModel = new TestViewModel(); await viewModel.initialize(); await viewModel.initialize(); expect(viewModel).toBeInstanceOf(ViewModel); diff --git a/src/view/ViewModel.ts b/src/view/ViewModel.ts index 5abc6df..48d3a04 100644 --- a/src/view/ViewModel.ts +++ b/src/view/ViewModel.ts @@ -3,8 +3,9 @@ * It exists as a parent class of ViewModel and as an abstract class. * * @class + * @abstract */ -export class ViewModel +export abstract class ViewModel { /** * @description constructorが起動した後にコールされます。 @@ -14,6 +15,5 @@ export class ViewModel * @method * @abstract */ - // eslint-disable-next-line no-empty-function - async initialize (): Promise {} + abstract initialize (): Promise; } \ No newline at end of file From 1155fd4efd2c5fcefa07fcc8f885af47018aa9f2 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 20:38:42 +0900 Subject: [PATCH 15/28] =?UTF-8?q?#155=20=E3=82=A2=E3=83=BC=E3=82=AD?= =?UTF-8?q?=E3=83=86=E3=82=AF=E3=83=88=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B?= =?UTF-8?q?=E3=81=A6=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D=E3=80=81?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E3=82=92?= =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 69 +++-- src/application/Application.ts | 12 +- ...ApplicationParseQueryStringService.test.ts | 35 --- .../ApplicationParseQueryStringService.ts | 20 -- .../service/ContextToCamelCaseService.test.ts | 45 --- .../service/ContextUnbindService.test.ts | 99 ------- .../Context/service/ContextUnbindService.ts | 33 --- .../usecase/ContextBindUseCase.test.ts | 140 ---------- .../Context/usecase/ContextBindUseCase.ts | 64 ----- .../QueryStringParserService.test.ts} | 12 +- .../QueryStringParserService.ts} | 20 +- .../RoutingRequestsParserService.test.ts} | 10 +- .../RoutingRequestsParserService.ts} | 8 +- .../ApplicationGotoViewUseCase.test.ts | 74 +++-- .../usecase/ApplicationGotoViewUseCase.ts | 43 ++- .../ApplicationInitializeUseCase.test.ts} | 24 +- .../ApplicationInitializeUseCase.ts} | 12 +- .../ContextRunUseCase.test.ts} | 10 +- .../ContextRunUseCase.ts} | 8 +- .../usecase/ExecuteCallbackUseCase.test.ts} | 35 ++- .../usecase/ExecuteCallbackUseCase.ts} | 12 +- .../variable/PopstateQueue.ts | 0 src/domain/entity/DefaultLoader.ts | 182 ++++++++++++ src/domain/loading/DefaultLoader.ts | 71 ----- .../helper/GetOrCreateJobHelper.ts | 36 --- .../service/DefaultLoaderEndService.test.ts | 53 ---- .../service/DefaultLoaderEndService.ts | 42 --- .../service/DefaultLoaderStartService.test.ts | 44 --- .../service/DefaultLoaderStartService.ts | 75 ----- .../DefaultLoadingInitializeService.test.ts | 19 -- .../DefaultLoadingInitializeService.ts | 18 -- .../Loading/service/LoadingEndService.test.ts | 40 --- .../Loading/service/LoadingEndService.ts | 19 -- .../service/LoadingGetInstanceService.test.ts | 96 ------- .../service/LoadingGetInstanceService.ts | 42 --- .../service/LoadingStartService.test.ts | 71 ----- .../Loading/service/LoadingStartService.ts | 19 -- src/domain/screen/Capture.ts | 13 - .../service/AddScreenCaptureService.test.ts | 40 --- .../service/AddScreenCaptureService.ts | 98 ------- .../service/DisposeCaptureService.test.ts | 40 --- .../Capture/service/DisposeCaptureService.ts | 31 --- src/domain/screen/Capture/variable/CacheX.ts | 19 -- src/domain/screen/Capture/variable/CacheY.ts | 19 -- src/domain/service/LoadingService.test.ts | 211 ++++++++++++++ src/domain/service/LoadingService.ts | 79 ++++++ .../service/ScreenCaptureService.test.ts | 71 +++++ src/domain/service/ScreenCaptureService.ts | 146 ++++++++++ src/domain/service/ViewBinderService.test.ts | 258 ++++++++++++++++++ src/domain/service/ViewBinderService.ts | 99 +++++++ src/domain/{loading => variable}/Loading.ts | 2 +- .../Request/variable/RepositoryMap.ts | 16 -- .../Response/variable/Response.ts | 5 - .../{Response => }/dto/ResponseDTO.test.ts | 2 +- .../{Response => }/dto/ResponseDTO.ts | 2 +- .../ContentRepository.ts} | 12 +- .../CustomRepository.test.ts} | 10 +- .../CustomRepository.ts} | 10 +- .../JsonRepository.test.ts} | 8 +- .../JsonRepository.ts} | 10 +- .../service/RequestCacheCheckService.test.ts | 2 +- .../service/RequestCacheCheckService.ts | 10 +- .../RequestResponseProcessService.test.ts | 2 +- .../service/RequestResponseProcessService.ts | 10 +- .../usecase/RequestUseCase.test.ts | 8 +- .../{Request => }/usecase/RequestUseCase.ts | 8 +- .../ResponseRemoveVariableUseCase.test.ts | 10 +- .../usecase/ResponseRemoveVariableUseCase.ts | 8 +- src/infrastructure/variable/RepositoryMap.ts | 16 ++ src/infrastructure/variable/Response.ts | 5 + src/interface/IURLRequestMethod.ts | 1 - src/shared/util/NormalizeHttpMethod.test.ts | 50 ++++ .../util/NormalizeHttpMethod.ts} | 8 +- src/shared/util/ParseQueryString.test.ts | 42 +++ src/shared/util/ParseQueryString.ts | 24 ++ src/shared/util/ToCamelCase.test.ts | 45 +++ .../util/ToCamelCase.ts} | 4 +- 77 files changed, 1481 insertions(+), 1585 deletions(-) delete mode 100644 src/application/Application/service/ApplicationParseQueryStringService.test.ts delete mode 100644 src/application/Application/service/ApplicationParseQueryStringService.ts delete mode 100644 src/application/Context/service/ContextToCamelCaseService.test.ts delete mode 100644 src/application/Context/service/ContextUnbindService.test.ts delete mode 100644 src/application/Context/service/ContextUnbindService.ts delete mode 100644 src/application/Context/usecase/ContextBindUseCase.test.ts delete mode 100644 src/application/Context/usecase/ContextBindUseCase.ts rename src/application/{Application/service/ApplicationQueryStringParserService.test.ts => service/QueryStringParserService.test.ts} (95%) rename src/application/{Application/service/ApplicationQueryStringParserService.ts => service/QueryStringParserService.ts} (77%) rename src/application/{Config/service/ConfigParserRequestsPropertyService.test.ts => service/RoutingRequestsParserService.test.ts} (93%) rename src/application/{Config/service/ConfigParserRequestsPropertyService.ts => service/RoutingRequestsParserService.ts} (86%) rename src/application/{Application => }/usecase/ApplicationGotoViewUseCase.test.ts (56%) rename src/application/{Application => }/usecase/ApplicationGotoViewUseCase.ts (62%) rename src/application/{Application/service/ApplicationInitializeService.test.ts => usecase/ApplicationInitializeUseCase.test.ts} (71%) rename src/application/{Application/service/ApplicationInitializeService.ts => usecase/ApplicationInitializeUseCase.ts} (76%) rename src/application/{Context/service/ContextRunService.test.ts => usecase/ContextRunUseCase.test.ts} (75%) rename src/application/{Context/service/ContextRunService.ts => usecase/ContextRunUseCase.ts} (74%) rename src/{domain/callback/service/CallbackService.test.ts => application/usecase/ExecuteCallbackUseCase.test.ts} (60%) rename src/{domain/callback/service/CallbackService.ts => application/usecase/ExecuteCallbackUseCase.ts} (72%) rename src/application/{Application => }/variable/PopstateQueue.ts (100%) create mode 100644 src/domain/entity/DefaultLoader.ts delete mode 100644 src/domain/loading/DefaultLoader.ts delete mode 100644 src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.test.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoaderEndService.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.test.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.test.ts delete mode 100644 src/domain/loading/DefaultLoading/service/DefaultLoadingInitializeService.ts delete mode 100644 src/domain/loading/Loading/service/LoadingEndService.test.ts delete mode 100644 src/domain/loading/Loading/service/LoadingEndService.ts delete mode 100644 src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts delete mode 100644 src/domain/loading/Loading/service/LoadingGetInstanceService.ts delete mode 100644 src/domain/loading/Loading/service/LoadingStartService.test.ts delete mode 100644 src/domain/loading/Loading/service/LoadingStartService.ts delete mode 100644 src/domain/screen/Capture.ts delete mode 100644 src/domain/screen/Capture/service/AddScreenCaptureService.test.ts delete mode 100644 src/domain/screen/Capture/service/AddScreenCaptureService.ts delete mode 100644 src/domain/screen/Capture/service/DisposeCaptureService.test.ts delete mode 100644 src/domain/screen/Capture/service/DisposeCaptureService.ts delete mode 100644 src/domain/screen/Capture/variable/CacheX.ts delete mode 100644 src/domain/screen/Capture/variable/CacheY.ts create mode 100644 src/domain/service/LoadingService.test.ts create mode 100644 src/domain/service/LoadingService.ts create mode 100644 src/domain/service/ScreenCaptureService.test.ts create mode 100644 src/domain/service/ScreenCaptureService.ts create mode 100644 src/domain/service/ViewBinderService.test.ts create mode 100644 src/domain/service/ViewBinderService.ts rename src/domain/{loading => variable}/Loading.ts (99%) delete mode 100644 src/infrastructure/Request/variable/RepositoryMap.ts delete mode 100644 src/infrastructure/Response/variable/Response.ts rename src/infrastructure/{Response => }/dto/ResponseDTO.test.ts (99%) rename src/infrastructure/{Response => }/dto/ResponseDTO.ts (99%) rename src/infrastructure/{Request/repository/RequestContentRepository.ts => repository/ContentRepository.ts} (83%) rename src/infrastructure/{Request/repository/RequestCustomRepository.test.ts => repository/CustomRepository.test.ts} (83%) rename src/infrastructure/{Request/repository/RequestCustomRepository.ts => repository/CustomRepository.ts} (86%) rename src/infrastructure/{Request/repository/RequestJsonRepository.test.ts => repository/JsonRepository.test.ts} (95%) rename src/infrastructure/{Request/repository/RequestJsonRepository.ts => repository/JsonRepository.ts} (80%) rename src/infrastructure/{Request => }/service/RequestCacheCheckService.test.ts (95%) rename src/infrastructure/{Request => }/service/RequestCacheCheckService.ts (66%) rename src/infrastructure/{Request => }/service/RequestResponseProcessService.test.ts (95%) rename src/infrastructure/{Request => }/service/RequestResponseProcessService.ts (64%) rename src/infrastructure/{Request => }/usecase/RequestUseCase.test.ts (88%) rename src/infrastructure/{Request => }/usecase/RequestUseCase.ts (74%) rename src/infrastructure/{Response => }/usecase/ResponseRemoveVariableUseCase.test.ts (89%) rename src/infrastructure/{Response => }/usecase/ResponseRemoveVariableUseCase.ts (84%) create mode 100644 src/infrastructure/variable/RepositoryMap.ts create mode 100644 src/infrastructure/variable/Response.ts delete mode 100644 src/interface/IURLRequestMethod.ts create mode 100644 src/shared/util/NormalizeHttpMethod.test.ts rename src/{infrastructure/Request/service/RequestNormalizeMethodService.ts => shared/util/NormalizeHttpMethod.ts} (73%) create mode 100644 src/shared/util/ParseQueryString.test.ts create mode 100644 src/shared/util/ParseQueryString.ts create mode 100644 src/shared/util/ToCamelCase.test.ts rename src/{application/Context/service/ContextToCamelCaseService.ts => shared/util/ToCamelCase.ts} (87%) diff --git a/README.md b/README.md index 81a1d28..a7a3e6f 100644 --- a/README.md +++ b/README.md @@ -38,29 +38,54 @@ Next2D框架是根据简洁架构、领域驱动开发、测试驱动开发和MV ``` src/ -├── application/ # Application Layer -│ ├── Application.ts # Main application class -│ ├── Context.ts # View/ViewModel context management -│ ├── Application/ # Application services and use cases -│ ├── Config/ # Configuration services -│ ├── Context/ # Context services and use cases -│ ├── content/ # Content classes (MovieClip, Shape, TextField, Video) -│ └── variable/ # Application state (Config, Context, Cache, Packages, Query) -├── domain/ # Domain Layer -│ ├── callback/ # Callback services -│ ├── loading/ # Loading animation (DefaultLoader, Loading services) -│ └── screen/ # Screen capture services -├── infrastructure/ # Infrastructure Layer -│ ├── Request/ # HTTP request handling (JSON, Content, Custom) -│ └── Response/ # Response data management -├── interface/ # TypeScript interfaces -│ ├── IConfig.ts # Configuration interface -│ ├── IRequest.ts # Request interface -│ ├── IRouting.ts # Routing interface +├── 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 +│ │ ├── ScreenCaptureService.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 │ └── ... -└── view/ # View Layer - ├── View.ts # Base View class - └── ViewModel.ts # Base ViewModel class +├── 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 diff --git a/src/application/Application.ts b/src/application/Application.ts index 7ea6226..16b2b24 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()); } /** diff --git a/src/application/Application/service/ApplicationParseQueryStringService.test.ts b/src/application/Application/service/ApplicationParseQueryStringService.test.ts deleted file mode 100644 index c58de20..0000000 --- a/src/application/Application/service/ApplicationParseQueryStringService.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { execute } from "./ApplicationParseQueryStringService"; -import { query } from "../../variable/Query"; -import { describe, expect, it, beforeEach } from "vitest"; - -describe("ApplicationParseQueryStringService Test", () => -{ - beforeEach(() => - { - query.clear(); - }); - - it("should parse query string without leading ?", () => - { - execute("key1=value1&key2=value2"); - - expect(query.get("key1")).toBe("value1"); - expect(query.get("key2")).toBe("value2"); - }); - - it("should parse query string with leading ?", () => - { - execute("?key1=value1&key2=value2"); - - expect(query.get("key1")).toBe("value1"); - expect(query.get("key2")).toBe("value2"); - }); - - it("should parse single parameter", () => - { - execute("single=param"); - - expect(query.get("single")).toBe("param"); - expect(query.size).toBe(1); - }); -}); diff --git a/src/application/Application/service/ApplicationParseQueryStringService.ts b/src/application/Application/service/ApplicationParseQueryStringService.ts deleted file mode 100644 index 2b2784b..0000000 --- a/src/application/Application/service/ApplicationParseQueryStringService.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { query } from "../../variable/Query"; - -/** - * @description QueryStringをパースしてqueryマップに登録 - * Parse QueryString and register to query map - * - * @param {string} queryString - * @return {void} - * @method - * @protected - */ -export const execute = (queryString: string): void => -{ - const startIndex = queryString.charAt(0) === "?" ? 1 : 0; - const parameters = queryString.slice(startIndex).split("&"); - for (let idx = 0; idx < parameters.length; ++idx) { - const pair = parameters[idx].split("="); - query.set(pair[0], pair[1]); - } -}; diff --git a/src/application/Context/service/ContextToCamelCaseService.test.ts b/src/application/Context/service/ContextToCamelCaseService.test.ts deleted file mode 100644 index 9252358..0000000 --- a/src/application/Context/service/ContextToCamelCaseService.test.ts +++ /dev/null @@ -1,45 +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"); - }); - - it("execute test case4 - hyphen separator", () => - { - expect(execute("user-profile")).toBe("UserProfile"); - }); - - it("execute test case5 - underscore separator", () => - { - expect(execute("user_settings")).toBe("UserSettings"); - }); - - it("execute test case6 - mixed separators", () => - { - expect(execute("game/user-profile_page")).toBe("GameUserProfilePage"); - }); - - it("execute test case7 - multiple hyphens", () => - { - expect(execute("my-awesome-component")).toBe("MyAwesomeComponent"); - }); - - it("execute test case8 - multiple underscores", () => - { - expect(execute("my_awesome_component")).toBe("MyAwesomeComponent"); - }); -}); \ 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 28357e8..0000000 --- a/src/application/Context/service/ContextUnbindService.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -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 { ViewModel } from "../../../view/ViewModel"; -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 onExitCalled = false; - - class TestView extends View - { - async onExit () - { - onExitCalled = true; - } - } - - context.view = new TestView(); - root.addChild(context.view); - context.viewModel = new ViewModel(); - - expect(onExitCalled).toBe(false); - expect(root.numChildren).toBe(1); - - await execute(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); - - context.view = null; - context.viewModel = new ViewModel(); - - await execute(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); - - context.view = new View(); - context.viewModel = null; - - await execute(context); - - 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 4f799f2..0000000 --- a/src/application/Context/service/ContextUnbindService.ts +++ /dev/null @@ -1,33 +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 ; - } - - /** - * 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); -}; \ 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 6d85768..0000000 --- a/src/application/Context/usecase/ContextBindUseCase.test.ts +++ /dev/null @@ -1,140 +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 viewModelInitialized = false; - let viewInitialized = false; - let viewEntered = false; - - class TestViewModel extends ViewModel - { - async initialize () - { - viewModelInitialized = true; - } - } - - class TestView extends View - { - async initialize () - { - viewInitialized = true; - } - - async onEnter () - { - viewEntered = true; - } - - 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(viewEntered).toBe(false); - expect(root.numChildren).toBe(0); - - await execute(context, "test"); - - expect(viewModelInitialized).toBe(true); - expect(viewInitialized).toBe(true); - expect(viewEntered).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(execute(context, "notfound")).rejects.toThrow("not found view or viewMode."); - }); - - 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 execute(context, "test"); - - expect(root.numChildren).toBe(1); - expect(context.view).toBeInstanceOf(TestView); - }); -}); \ 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 3e44022..0000000 --- a/src/application/Context/usecase/ContextBindUseCase.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Context } from "../../Context"; -import type { View } from "../../../view/View"; -import type { ViewModel } from "../../../view/ViewModel"; -import type { Constructor } from "../../../interface/IPackages"; -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を起動、初期化処理を実行 - * 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(); - - /** - * 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); - - /** - * 画面表示時の処理を実行 - * Execute processing when the screen is displayed - */ - await context.view.onEnter(); - - return context.view; -}; \ 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 95% rename from src/application/Application/service/ApplicationQueryStringParserService.test.ts rename to src/application/service/QueryStringParserService.test.ts index 57412b7..e00e675 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,7 +11,7 @@ Object.defineProperty(window, "location", { }) }); -describe("ApplicationQueryStringParserService", () => +describe("QueryStringParserService", () => { it("parse query test case1", () => { @@ -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 77% rename from src/application/Application/service/ApplicationQueryStringParserService.ts rename to src/application/service/QueryStringParserService.ts index 8aa043c..943b613 100644 --- a/src/application/Application/service/ApplicationQueryStringParserService.ts +++ b/src/application/service/QueryStringParserService.ts @@ -1,7 +1,7 @@ -import type { IQueryObject } from "../../../interface/IQueryObject"; -import { $getConfig } from "../../variable/Config"; -import { query } from "../../variable/Query"; -import { execute as applicationParseQueryStringService } from "./ApplicationParseQueryStringService"; +import type { IQueryObject } from "../../interface/IQueryObject"; +import { $getConfig } from "../variable/Config"; +import { query } from "../variable/Query"; +import { parseQueryString } from "../../shared/util/ParseQueryString"; /** * @description 指定されたQueryStringか、URLのQueryStringをquery mapに登録 @@ -29,7 +29,10 @@ export const execute = (name: string = ""): IQueryObject => let queryString = ""; if (!name && location.search) { queryString = location.search; - applicationParseQueryStringService(queryString); + const parsed = parseQueryString(queryString); + for (const [key, value] of parsed) { + query.set(key, value); + } } const config = $getConfig(); @@ -61,7 +64,10 @@ export const execute = (name: string = ""): IQueryObject => if (name.indexOf("?") > -1) { const idx = name.indexOf("?"); queryString = name.slice(idx); - applicationParseQueryStringService(name.slice(idx + 1)); + const parsed = parseQueryString(name.slice(idx + 1)); + for (const [key, value] of parsed) { + query.set(key, value); + } name = name.slice(0, idx); } @@ -77,4 +83,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 86% rename from src/application/Config/service/ConfigParserRequestsPropertyService.ts rename to src/application/service/RoutingRequestsParserService.ts index 45652f5..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を返却します。 @@ -48,4 +48,4 @@ export const execute = (name: string): IRequest[] => } return requests; -}; \ No newline at end of file +}; diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/usecase/ApplicationGotoViewUseCase.test.ts similarity index 56% rename from src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts rename to src/application/usecase/ApplicationGotoViewUseCase.test.ts index f668625..8ce988a 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.test.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.test.ts @@ -1,49 +1,47 @@ 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) +import { Context } from "../Context"; +import { $setConfig } from "../variable/Config"; +import { $setContext } from "../variable/Context"; +import { response } from "../../infrastructure/variable/Response"; + +vi.mock("../../domain/service/ScreenCaptureService", () => ({ + ScreenCaptureService: { + add: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn() + } })); -vi.mock("../../../domain/loading/Loading/service/LoadingEndService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) +vi.mock("../../domain/service/LoadingService", () => ({ + LoadingService: { + start: vi.fn().mockResolvedValue(undefined), + end: vi.fn().mockResolvedValue(undefined), + getInstance: vi.fn().mockReturnValue(null) + } })); -vi.mock("../../../infrastructure/Response/usecase/ResponseRemoveVariableUseCase", () => ({ +vi.mock("../../infrastructure/usecase/ResponseRemoveVariableUseCase", () => ({ execute: vi.fn() })); -vi.mock("../service/ApplicationQueryStringParserService", () => ({ +vi.mock("../service/QueryStringParserService", () => ({ execute: vi.fn() })); -vi.mock("../../../infrastructure/Request/usecase/RequestUseCase", () => ({ +vi.mock("../../infrastructure/usecase/RequestUseCase", () => ({ execute: vi.fn().mockResolvedValue([]) })); -vi.mock("../../../domain/callback/service/CallbackService", () => ({ +vi.mock("./ExecuteCallbackUseCase", () => ({ execute: vi.fn().mockResolvedValue(undefined) })); -vi.mock("../../Context/usecase/ContextBindUseCase", () => ({ - execute: vi.fn().mockResolvedValue(null) -})); - -vi.mock("../../Context/service/ContextUnbindService", () => ({ - execute: vi.fn().mockResolvedValue(undefined) +vi.mock("../../domain/service/ViewBinderService", () => ({ + ViewBinderService: { + bind: vi.fn().mockResolvedValue(null), + unbind: vi.fn().mockResolvedValue(undefined) + } })); describe("ApplicationGotoViewUseCase Test", () => @@ -88,11 +86,11 @@ describe("ApplicationGotoViewUseCase Test", () => 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"); - const { execute: contextBindUseCase } = await import("../../Context/usecase/ContextBindUseCase"); + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); + const { ViewBinderService } = await import("../../domain/service/ViewBinderService"); - vi.mocked(applicationQueryStringParserService).mockReturnValue({ + vi.mocked(queryStringParserService).mockReturnValue({ name: "home", queryString: "" }); @@ -102,15 +100,15 @@ describe("ApplicationGotoViewUseCase Test", () => await execute(mockApplication, "home"); expect(mockApplication.currentName).toBe("home"); - expect(contextBindUseCase).toHaveBeenCalledWith(mockContext, "home"); + expect(ViewBinderService.bind).toHaveBeenCalledWith(mockContext, "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"); + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); - vi.mocked(applicationQueryStringParserService).mockReturnValue({ + vi.mocked(queryStringParserService).mockReturnValue({ name: "dashboard", queryString: "" }); @@ -131,10 +129,10 @@ describe("ApplicationGotoViewUseCase Test", () => 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"); + const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); + const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); - vi.mocked(applicationQueryStringParserService).mockReturnValue({ + vi.mocked(queryStringParserService).mockReturnValue({ name: "test", queryString: "" }); diff --git a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts b/src/application/usecase/ApplicationGotoViewUseCase.ts similarity index 62% rename from src/application/Application/usecase/ApplicationGotoViewUseCase.ts rename to src/application/usecase/ApplicationGotoViewUseCase.ts index 8624523..8b4f387 100644 --- a/src/application/Application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.ts @@ -1,17 +1,14 @@ -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 { execute as contextUnbindService } from "../../Context/service/ContextUnbindService"; -import { execute as contextBindUseCase } from "../../Context/usecase/ContextBindUseCase"; +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 } 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 { ScreenCaptureService } from "../../domain/service/ScreenCaptureService"; /** * @description 指定されたパス、もしくはURLのクラスを起動 @@ -33,13 +30,13 @@ export const execute = async (application: Application, name: string = ""): Prom * 現時点の描画をキャプチャーして表示 * Capture and display the current drawing */ - await addScreenCaptureService(); + await ScreenCaptureService.add(); /** * ローディング表示を起動 * Launch loading display */ - await loadingStartService(); + await LoadingService.start(); } /** @@ -47,7 +44,7 @@ export const execute = async (application: Application, name: string = ""): Prom * Unbind the View and ViewModel of the current screen */ const context = $getContext(); - await contextUnbindService(context); + await ViewBinderService.unbind(context); /** * 前の画面で取得したレスポンスデータを初期化 @@ -59,7 +56,7 @@ export const execute = async (application: Application, name: string = ""): Prom * 指定されたパス、もしくはURLからアクセス先を算出 * Calculate the access point from the specified path or URL */ - const queryObject = applicationQueryStringParserService(name); + const queryObject = queryStringParserService(name); /** * 現在の画面名を更新 @@ -103,26 +100,26 @@ export const execute = async (application: Application, name: string = ""): Prom * ローディング表示を終了 * End loading display */ - await loadingEndService(); + await LoadingService.end(); /** * 前の画面のキャプチャーを終了 * End previous screen capture */ - disposeCaptureService(); + ScreenCaptureService.dispose(); } /** * ViewとViewModelを起動 * Start View and ViewModel */ - const view = await contextBindUseCase(context, 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 +}; diff --git a/src/application/Application/service/ApplicationInitializeService.test.ts b/src/application/usecase/ApplicationInitializeUseCase.test.ts similarity index 71% rename from src/application/Application/service/ApplicationInitializeService.test.ts rename to src/application/usecase/ApplicationInitializeUseCase.test.ts index b7d3c8c..8635644 100644 --- a/src/application/Application/service/ApplicationInitializeService.test.ts +++ b/src/application/usecase/ApplicationInitializeUseCase.test.ts @@ -1,18 +1,18 @@ -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 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("ApplicationInitializeService", () => +describe("ApplicationInitializeUseCase", () => { - it("test case", () => + it("test case", () => { let state = ""; - window.addEventListener = vi.fn((name) => + window.addEventListener = vi.fn((name) => { state = name; }); @@ -47,7 +47,7 @@ describe("ApplicationInitializeService", () => it("test case", () => { let state = ""; - window.addEventListener = vi.fn((name) => + window.addEventListener = vi.fn((name) => { state = name; }); @@ -71,4 +71,4 @@ describe("ApplicationInitializeService", () => execute(app, config, buildPackages); expect(state).toBe(""); }); -}); \ No newline at end of file +}); diff --git a/src/application/Application/service/ApplicationInitializeService.ts b/src/application/usecase/ApplicationInitializeUseCase.ts similarity index 76% rename from src/application/Application/service/ApplicationInitializeService.ts rename to src/application/usecase/ApplicationInitializeUseCase.ts index 79220ab..6a8b83c 100644 --- a/src/application/Application/service/ApplicationInitializeService.ts +++ b/src/application/usecase/ApplicationInitializeUseCase.ts @@ -1,8 +1,8 @@ -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 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"; /** @@ -38,4 +38,4 @@ export const execute = ( } 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 72% rename from src/domain/callback/service/CallbackService.ts rename to src/application/usecase/ExecuteCallbackUseCase.ts index 8512349..a4d0b80 100644 --- a/src/domain/callback/service/CallbackService.ts +++ b/src/application/usecase/ExecuteCallbackUseCase.ts @@ -1,10 +1,10 @@ -import type { ICallback } from "../../../interface/ICallback"; -import type { Constructor } from "../../../interface/IPackages"; -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 {unknown} [value=null] @@ -35,4 +35,4 @@ export const execute = async ( const CallbackClass = packages.get(name) as Constructor; await new CallbackClass().execute(value); } -}; \ No newline at end of file +}; diff --git a/src/application/Application/variable/PopstateQueue.ts b/src/application/variable/PopstateQueue.ts similarity index 100% rename from src/application/Application/variable/PopstateQueue.ts rename to src/application/variable/PopstateQueue.ts diff --git a/src/domain/entity/DefaultLoader.ts b/src/domain/entity/DefaultLoader.ts new file mode 100644 index 0000000..3545bcf --- /dev/null +++ b/src/domain/entity/DefaultLoader.ts @@ -0,0 +1,182 @@ +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 Tweenジョブを取得または作成 + * Get or create Tween job + * + * @param {Shape} shape + * @param {string} jobName + * @return {Job} + */ +const getOrCreateJob = (shape: Shape, jobName: string): Job => +{ + if (shape.hasLocalVariable(jobName)) { + const job = shape.getLocalVariable(jobName) as Job; + job.stop(); + return job; + } + + const job = 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(jobName, job); + return job; +}; + +/** + * @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; + + const reduceJob = getOrCreateJob(shape, "reduceJob"); + const expandJob = getOrCreateJob(shape, "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); + } + + /** + * @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; + } + + 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); + } +} 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/helper/GetOrCreateJobHelper.ts b/src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts deleted file mode 100644 index b2a9504..0000000 --- a/src/domain/loading/DefaultLoading/helper/GetOrCreateJobHelper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Shape } from "@next2d/display"; -import type { Job } from "@next2d/ui"; -import { - Tween, - Easing -} from "@next2d/ui"; - -/** - * @description Tweenジョブを取得または作成 - * Get or create Tween job - * - * @param {Shape} shape - * @param {string} jobName - * @return {Job} - * @method - * @protected - */ -export const execute = (shape: Shape, jobName: string): Job => -{ - if (shape.hasLocalVariable(jobName)) { - const job = shape.getLocalVariable(jobName) as Job; - job.stop(); - return job; - } - - const job = 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(jobName, job); - return job; -}; 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 187782a..0000000 --- a/src/domain/loading/DefaultLoading/service/DefaultLoaderStartService.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { DefaultLoader } from "../../DefaultLoader"; -import type { Shape } from "@next2d/display"; -import { $getConfig } from "../../../../application/variable/Config"; -import { $getContext } from "../../../../application/variable/Context"; -import { execute as getOrCreateJobHelper } from "../helper/GetOrCreateJobHelper"; - -/** - * @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; - - const reduceJob = getOrCreateJobHelper(shape, "reduceJob"); - const expandJob = getOrCreateJobHelper(shape, "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 5892a53..0000000 --- a/src/domain/loading/Loading/service/LoadingEndService.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { execute as loadingGetInstanceService } from "./LoadingGetInstanceService"; - -/** - * @description ローダーのアニメーションを終了 - * End loader animation - * - * @return {Promise} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - const instance = loadingGetInstanceService(); - if (!instance) { - return ; - } - - await instance.end(); -}; \ No newline at end of file diff --git a/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts b/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts deleted file mode 100644 index fbe3cd9..0000000 --- a/src/domain/loading/Loading/service/LoadingGetInstanceService.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { execute } from "./LoadingGetInstanceService"; -import { $setConfig } from "../../../../application/variable/Config"; -import { $setPackages } from "../../../../application/variable/Packages"; -import { $setInstance } from "../../Loading"; -import { DefaultLoader } from "../../DefaultLoader"; -import { describe, expect, it, beforeEach } from "vitest"; - -describe("LoadingGetInstanceService Test", () => -{ - beforeEach(() => - { - $setInstance(null as any); - }); - - it("should return null if config has no loading", () => - { - $setConfig({ - "platform": "web", - "spa": false, - "stage": { - "width": 800, - "height": 600, - "fps": 60 - } - }); - - const result = execute(); - - 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 = execute(); - - 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 = execute(); - - 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 = execute(); - const result2 = execute(); - - expect(result1).toBe(result2); - }); -}); diff --git a/src/domain/loading/Loading/service/LoadingGetInstanceService.ts b/src/domain/loading/Loading/service/LoadingGetInstanceService.ts deleted file mode 100644 index e7e7285..0000000 --- a/src/domain/loading/Loading/service/LoadingGetInstanceService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ILoading } from "../../../../interface/ILoading"; -import type { Constructor } from "../../../../interface/IPackages"; -import { DefaultLoader } from "../../DefaultLoader"; -import { $getConfig } from "../../../../application/variable/Config"; -import { packages } from "../../../../application/variable/Packages"; -import { - $getInstance, - $setInstance -} from "../../Loading"; - -/** - * @description ローダーのインスタンスを取得または作成 - * Get or create loader instance - * - * @return {ILoading | null} - * @method - * @protected - */ -export const execute = (): 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; -}; 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 2e71c02..0000000 --- a/src/domain/loading/Loading/service/LoadingStartService.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { execute as loadingGetInstanceService } from "./LoadingGetInstanceService"; - -/** - * @description ローダーのアニメーションを実行 - * Execute loader animation - * - * @return {Promise} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - const instance = loadingGetInstanceService(); - if (!instance) { - return ; - } - - 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 54bd28e..0000000 --- a/src/domain/screen/Capture/service/AddScreenCaptureService.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { $getConfig } from "../../../../application/variable/Config"; -import { $getContext } from "../../../../application/variable/Context"; -import { Matrix } from "@next2d/geom"; -import { shape } from "../../Capture"; -import { cacheX, setCacheX } from "../variable/CacheX"; -import { cacheY, setCacheY } from "../variable/CacheY"; -import { - stage, - BitmapData, - Shape -} from "@next2d/display"; - -/** - * @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 - ), - "bgColor": config.stage.options?.bgColor || null, - "bgAlpha": config.stage.options?.bgColor !== "" ? 1 : 0 - }); - - 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) { - setCacheX(tx); - shape.width = stage.rendererWidth / scale; - shape.x = -tx / scale; - } - - if (ty && cacheY !== ty) { - setCacheY(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/screen/Capture/variable/CacheX.ts b/src/domain/screen/Capture/variable/CacheX.ts deleted file mode 100644 index 9939873..0000000 --- a/src/domain/screen/Capture/variable/CacheX.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @type {number} - * @protected - */ -export let cacheX: number = 0; - -/** - * @description キャッシュXを設定 - * Set cache X - * - * @param {number} value - * @return {void} - * @method - * @protected - */ -export const setCacheX = (value: number): void => -{ - cacheX = value; -}; diff --git a/src/domain/screen/Capture/variable/CacheY.ts b/src/domain/screen/Capture/variable/CacheY.ts deleted file mode 100644 index bb688a4..0000000 --- a/src/domain/screen/Capture/variable/CacheY.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @type {number} - * @protected - */ -export let cacheY: number = 0; - -/** - * @description キャッシュYを設定 - * Set cache Y - * - * @param {number} value - * @return {void} - * @method - * @protected - */ -export const setCacheY = (value: number): void => -{ - cacheY = value; -}; diff --git a/src/domain/service/LoadingService.test.ts b/src/domain/service/LoadingService.test.ts new file mode 100644 index 0000000..b60d3bf --- /dev/null +++ b/src/domain/service/LoadingService.test.ts @@ -0,0 +1,211 @@ +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(); + }); + }); +}); diff --git a/src/domain/service/LoadingService.ts b/src/domain/service/LoadingService.ts new file mode 100644 index 0000000..8921e84 --- /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/ScreenCaptureService.test.ts b/src/domain/service/ScreenCaptureService.test.ts new file mode 100644 index 0000000..0a84dc2 --- /dev/null +++ b/src/domain/service/ScreenCaptureService.test.ts @@ -0,0 +1,71 @@ +import { ScreenCaptureService } from "./ScreenCaptureService"; +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, vi } from "vitest"; + +Object.defineProperty(window, "next2d", { + "get": vi.fn().mockReturnValue({ + "captureToCanvas": async () => { + return document.createElement("canvas"); + } + }) +}); + +describe("ScreenCaptureService Test", () => +{ + describe("add", () => + { + it("should add capture shape to stage", async () => + { + $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 ScreenCaptureService.add(); + expect(root.numChildren).toBe(1); + expect(root.mouseChildren).toBe(false); + }); + }); + + describe("dispose", () => + { + it("should remove all children and enable mouse", () => + { + $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(new Shape()); + root.addChild(new Shape()); + + expect(root.numChildren).toBe(2); + expect(root.mouseChildren).toBe(false); + ScreenCaptureService.dispose(); + expect(root.numChildren).toBe(0); + expect(root.mouseChildren).toBe(true); + }); + }); +}); diff --git a/src/domain/service/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts new file mode 100644 index 0000000..e27c825 --- /dev/null +++ b/src/domain/service/ScreenCaptureService.ts @@ -0,0 +1,146 @@ +import { $getConfig } from "../../application/variable/Config"; +import { $getContext } from "../../application/variable/Context"; +import { Matrix } from "@next2d/geom"; +import { + stage, + BitmapData, + Shape +} from "@next2d/display"; + +/** + * @type {Shape} + * @private + */ +const shape: Shape = new Shape(); + +/** + * @type {number} + * @private + */ +let cacheX: number = 0; + +/** + * @type {number} + * @private + */ +let cacheY: number = 0; + +/** + * @description 画面キャプチャーを管理するドメインサービス + * Domain service for managing screen capture + */ +export const ScreenCaptureService = { + + /** + * @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; + + 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 + ), + "bgColor": config.stage.options?.bgColor || null, + "bgAlpha": config.stage.options?.bgColor !== "" ? 1 : 0 + }); + + 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); + }, + + /** + * @description 画面キャプチャーのShapeをStageから削除 + * Delete Screen Capture Shape from Stage + * + * @return {void} + */ + "dispose": (): 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; + } +}; diff --git a/src/domain/service/ViewBinderService.test.ts b/src/domain/service/ViewBinderService.test.ts new file mode 100644 index 0000000..1bf73c3 --- /dev/null +++ b/src/domain/service/ViewBinderService.test.ts @@ -0,0 +1,258 @@ +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; + let viewEntered = false; + + class TestViewModel extends ViewModel + { + async initialize () + { + viewModelInitialized = true; + } + } + + class TestView extends View + { + async initialize () + { + viewInitialized = true; + } + + async onEnter () + { + viewEntered = true; + } + + 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(viewEntered).toBe(false); + expect(root.numChildren).toBe(0); + + await ViewBinderService.bind(context, "test"); + + expect(viewModelInitialized).toBe(true); + expect(viewInitialized).toBe(true); + expect(viewEntered).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(1); + 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); + }); + }); +}); diff --git a/src/domain/service/ViewBinderService.ts b/src/domain/service/ViewBinderService.ts new file mode 100644 index 0000000..b06cb25 --- /dev/null +++ b/src/domain/service/ViewBinderService.ts @@ -0,0 +1,99 @@ +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(); + + /** + * 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); + + /** + * 画面表示時の処理を実行 + * Execute processing when the screen is displayed + */ + await context.view.onEnter(); + + 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 99% rename from src/domain/loading/Loading.ts rename to src/domain/variable/Loading.ts index a7bed1b..1d41632 100644 --- a/src/domain/loading/Loading.ts +++ b/src/domain/variable/Loading.ts @@ -32,4 +32,4 @@ export const $getInstance = (): ILoading => export const $setInstance = (instance: ILoading): void => { $instance = instance; -}; \ No newline at end of file +}; diff --git a/src/infrastructure/Request/variable/RepositoryMap.ts b/src/infrastructure/Request/variable/RepositoryMap.ts deleted file mode 100644 index c119bf5..0000000 --- a/src/infrastructure/Request/variable/RepositoryMap.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { execute as requestContentRepository } from "../repository/RequestContentRepository"; -import { execute as requestCustomRepository } from "../repository/RequestCustomRepository"; -import { execute as requestJsonRepository } from "../repository/RequestJsonRepository"; - -/** - * @description リクエストタイプとリポジトリのマップ - * Map of request types and repositories - * - * @type {Map} - * @protected - */ -export const repositoryMap: Map = new Map([ - ["json", requestJsonRepository], - ["content", requestContentRepository], - ["custom", requestCustomRepository] -]); 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/Response/dto/ResponseDTO.test.ts b/src/infrastructure/dto/ResponseDTO.test.ts similarity index 99% rename from src/infrastructure/Response/dto/ResponseDTO.test.ts rename to src/infrastructure/dto/ResponseDTO.test.ts index 2778376..3571bc2 100644 --- a/src/infrastructure/Response/dto/ResponseDTO.test.ts +++ b/src/infrastructure/dto/ResponseDTO.test.ts @@ -16,4 +16,4 @@ describe("ResponseDTOTest", () => expect(responseDTO.name).toBe("sample"); expect(responseDTO.response).toBe(100); }); -}); \ No newline at end of file +}); diff --git a/src/infrastructure/Response/dto/ResponseDTO.ts b/src/infrastructure/dto/ResponseDTO.ts similarity index 99% rename from src/infrastructure/Response/dto/ResponseDTO.ts rename to src/infrastructure/dto/ResponseDTO.ts index 9c0ce22..d2a3a89 100644 --- a/src/infrastructure/Response/dto/ResponseDTO.ts +++ b/src/infrastructure/dto/ResponseDTO.ts @@ -39,4 +39,4 @@ export class ResponseDTO this.name = name; this.response = response; } -} \ No newline at end of file +} diff --git a/src/infrastructure/Request/repository/RequestContentRepository.ts b/src/infrastructure/repository/ContentRepository.ts similarity index 83% rename from src/infrastructure/Request/repository/RequestContentRepository.ts rename to src/infrastructure/repository/ContentRepository.ts index e156665..ee76145 100644 --- a/src/infrastructure/Request/repository/RequestContentRepository.ts +++ b/src/infrastructure/repository/ContentRepository.ts @@ -1,11 +1,11 @@ -import type { IRequest } from "../../../interface/IRequest"; +import type { IRequest } from "../../interface/IRequest"; import { Loader } from "@next2d/display"; import { URLRequest } from "@next2d/net"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; +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"; -import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; /** * @description 指定先のコンテンツを非同期で取得 @@ -28,7 +28,7 @@ export const execute = async (request_object: IRequest): Promise => } const urlRequest = new URLRequest(request_object.path); - urlRequest.method = requestNormalizeMethodService(request_object.method); + urlRequest.method = normalizeHttpMethod(request_object.method); if (request_object.headers) { for (const [name, value] of Object.entries(request_object.headers)) { @@ -62,4 +62,4 @@ export const execute = async (request_object: IRequest): Promise => } return requestResponseProcessService(request_object, content); -}; \ No newline at end of file +}; diff --git a/src/infrastructure/Request/repository/RequestCustomRepository.test.ts b/src/infrastructure/repository/CustomRepository.test.ts similarity index 83% rename from src/infrastructure/Request/repository/RequestCustomRepository.test.ts rename to src/infrastructure/repository/CustomRepository.test.ts index e09537a..7ea3d45 100644 --- a/src/infrastructure/Request/repository/RequestCustomRepository.test.ts +++ b/src/infrastructure/repository/CustomRepository.test.ts @@ -1,9 +1,9 @@ -import { execute } from "./RequestCustomRepository"; -import { packages } from "../../../application/variable/Packages"; -import type { IRequest } from "../../../interface/IRequest"; +import { execute } from "./CustomRepository"; +import { packages } from "../../application/variable/Packages"; +import type { IRequest } from "../../interface/IRequest"; import { describe, expect, it } from "vitest"; -describe("RequestCustomRepository Test", () => +describe("CustomRepository Test", () => { it("execute public test", async () => { @@ -56,7 +56,7 @@ describe("RequestCustomRepository Test", () => packages.clear(); packages.set("CustomClass", CustomClass); - const responseDTO = await execute(object) + 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/repository/CustomRepository.ts similarity index 86% rename from src/infrastructure/Request/repository/RequestCustomRepository.ts rename to src/infrastructure/repository/CustomRepository.ts index 319802e..2cc56aa 100644 --- a/src/infrastructure/Request/repository/RequestCustomRepository.ts +++ b/src/infrastructure/repository/CustomRepository.ts @@ -1,7 +1,7 @@ -import type { IRequest } from "../../../interface/IRequest"; -import type { Constructor } from "../../../interface/IPackages"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { packages } from "../../../application/variable/Packages"; +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"; @@ -48,4 +48,4 @@ export const execute = async (request_object: IRequest): Promise => : await new CustomClass()[request_object.method](); return requestResponseProcessService(request_object, value); -}; \ No newline at end of file +}; diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.test.ts b/src/infrastructure/repository/JsonRepository.test.ts similarity index 95% rename from src/infrastructure/Request/repository/RequestJsonRepository.test.ts rename to src/infrastructure/repository/JsonRepository.test.ts index 426c6dc..60f93c9 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 () => { @@ -107,4 +107,4 @@ describe("RequestJsonRepository Test", () => expect(responseDTO.name).toBe("JsonRepository"); expect(responseDTO.response).toBe("success fetch json"); }); -}); \ No newline at end of file +}); diff --git a/src/infrastructure/Request/repository/RequestJsonRepository.ts b/src/infrastructure/repository/JsonRepository.ts similarity index 80% rename from src/infrastructure/Request/repository/RequestJsonRepository.ts rename to src/infrastructure/repository/JsonRepository.ts index 2ca6aa2..2b7fb63 100644 --- a/src/infrastructure/Request/repository/RequestJsonRepository.ts +++ b/src/infrastructure/repository/JsonRepository.ts @@ -1,8 +1,8 @@ -import type { IRequest } from "../../../interface/IRequest"; -import { ResponseDTO } from "../../Response/dto/ResponseDTO"; +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"; -import { execute as requestNormalizeMethodService } from "../service/RequestNormalizeMethodService"; /** * @description 指定先のJSONを非同期で取得 @@ -24,7 +24,7 @@ export const execute = async (request_object: IRequest): Promise => return cachedResponse; } - const method = requestNormalizeMethodService(request_object.method); + const method = normalizeHttpMethod(request_object.method); const options: RequestInit = { method }; if (request_object.body && (method === "POST" || method === "PUT")) { @@ -39,4 +39,4 @@ export const execute = async (request_object: IRequest): Promise => const value = await response.json(); return requestResponseProcessService(request_object, value); -}; \ No newline at end of file +}; diff --git a/src/infrastructure/Request/service/RequestCacheCheckService.test.ts b/src/infrastructure/service/RequestCacheCheckService.test.ts similarity index 95% rename from src/infrastructure/Request/service/RequestCacheCheckService.test.ts rename to src/infrastructure/service/RequestCacheCheckService.test.ts index 50e4b55..50654a7 100644 --- a/src/infrastructure/Request/service/RequestCacheCheckService.test.ts +++ b/src/infrastructure/service/RequestCacheCheckService.test.ts @@ -1,5 +1,5 @@ import { execute } from "./RequestCacheCheckService"; -import { cache } from "../../../application/variable/Cache"; +import { cache } from "../../application/variable/Cache"; import { describe, expect, it, beforeEach } from "vitest"; describe("RequestCacheCheckService Test", () => diff --git a/src/infrastructure/Request/service/RequestCacheCheckService.ts b/src/infrastructure/service/RequestCacheCheckService.ts similarity index 66% rename from src/infrastructure/Request/service/RequestCacheCheckService.ts rename to src/infrastructure/service/RequestCacheCheckService.ts index b5cff71..4193c30 100644 --- a/src/infrastructure/Request/service/RequestCacheCheckService.ts +++ b/src/infrastructure/service/RequestCacheCheckService.ts @@ -1,7 +1,7 @@ -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"; +import type { IRequest } from "../../interface/IRequest"; +import { cache } from "../../application/variable/Cache"; +import { ResponseDTO } from "../dto/ResponseDTO"; +import { execute as executeCallbackUseCase } from "../../application/usecase/ExecuteCallbackUseCase"; /** * @description キャッシュをチェックし、存在すればキャッシュデータを返却 @@ -27,7 +27,7 @@ export const execute = async (request_object: IRequest): Promise diff --git a/src/infrastructure/Request/service/RequestResponseProcessService.ts b/src/infrastructure/service/RequestResponseProcessService.ts similarity index 64% rename from src/infrastructure/Request/service/RequestResponseProcessService.ts rename to src/infrastructure/service/RequestResponseProcessService.ts index 06cac3a..a0bc14c 100644 --- a/src/infrastructure/Request/service/RequestResponseProcessService.ts +++ b/src/infrastructure/service/RequestResponseProcessService.ts @@ -1,7 +1,7 @@ -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"; +import type { IRequest } from "../../interface/IRequest"; +import { cache } from "../../application/variable/Cache"; +import { ResponseDTO } from "../dto/ResponseDTO"; +import { execute as executeCallbackUseCase } from "../../application/usecase/ExecuteCallbackUseCase"; /** * @description レスポンスをキャッシュに保存し、コールバックを実行してDTOを返却 @@ -22,7 +22,7 @@ export const execute = async (request_object: IRequest, value: T): } if (request_object.callback) { - await callbackService(request_object.callback, value); + await executeCallbackUseCase(request_object.callback, value); } return new ResponseDTO(name, value); 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/Request/usecase/RequestUseCase.ts b/src/infrastructure/usecase/RequestUseCase.ts similarity index 74% rename from src/infrastructure/Request/usecase/RequestUseCase.ts rename to src/infrastructure/usecase/RequestUseCase.ts index 86bc173..5e4c9a6 100644 --- a/src/infrastructure/Request/usecase/RequestUseCase.ts +++ b/src/infrastructure/usecase/RequestUseCase.ts @@ -1,5 +1,5 @@ -import type { ResponseDTO } from "../../Response/dto/ResponseDTO"; -import { execute as configParserRequestsPropertyService } from "../../../application/Config/service/ConfigParserRequestsPropertyService"; +import type { ResponseDTO } from "../dto/ResponseDTO"; +import { execute as routingRequestsParserService } from "../../application/service/RoutingRequestsParserService"; import { repositoryMap } from "../variable/RepositoryMap"; /** @@ -14,7 +14,7 @@ import { repositoryMap } from "../variable/RepositoryMap"; export const execute = async (name: string): Promise => { const responses: ResponseDTO[] = []; - const requests = configParserRequestsPropertyService(name); + const requests = routingRequestsParserService(name); for (let idx = 0; idx < requests.length; ++idx) { @@ -31,4 +31,4 @@ export const execute = async (name: string): Promise => } return responses; -}; \ No newline at end of file +}; diff --git a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts similarity index 89% rename from src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts rename to src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts index 9e5ab40..ecdf3d4 100644 --- a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.test.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts @@ -1,8 +1,8 @@ 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 { $setConfig } from "../../application/variable/Config"; +import { IConfig } from "../../interface/IConfig"; +import { response } from "../variable/Response"; import { describe, expect, it } from "vitest"; describe("ResponseRemoveVariableUseCase", () => @@ -80,4 +80,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 84% rename from src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts rename to src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts index 7a546fe..85e6671 100644 --- a/src/infrastructure/Response/usecase/ResponseRemoveVariableUseCase.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts @@ -1,6 +1,6 @@ import type { DisplayObject } from "@next2d/display"; -import { execute as configParserRequestsPropertyService } from "../../../application/Config/service/ConfigParserRequestsPropertyService"; -import { loaderInfoMap } from "../../../application/variable/LoaderInfoMap"; +import { execute as routingRequestsParserService } from "../../application/service/RoutingRequestsParserService"; +import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; import { response } from "../variable/Response"; /** @@ -14,7 +14,7 @@ import { response } from "../variable/Response"; */ export const execute = (name: string): void => { - const requests = configParserRequestsPropertyService(name); + const requests = routingRequestsParserService(name); for (let idx = 0; idx < requests.length; ++idx) { const object = requests[idx]; @@ -53,4 +53,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..c2e4421 --- /dev/null +++ b/src/infrastructure/variable/RepositoryMap.ts @@ -0,0 +1,16 @@ +import { execute as contentRepository } from "../repository/ContentRepository"; +import { execute as customRepository } from "../repository/CustomRepository"; +import { execute as jsonRepository } from "../repository/JsonRepository"; + +/** + * @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/IURLRequestMethod.ts b/src/interface/IURLRequestMethod.ts deleted file mode 100644 index 202e485..0000000 --- a/src/interface/IURLRequestMethod.ts +++ /dev/null @@ -1 +0,0 @@ -export type IURLRequestMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "POST" | "PUT"; \ 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/infrastructure/Request/service/RequestNormalizeMethodService.ts b/src/shared/util/NormalizeHttpMethod.ts similarity index 73% rename from src/infrastructure/Request/service/RequestNormalizeMethodService.ts rename to src/shared/util/NormalizeHttpMethod.ts index e94bda3..e6381a3 100644 --- a/src/infrastructure/Request/service/RequestNormalizeMethodService.ts +++ b/src/shared/util/NormalizeHttpMethod.ts @@ -1,15 +1,15 @@ -import { IURLRequestMethod } from "../../../interface/IURLRequestMethod"; +import type { IHttpMethod } from "../../interface/IRequest"; /** * @description HTTPメソッドを正規化 * Normalize HTTP method * * @param {string} [method] - * @return {string} + * @return {IHttpMethod} * @method - * @protected + * @public */ -export const execute = (method?: string): IURLRequestMethod => +export const normalizeHttpMethod = (method?: string): IHttpMethod => { if (!method) { return "GET"; diff --git a/src/shared/util/ParseQueryString.test.ts b/src/shared/util/ParseQueryString.test.ts new file mode 100644 index 0000000..3858ee5 --- /dev/null +++ b/src/shared/util/ParseQueryString.test.ts @@ -0,0 +1,42 @@ +import { parseQueryString } from "./ParseQueryString"; +import { describe, expect, it } from "vitest"; + +describe("ParseQueryString Test", () => +{ + it("should parse single key-value pair", () => + { + const result = parseQueryString("key=value"); + expect(result.get("key")).toBe("value"); + }); + + it("should parse multiple key-value pairs", () => + { + const result = parseQueryString("foo=bar&baz=qux"); + expect(result.get("foo")).toBe("bar"); + expect(result.get("baz")).toBe("qux"); + }); + + it("should handle leading question mark", () => + { + const result = parseQueryString("?key=value"); + expect(result.get("key")).toBe("value"); + }); + + it("should handle empty value", () => + { + const result = parseQueryString("key="); + expect(result.get("key")).toBe(""); + }); + + it("should handle key without value", () => + { + const result = parseQueryString("key"); + expect(result.get("key")).toBe(""); + }); + + it("should return empty map for empty string", () => + { + const result = parseQueryString(""); + expect(result.size).toBe(0); + }); +}); diff --git a/src/shared/util/ParseQueryString.ts b/src/shared/util/ParseQueryString.ts new file mode 100644 index 0000000..e289443 --- /dev/null +++ b/src/shared/util/ParseQueryString.ts @@ -0,0 +1,24 @@ +/** + * @description QueryStringをパースしてMapに変換 + * Parse QueryString and convert to Map + * + * @param {string} queryString + * @return {Map} + * @method + * @public + */ +export const parseQueryString = (queryString: string): Map => +{ + const result = new Map(); + const startIndex = queryString.charAt(0) === "?" ? 1 : 0; + const parameters = queryString.slice(startIndex).split("&"); + + for (let idx = 0; idx < parameters.length; ++idx) { + const pair = parameters[idx].split("="); + if (pair[0]) { + result.set(pair[0], pair[1] ?? ""); + } + } + + return result; +}; 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/application/Context/service/ContextToCamelCaseService.ts b/src/shared/util/ToCamelCase.ts similarity index 87% rename from src/application/Context/service/ContextToCamelCaseService.ts rename to src/shared/util/ToCamelCase.ts index 634a156..358d957 100644 --- a/src/application/Context/service/ContextToCamelCaseService.ts +++ b/src/shared/util/ToCamelCase.ts @@ -7,7 +7,7 @@ * @method * @public */ -export const execute = (name: string): string => +export const toCamelCase = (name: string): string => { const names = name.split(/-|\/|_/); let result = ""; @@ -16,4 +16,4 @@ export const execute = (name: string): string => result += word.charAt(0).toUpperCase() + word.slice(1); } return result; -}; \ No newline at end of file +}; From 813d9acdb9dc9fa13031f7065a361637a87d2597 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 21:54:18 +0900 Subject: [PATCH 16/28] =?UTF-8?q?#155=20=E5=88=A9=E7=94=A8=E9=A0=BB?= =?UTF-8?q?=E5=BA=A6=E3=81=8C=E5=B0=91=E3=81=AA=E3=81=84=E9=96=A2=E6=95=B0?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/application/content/MovieClipContent.ts | 15 --------------- src/application/content/ShapeContent.ts | 15 --------------- src/application/content/TextFieldContent.ts | 15 --------------- src/application/content/VideoContent.ts | 15 --------------- src/domain/service/ScreenCaptureService.test.ts | 5 +++++ src/domain/service/ScreenCaptureService.ts | 2 ++ 6 files changed, 7 insertions(+), 60 deletions(-) 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/domain/service/ScreenCaptureService.test.ts b/src/domain/service/ScreenCaptureService.test.ts index 0a84dc2..732bba5 100644 --- a/src/domain/service/ScreenCaptureService.test.ts +++ b/src/domain/service/ScreenCaptureService.test.ts @@ -34,9 +34,11 @@ describe("ScreenCaptureService Test", () => expect(root.numChildren).toBe(0); expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); await ScreenCaptureService.add(); expect(root.numChildren).toBe(1); expect(root.mouseChildren).toBe(false); + expect(root.mouseEnabled).toBe(false); }); }); @@ -58,14 +60,17 @@ describe("ScreenCaptureService Test", () => $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); ScreenCaptureService.dispose(); expect(root.numChildren).toBe(0); expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); }); }); }); diff --git a/src/domain/service/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts index e27c825..3d7cc6f 100644 --- a/src/domain/service/ScreenCaptureService.ts +++ b/src/domain/service/ScreenCaptureService.ts @@ -49,6 +49,7 @@ export const ScreenCaptureService = { * Mouse operation is forced to stop */ root.mouseChildren = false; + root.mouseEnabled = false; const scale = stage.rendererScale; const config = $getConfig(); @@ -142,5 +143,6 @@ export const ScreenCaptureService = { * Enable Mouse Operation */ root.mouseChildren = true; + root.mouseEnabled = true; } }; From f2380787bd1fda3bfb81393d10725ac88dda6650 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 22:06:54 +0900 Subject: [PATCH 17/28] =?UTF-8?q?#155=20=E3=83=A6=E3=83=8B=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 5 +- src/application/Application.test.ts | 268 ++++++++++++++++++ .../usecase/ApplicationGotoViewUseCase.ts | 7 +- src/domain/entity/DefaultLoader.test.ts | 238 ++++++++++++++++ src/domain/service/LoadingService.ts | 4 +- .../service/ScreenCaptureService.test.ts | 157 +++++++++- src/domain/service/ScreenCaptureService.ts | 4 +- src/domain/service/ViewBinderService.ts | 4 +- .../repository/ContentRepository.test.ts | 156 ++++++++++ 10 files changed, 819 insertions(+), 25 deletions(-) create mode 100644 src/application/Application.test.ts create mode 100644 src/domain/entity/DefaultLoader.test.ts create mode 100644 src/infrastructure/repository/ContentRepository.test.ts diff --git a/.gitignore b/.gitignore index 265f50c..02dd1b0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? +coverage package-lock.json \ No newline at end of file diff --git a/package.json b/package.json index 1fed544..e023f52 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,15 @@ "@types/node": "^24.10.1", "@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/parser": "^8.48.1", - "@vitest/web-worker": "^4.0.14", + "@vitest/coverage-v8": "^4.0.15", + "@vitest/web-worker": "^4.0.15", "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^16.5.0", "jsdom": "^27.2.0", "typescript": "^5.9.3", "vite": "^7.2.6", - "vitest": "^4.0.14", + "vitest": "^4.0.15", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { 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/usecase/ApplicationGotoViewUseCase.ts b/src/application/usecase/ApplicationGotoViewUseCase.ts index 8b4f387..8393952 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.ts @@ -20,8 +20,11 @@ import { ScreenCaptureService } from "../../domain/service/ScreenCaptureService" * @method * @protected */ -export const execute = async (application: Application, name: string = ""): Promise => -{ +export const execute = async ( + application: Application, + name: string = "" +): Promise => { + const config = $getConfig(); const hasLoading = !!config.loading; diff --git a/src/domain/entity/DefaultLoader.test.ts b/src/domain/entity/DefaultLoader.test.ts new file mode 100644 index 0000000..9088bb0 --- /dev/null +++ b/src/domain/entity/DefaultLoader.test.ts @@ -0,0 +1,238 @@ +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); + }); + }); +}); diff --git a/src/domain/service/LoadingService.ts b/src/domain/service/LoadingService.ts index 8921e84..7c0357e 100644 --- a/src/domain/service/LoadingService.ts +++ b/src/domain/service/LoadingService.ts @@ -12,8 +12,8 @@ import { * @description ローディング処理を管理するドメインサービス * Domain service for managing loading operations */ -export const LoadingService = { - +export const LoadingService = +{ /** * @description ローダーのインスタンスを取得または作成 * Get or create loader instance diff --git a/src/domain/service/ScreenCaptureService.test.ts b/src/domain/service/ScreenCaptureService.test.ts index 732bba5..bbd27dc 100644 --- a/src/domain/service/ScreenCaptureService.test.ts +++ b/src/domain/service/ScreenCaptureService.test.ts @@ -1,23 +1,79 @@ import { ScreenCaptureService } from "./ScreenCaptureService"; -import { MovieClip, Shape } from "@next2d/display"; +import { MovieClip, Shape, BitmapData, stage } from "@next2d/display"; import { Context } from "../../application/Context"; -import { $setContext } from "../../application/variable/Context"; +import { $setContext, $getContext } from "../../application/variable/Context"; import { $setConfig } from "../../application/variable/Config"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach } from "vitest"; Object.defineProperty(window, "next2d", { "get": vi.fn().mockReturnValue({ "captureToCanvas": async () => { - return document.createElement("canvas"); + const canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + return canvas; } }) }); describe("ScreenCaptureService Test", () => { + beforeEach(() => + { + $setConfig({ + "platform": "web", + "spa": false, + "stage": { + "width": 800, + "height": 600, + "fps": 60 + } + }); + }); + describe("add", () => { it("should add capture 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 ScreenCaptureService.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 ScreenCaptureService.add(); + }); + + it("should capture and add bitmap 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 ScreenCaptureService.add(); + + expect(root.mouseChildren).toBe(false); + expect(root.mouseEnabled).toBe(false); + }); + + it("should handle bgColor option", async () => { $setConfig({ "platform": "web", @@ -25,26 +81,22 @@ describe("ScreenCaptureService Test", () => "stage": { "width": 800, "height": 600, - "fps": 60 + "fps": 60, + "options": { + "bgColor": "#ffffff" + } } }); const root = new MovieClip(); $setContext(new Context(root)); - expect(root.numChildren).toBe(0); - expect(root.mouseChildren).toBe(true); - expect(root.mouseEnabled).toBe(true); await ScreenCaptureService.add(); - expect(root.numChildren).toBe(1); + expect(root.mouseChildren).toBe(false); - expect(root.mouseEnabled).toBe(false); }); - }); - describe("dispose", () => - { - it("should remove all children and enable mouse", () => + it("should handle empty bgColor option", async () => { $setConfig({ "platform": "web", @@ -52,13 +104,52 @@ describe("ScreenCaptureService Test", () => "stage": { "width": 800, "height": 600, - "fps": 60 + "fps": 60, + "options": { + "bgColor": "" + } } }); const root = new MovieClip(); $setContext(new Context(root)); + await ScreenCaptureService.add(); + + expect(root.mouseChildren).toBe(false); + }); + + it("should draw shape with correct dimensions", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenCaptureService.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 ScreenCaptureService.add(); + const firstCount = root.numChildren; + + await ScreenCaptureService.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()); @@ -72,5 +163,41 @@ describe("ScreenCaptureService Test", () => 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); + + ScreenCaptureService.dispose(); + }); + + it("should handle already empty root", () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + expect(root.numChildren).toBe(0); + ScreenCaptureService.dispose(); + expect(root.numChildren).toBe(0); + expect(root.mouseChildren).toBe(true); + expect(root.mouseEnabled).toBe(true); + }); + + it("should remove children added by add method", async () => + { + const root = new MovieClip(); + $setContext(new Context(root)); + + await ScreenCaptureService.add(); + expect(root.numChildren).toBeGreaterThan(0); + + ScreenCaptureService.dispose(); + expect(root.numChildren).toBe(0); + }); }); }); diff --git a/src/domain/service/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts index 3d7cc6f..aabef57 100644 --- a/src/domain/service/ScreenCaptureService.ts +++ b/src/domain/service/ScreenCaptureService.ts @@ -29,8 +29,8 @@ let cacheY: number = 0; * @description 画面キャプチャーを管理するドメインサービス * Domain service for managing screen capture */ -export const ScreenCaptureService = { - +export const ScreenCaptureService = +{ /** * @description 画面キャプチャーのShapeをStageに追加 * Add Screen Capture Shape to Stage diff --git a/src/domain/service/ViewBinderService.ts b/src/domain/service/ViewBinderService.ts index b06cb25..af620c9 100644 --- a/src/domain/service/ViewBinderService.ts +++ b/src/domain/service/ViewBinderService.ts @@ -9,8 +9,8 @@ import { toCamelCase } from "../../shared/util/ToCamelCase"; * @description ViewとViewModelのバインドを行うドメインサービス * Domain service for binding View and ViewModel */ -export const ViewBinderService = { - +export const ViewBinderService = +{ /** * @description ViewとViewModelをバインドします * Binds View and ViewModel 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 }); + }); + }); +}); From 1b5e079eb3f0e90aa450167e67689bd8366458b0 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 22:54:25 +0900 Subject: [PATCH 18/28] =?UTF-8?q?#155=20=E5=87=A6=E7=90=86=E8=B2=A0?= =?UTF-8?q?=E8=8D=B7=E3=81=AE=E9=AB=98=E3=81=84=E7=AE=87=E6=89=80=E3=82=92?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/application/Application.ts | 8 +-- .../service/QueryStringParserService.ts | 10 ++-- .../ApplicationGotoViewUseCase.test.ts | 3 +- .../usecase/ApplicationGotoViewUseCase.ts | 20 ++++++-- src/application/variable/Cache.ts | 4 +- src/application/variable/Context.ts | 12 +++-- src/application/variable/Query.ts | 4 +- src/domain/service/ScreenCaptureService.ts | 8 +-- src/domain/service/ViewBinderService.ts | 8 +-- src/domain/variable/Loading.ts | 16 +++--- src/infrastructure/dto/ResponseDTO.test.ts | 20 +++++++- src/infrastructure/dto/ResponseDTO.ts | 26 +++++++--- .../repository/ContentRepository.ts | 2 +- .../repository/CustomRepository.ts | 2 +- .../repository/JsonRepository.test.ts | 42 ++++++++++++++-- .../repository/JsonRepository.ts | 28 ++++++----- .../service/RequestCacheCheckService.ts | 17 +++---- .../service/RequestResponseProcessService.ts | 23 ++++----- src/infrastructure/usecase/RequestUseCase.ts | 33 ++++++++----- .../ResponseRemoveVariableUseCase.test.ts | 49 ++++++------------- .../usecase/ResponseRemoveVariableUseCase.ts | 9 ++-- src/infrastructure/variable/RepositoryMap.ts | 12 ++++- src/interface/ILoading.ts | 19 ++++++- 23 files changed, 231 insertions(+), 144 deletions(-) diff --git a/src/application/Application.ts b/src/application/Application.ts index 16b2b24..984a407 100644 --- a/src/application/Application.ts +++ b/src/application/Application.ts @@ -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/service/QueryStringParserService.ts b/src/application/service/QueryStringParserService.ts index 943b613..14d8c2c 100644 --- a/src/application/service/QueryStringParserService.ts +++ b/src/application/service/QueryStringParserService.ts @@ -61,14 +61,14 @@ export const execute = (name: string = ""): IQueryObject => * 任意で設定したQueryStringを分解 * Decompose an arbitrarily set QueryString */ - if (name.indexOf("?") > -1) { - const idx = name.indexOf("?"); - queryString = name.slice(idx); - const parsed = parseQueryString(name.slice(idx + 1)); + const questionIdx = name.indexOf("?"); + if (questionIdx > -1) { + queryString = name.slice(questionIdx); + const parsed = parseQueryString(name.slice(questionIdx + 1)); for (const [key, value] of parsed) { query.set(key, value); } - name = name.slice(0, idx); + name = name.slice(0, questionIdx); } if (name.charAt(0) === ".") { diff --git a/src/application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/usecase/ApplicationGotoViewUseCase.test.ts index 8ce988a..ff49fa3 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.test.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.test.ts @@ -30,7 +30,8 @@ vi.mock("../service/QueryStringParserService", () => ({ })); vi.mock("../../infrastructure/usecase/RequestUseCase", () => ({ - execute: vi.fn().mockResolvedValue([]) + execute: vi.fn().mockResolvedValue([]), + getRequests: vi.fn().mockReturnValue([]) })); vi.mock("./ExecuteCallbackUseCase", () => ({ diff --git a/src/application/usecase/ApplicationGotoViewUseCase.ts b/src/application/usecase/ApplicationGotoViewUseCase.ts index 8393952..d03bdb7 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.ts @@ -3,7 +3,10 @@ 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 } from "../../infrastructure/usecase/RequestUseCase"; +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"; @@ -53,7 +56,8 @@ export const execute = async ( * 前の画面で取得したレスポンスデータを初期化 * Initialize the response data obtained on the previous screen */ - responseRemoveVariableUseCase(application.currentName); + const previousRequests = getRequests(application.currentName); + responseRemoveVariableUseCase(previousRequests); /** * 指定されたパス、もしくはURLからアクセス先を算出 @@ -87,8 +91,8 @@ export const execute = async ( const responses = await requestUseCase(application.currentName); /** - * レスポンス情報をマップに登録 - * Response information is registered on the map + * レスポンス情報をマップに登録し、コールバックを実行 + * Register response information on the map and execute callbacks */ for (let idx = 0; idx < responses.length; ++idx) { @@ -96,6 +100,14 @@ export const execute = async ( if (object.name) { response.set(object.name, object.response); } + + /** + * リクエストごとのコールバック処理を実行 + * Execute callback for each request + */ + if (object.callback) { + await executeCallbackUseCase(object.callback, object.response); + } } if (hasLoading) { 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.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/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/service/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts index aabef57..5017f1b 100644 --- a/src/domain/service/ScreenCaptureService.ts +++ b/src/domain/service/ScreenCaptureService.ts @@ -131,11 +131,11 @@ export const ScreenCaptureService = } /** - * rootの子要素を全て削除 - * Remove all child elements of root + * rootの子要素を全て削除(末尾から削除することでO(n)に最適化) + * Remove all child elements of root (optimized to O(n) by removing from end) */ - while (root.numChildren > 0) { - root.removeChildAt(0); + for (let idx = root.numChildren - 1; idx >= 0; --idx) { + root.removeChildAt(idx); } /** diff --git a/src/domain/service/ViewBinderService.ts b/src/domain/service/ViewBinderService.ts index af620c9..e016d49 100644 --- a/src/domain/service/ViewBinderService.ts +++ b/src/domain/service/ViewBinderService.ts @@ -44,12 +44,12 @@ export const ViewBinderService = await context.view.initialize(); /** - * rootの子要素を全て削除 - * Remove all child elements of root + * rootの子要素を全て削除(末尾から削除することでO(n)に最適化) + * Remove all child elements of root (optimized to O(n) by removing from end) */ const root = context.root; - while (root.numChildren) { - root.removeChildAt(0); + for (let idx = root.numChildren - 1; idx >= 0; --idx) { + root.removeChildAt(idx); } /** diff --git a/src/domain/variable/Loading.ts b/src/domain/variable/Loading.ts index 1d41632..957ee9f 100644 --- a/src/domain/variable/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; }; diff --git a/src/infrastructure/dto/ResponseDTO.test.ts b/src/infrastructure/dto/ResponseDTO.test.ts index 3571bc2..9a6605c 100644 --- a/src/infrastructure/dto/ResponseDTO.test.ts +++ b/src/infrastructure/dto/ResponseDTO.test.ts @@ -5,9 +5,10 @@ describe("ResponseDTOTest", () => { it("execute test case1", () => { - const responseDTO = new ResponseDTO(); + const responseDTO = new ResponseDTO("", null); expect(responseDTO.name).toBe(""); expect(responseDTO.response).toBe(null); + expect(responseDTO.callback).toBeUndefined(); }); it("execute test case2", () => @@ -15,5 +16,22 @@ describe("ResponseDTOTest", () => 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/dto/ResponseDTO.ts b/src/infrastructure/dto/ResponseDTO.ts index d2a3a89..e98e526 100644 --- a/src/infrastructure/dto/ResponseDTO.ts +++ b/src/infrastructure/dto/ResponseDTO.ts @@ -10,8 +10,7 @@ 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 {T} - * @default null + * @type {T} * @readonly * @public */ public readonly response: T; /** - * @param {string} [name=""] - * @param {T} [response] + * @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: T = null as T) + constructor (name: string, response: T, callback?: string | string[]) { - this.name = name; + this.name = name; this.response = response; + this.callback = callback; } } diff --git a/src/infrastructure/repository/ContentRepository.ts b/src/infrastructure/repository/ContentRepository.ts index ee76145..88bfb31 100644 --- a/src/infrastructure/repository/ContentRepository.ts +++ b/src/infrastructure/repository/ContentRepository.ts @@ -22,7 +22,7 @@ export const execute = async (request_object: IRequest): Promise => throw new Error("`path` and `name` must be set for content requests."); } - const cachedResponse = await requestCacheCheckService(request_object); + const cachedResponse = requestCacheCheckService(request_object); if (cachedResponse) { return cachedResponse; } diff --git a/src/infrastructure/repository/CustomRepository.ts b/src/infrastructure/repository/CustomRepository.ts index 2cc56aa..46ce82d 100644 --- a/src/infrastructure/repository/CustomRepository.ts +++ b/src/infrastructure/repository/CustomRepository.ts @@ -32,7 +32,7 @@ export const execute = async (request_object: IRequest): Promise => throw new Error("`class`, `access`, `method` and `name` must be set for custom requests."); } - const cachedResponse = await requestCacheCheckService(request_object); + const cachedResponse = requestCacheCheckService(request_object); if (cachedResponse) { return cachedResponse; } diff --git a/src/infrastructure/repository/JsonRepository.test.ts b/src/infrastructure/repository/JsonRepository.test.ts index 60f93c9..21e4ab2 100644 --- a/src/infrastructure/repository/JsonRepository.test.ts +++ b/src/infrastructure/repository/JsonRepository.test.ts @@ -14,13 +14,15 @@ describe("JsonRepository 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("JsonRepository 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("JsonRepository 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,30 @@ describe("JsonRepository Test", () => expect(responseDTO.name).toBe("JsonRepository"); expect(responseDTO.response).toBe("success fetch json"); }); + + 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"); + }); }); diff --git a/src/infrastructure/repository/JsonRepository.ts b/src/infrastructure/repository/JsonRepository.ts index 2b7fb63..cb806b3 100644 --- a/src/infrastructure/repository/JsonRepository.ts +++ b/src/infrastructure/repository/JsonRepository.ts @@ -8,35 +8,41 @@ import { execute as requestResponseProcessService } from "../service/RequestResp * @description 指定先のJSONを非同期で取得 * Asynchronously obtain JSON of the specified destination * - * @param {IRequest} request_object + * @param {IRequest} requestObject * @return {Promise} + * @throws {Error} path/nameが未設定の場合、HTTPエラーの場合 * @method * @public */ -export const execute = async (request_object: IRequest): Promise => +export const execute = async (requestObject: IRequest): Promise => { - if (!request_object.path || !request_object.name) { + if (!requestObject.path || !requestObject.name) { throw new Error("`path` and `name` must be set for json requests."); } - const cachedResponse = await requestCacheCheckService(request_object); + const cachedResponse = requestCacheCheckService(requestObject); if (cachedResponse) { return cachedResponse; } - const method = normalizeHttpMethod(request_object.method); + const method = normalizeHttpMethod(requestObject.method); const options: RequestInit = { method }; - if (request_object.body && (method === "POST" || method === "PUT")) { - options.body = JSON.stringify(request_object.body); + if (requestObject.body && (method === "POST" || method === "PUT")) { + options.body = JSON.stringify(requestObject.body); } - if (request_object.headers) { - options.headers = request_object.headers; + 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 response = await fetch(request_object.path, options); const value = await response.json(); - return requestResponseProcessService(request_object, value); + return requestResponseProcessService(requestObject, value); }; diff --git a/src/infrastructure/service/RequestCacheCheckService.ts b/src/infrastructure/service/RequestCacheCheckService.ts index 4193c30..7095c2a 100644 --- a/src/infrastructure/service/RequestCacheCheckService.ts +++ b/src/infrastructure/service/RequestCacheCheckService.ts @@ -1,24 +1,23 @@ import type { IRequest } from "../../interface/IRequest"; import { cache } from "../../application/variable/Cache"; import { ResponseDTO } from "../dto/ResponseDTO"; -import { execute as executeCallbackUseCase } from "../../application/usecase/ExecuteCallbackUseCase"; /** * @description キャッシュをチェックし、存在すればキャッシュデータを返却 * Check cache and return cached data if exists * - * @param {IRequest} request_object - * @return {Promise} + * @param {IRequest} requestObject + * @return {ResponseDTO | null} * @method * @public */ -export const execute = async (request_object: IRequest): Promise => +export const execute = (requestObject: IRequest): ResponseDTO | null => { - if (!request_object.cache || !request_object.name) { + if (!requestObject.cache || !requestObject.name) { return null; } - const name = request_object.name; + const name = requestObject.name; if (!cache.size || !cache.has(name)) { return null; @@ -26,9 +25,5 @@ export const execute = async (request_object: IRequest): Promise} + * @param {IRequest} requestObject + * @param {T} value + * @return {ResponseDTO} * @method * @public */ -export const execute = async (request_object: IRequest, value: T): Promise> => +export const execute = (requestObject: IRequest, value: T): ResponseDTO => { - const name = request_object.name as string; + const name = requestObject.name as string; - if (request_object.cache) { + if (requestObject.cache) { cache.set(name, value); } - if (request_object.callback) { - await executeCallbackUseCase(request_object.callback, value); - } - - return new ResponseDTO(name, value); + return new ResponseDTO(name, value, requestObject.callback); }; diff --git a/src/infrastructure/usecase/RequestUseCase.ts b/src/infrastructure/usecase/RequestUseCase.ts index 5e4c9a6..fa5be71 100644 --- a/src/infrastructure/usecase/RequestUseCase.ts +++ b/src/infrastructure/usecase/RequestUseCase.ts @@ -1,10 +1,25 @@ +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 Routing設定で指定したタイプへリクエストを実行 - * Execute requests to the type specified in Routing settings + * @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} @@ -13,22 +28,16 @@ import { repositoryMap } from "../variable/RepositoryMap"; */ export const execute = async (name: string): Promise => { - const responses: ResponseDTO[] = []; 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) { - continue; - } - - const response = await repository(requestObject); - if (response) { - responses.push(response); + if (repository) { + promises.push(repository(requestObject)); } } - return responses; + return Promise.all(promises); }; diff --git a/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts index ecdf3d4..4a88e27 100644 --- a/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.test.ts @@ -1,7 +1,6 @@ import { execute } from "./ResponseRemoveVariableUseCase"; import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; -import { $setConfig } from "../../application/variable/Config"; -import { IConfig } from "../../interface/IConfig"; +import type { IRequest } from "../../interface/IRequest"; import { response } from "../variable/Response"; import { describe, expect, it } from "vitest"; @@ -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); diff --git a/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts index 85e6671..688c5d1 100644 --- a/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts +++ b/src/infrastructure/usecase/ResponseRemoveVariableUseCase.ts @@ -1,5 +1,5 @@ import type { DisplayObject } from "@next2d/display"; -import { execute as routingRequestsParserService } from "../../application/service/RoutingRequestsParserService"; +import type { IRequest } from "../../interface/IRequest"; import { loaderInfoMap } from "../../application/variable/LoaderInfoMap"; import { response } from "../variable/Response"; @@ -7,14 +7,13 @@ 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 = routingRequestsParserService(name); for (let idx = 0; idx < requests.length; ++idx) { const object = requests[idx]; @@ -34,7 +33,7 @@ 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; diff --git a/src/infrastructure/variable/RepositoryMap.ts b/src/infrastructure/variable/RepositoryMap.ts index c2e4421..c43a225 100644 --- a/src/infrastructure/variable/RepositoryMap.ts +++ b/src/infrastructure/variable/RepositoryMap.ts @@ -1,15 +1,23 @@ +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} + * @type {Map} * @protected */ -export const repositoryMap: Map = new Map([ +export const repositoryMap: Map = new Map([ ["json", jsonRepository], ["content", contentRepository], ["custom", customRepository] 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 From 534091c4ddd734d2cb2638c3c1ca982c038525df Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 23:45:52 +0900 Subject: [PATCH 19/28] =?UTF-8?q?#155=20=E3=83=AD=E3=83=BC=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=A2=E3=83=8B=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E4=BF=AE=E6=AD=A3=E3=80=81?= =?UTF-8?q?=E3=83=A6=E3=83=8B=E3=83=83=E3=83=88=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ContentBuilderService.test.ts | 80 +++++++++++- .../service/QueryStringParserService.ts | 4 +- .../ApplicationGotoViewUseCase.test.ts | 91 ++++++++++++++ .../ApplicationInitializeUseCase.test.ts | 39 ++++++ src/application/variable/Context.test.ts | 48 ++++++++ .../variable/PopstateQueue.test.ts | 26 ++++ src/domain/entity/DefaultLoader.test.ts | 56 +++++++++ src/domain/entity/DefaultLoader.ts | 116 +++++++++++++----- src/domain/service/LoadingService.test.ts | 24 ++++ .../service/ScreenCaptureService.test.ts | 91 ++++++++++++++ src/domain/service/ScreenCaptureService.ts | 2 +- src/domain/service/ViewBinderService.test.ts | 27 ++++ .../repository/CustomRepository.test.ts | 67 +++++++++- .../repository/JsonRepository.test.ts | 23 ++++ .../service/RequestCacheCheckService.ts | 2 +- src/shared/util/ToCamelCase.ts | 11 +- 16 files changed, 660 insertions(+), 47 deletions(-) create mode 100644 src/application/variable/Context.test.ts create mode 100644 src/application/variable/PopstateQueue.test.ts 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/service/QueryStringParserService.ts b/src/application/service/QueryStringParserService.ts index 14d8c2c..31198d7 100644 --- a/src/application/service/QueryStringParserService.ts +++ b/src/application/service/QueryStringParserService.ts @@ -18,9 +18,7 @@ export const execute = (name: string = ""): IQueryObject => * 前のシーンのクエリデータを初期化 * Initialize query data from previous scene */ - if (query.size) { - query.clear(); - } + query.clear(); /** * QueryStringがあれば分解 diff --git a/src/application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/usecase/ApplicationGotoViewUseCase.test.ts index ff49fa3..9540607 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.test.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.test.ts @@ -150,4 +150,95 @@ describe("ApplicationGotoViewUseCase 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 { ScreenCaptureService } = await import("../../domain/service/ScreenCaptureService"); + + $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(ScreenCaptureService.add).toHaveBeenCalled(); + expect(LoadingService.start).toHaveBeenCalled(); + expect(LoadingService.end).toHaveBeenCalled(); + expect(ScreenCaptureService.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" }; + vi.mocked(ViewBinderService.bind).mockResolvedValue(mockView as any); + + await execute(mockApplication, "withCallback"); + + expect(executeCallbackUseCase).toHaveBeenCalledWith(mockGotoViewCallback, mockView); + }); }); diff --git a/src/application/usecase/ApplicationInitializeUseCase.test.ts b/src/application/usecase/ApplicationInitializeUseCase.test.ts index 8635644..c27fbd6 100644 --- a/src/application/usecase/ApplicationInitializeUseCase.test.ts +++ b/src/application/usecase/ApplicationInitializeUseCase.test.ts @@ -71,4 +71,43 @@ describe("ApplicationInitializeUseCase", () => 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/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/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/domain/entity/DefaultLoader.test.ts b/src/domain/entity/DefaultLoader.test.ts index 9088bb0..8edf589 100644 --- a/src/domain/entity/DefaultLoader.test.ts +++ b/src/domain/entity/DefaultLoader.test.ts @@ -235,4 +235,60 @@ describe("DefaultLoader Test", () => 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 index 3545bcf..4a00a82 100644 --- a/src/domain/entity/DefaultLoader.ts +++ b/src/domain/entity/DefaultLoader.ts @@ -5,31 +5,50 @@ import { $getContext } from "../../application/variable/Context"; import { Tween, Easing } from "@next2d/ui"; /** - * @description Tweenジョブを取得または作成 - * Get or create Tween job + * @description アニメーションの定数 + * Animation constants + */ +const ANIMATION_DURATION = 0.4; +const ANIMATION_DELAY_INTERVAL = 0.15; + +/** + * @description 拡大アニメーションのジョブを作成 + * Create expand animation job * * @param {Shape} shape - * @param {string} jobName + * @param {number} delay * @return {Job} */ -const getOrCreateJob = (shape: Shape, jobName: string): Job => +const createExpandJob = (shape: Shape, delay: number): Job => { - if (shape.hasLocalVariable(jobName)) { - const job = shape.getLocalVariable(jobName) as Job; - job.stop(); - return job; - } - - const job = Tween.add( + return Tween.add( shape, { "scaleX": 0.1, "scaleY": 0.1, "alpha": 0 }, { "scaleX": 1, "scaleY": 1, "alpha": 1 }, - 0.12, - 0.5, + 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 ); - shape.setLocalVariable(jobName, job); - return job; }; /** @@ -110,20 +129,47 @@ export class DefaultLoader shape.scaleY = 0.1; shape.alpha = 0; - const reduceJob = getOrCreateJob(shape, "reduceJob"); - const expandJob = getOrCreateJob(shape, "expandJob"); + /** + * 既存のジョブを停止してクリア + * 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); - reduceJob.nextJob = expandJob; + // ループ用の拡大アニメーション(遅延なし) + const loopExpandJob = createExpandJob(shape, 0); + + // ジョブチェーンを構築(ループ) expandJob.nextJob = reduceJob; + reduceJob.nextJob = loopExpandJob; + loopExpandJob.nextJob = reduceJob; - if (idx) { - setTimeout((): void => - { - expandJob.start(); - }, 120 * idx); - } else { - expandJob.start(); - } + // ローカル変数に保存(end時に停止するため) + shape.setLocalVariable("expandJob", expandJob); + shape.setLocalVariable("reduceJob", reduceJob); + shape.setLocalVariable("loopExpandJob", loopExpandJob); + + expandJob.start(); if (shape.width === minSize) { continue; @@ -166,14 +212,16 @@ export class DefaultLoader 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(); + /** + * 全てのジョブを停止 + * 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(); + } } } diff --git a/src/domain/service/LoadingService.test.ts b/src/domain/service/LoadingService.test.ts index b60d3bf..ae35395 100644 --- a/src/domain/service/LoadingService.test.ts +++ b/src/domain/service/LoadingService.test.ts @@ -208,4 +208,28 @@ describe("LoadingService Test", () => 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/ScreenCaptureService.test.ts b/src/domain/service/ScreenCaptureService.test.ts index bbd27dc..421028c 100644 --- a/src/domain/service/ScreenCaptureService.test.ts +++ b/src/domain/service/ScreenCaptureService.test.ts @@ -200,4 +200,95 @@ describe("ScreenCaptureService Test", () => 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 ScreenCaptureService.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 ScreenCaptureService.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 ScreenCaptureService.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 ScreenCaptureService.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/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts index 5017f1b..6541d6c 100644 --- a/src/domain/service/ScreenCaptureService.ts +++ b/src/domain/service/ScreenCaptureService.ts @@ -93,7 +93,7 @@ export const ScreenCaptureService = root.addChild(bitmap); } - if (shape.width !== width || shape.width !== height) { + if (shape.width !== width || shape.height !== height) { shape .graphics .clear() diff --git a/src/domain/service/ViewBinderService.test.ts b/src/domain/service/ViewBinderService.test.ts index 1bf73c3..177f32f 100644 --- a/src/domain/service/ViewBinderService.test.ts +++ b/src/domain/service/ViewBinderService.test.ts @@ -254,5 +254,32 @@ describe("ViewBinderService Test", () => 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/infrastructure/repository/CustomRepository.test.ts b/src/infrastructure/repository/CustomRepository.test.ts index 7ea3d45..211bf62 100644 --- a/src/infrastructure/repository/CustomRepository.test.ts +++ b/src/infrastructure/repository/CustomRepository.test.ts @@ -1,10 +1,17 @@ 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 } from "vitest"; +import { describe, expect, it, beforeEach } from "vitest"; describe("CustomRepository Test", () => { + beforeEach(() => + { + packages.clear(); + cache.clear(); + }); + it("execute public test", async () => { // mock @@ -25,7 +32,6 @@ describe("CustomRepository Test", () => } }; - packages.clear(); packages.set("CustomClass", CustomClass); const responseDTO = await execute(object); @@ -53,11 +59,66 @@ describe("CustomRepository Test", () => } }; - packages.clear(); 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/JsonRepository.test.ts b/src/infrastructure/repository/JsonRepository.test.ts index 21e4ab2..98ea73b 100644 --- a/src/infrastructure/repository/JsonRepository.test.ts +++ b/src/infrastructure/repository/JsonRepository.test.ts @@ -139,4 +139,27 @@ describe("JsonRepository Test", () => 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/service/RequestCacheCheckService.ts b/src/infrastructure/service/RequestCacheCheckService.ts index 7095c2a..89e9971 100644 --- a/src/infrastructure/service/RequestCacheCheckService.ts +++ b/src/infrastructure/service/RequestCacheCheckService.ts @@ -19,7 +19,7 @@ export const execute = (requestObject: IRequest): ResponseDTO | null => const name = requestObject.name; - if (!cache.size || !cache.has(name)) { + if (!cache.has(name)) { return null; } diff --git a/src/shared/util/ToCamelCase.ts b/src/shared/util/ToCamelCase.ts index 358d957..e395aaa 100644 --- a/src/shared/util/ToCamelCase.ts +++ b/src/shared/util/ToCamelCase.ts @@ -1,3 +1,12 @@ +/** + * @description セパレータ用の正規表現(モジュールレベルで定義して再コンパイルを回避) + * Regex for separators (defined at module level to avoid recompilation) + * + * @type {RegExp} + * @private + */ +const SEPARATOR_REGEX = /-|\/|_/; + /** * @description キャメルケースに変換 * Convert to CamelCase @@ -9,7 +18,7 @@ */ export const toCamelCase = (name: string): string => { - const names = name.split(/-|\/|_/); + const names = name.split(SEPARATOR_REGEX); let result = ""; for (let idx = 0; idx < names.length; ++idx) { const word = names[idx]; From 41e771944bfe5329e3f44ce61977928d2f4a6cbf Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 30 Dec 2025 08:34:43 +0900 Subject: [PATCH 20/28] =?UTF-8?q?#155=20=E3=82=A4=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=95=E3=82=A7=E3=83=BC=E3=82=B9=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interface/IAccessType.ts | 5 +++++ src/interface/IConfig.ts | 6 +++++- src/interface/IRequest.ts | 7 +------ src/interface/IRequestType.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 src/interface/IAccessType.ts 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/IConfig.ts b/src/interface/IConfig.ts index 6f55294..466a77a 100644 --- a/src/interface/IConfig.ts +++ b/src/interface/IConfig.ts @@ -2,13 +2,17 @@ import type { IStage } from "./IStage"; import type { IRouting } from "./IRouting"; import type { IGotoView } from "./IGotoView"; +interface IBaseConfig { + [key: string]: any +} + /** * @description アプリケーション設定のインターフェース * Application configuration interface * * @interface */ -export interface IConfig { +export interface IConfig extends IBaseConfig { /** * @description プラットフォーム識別子 * Platform identifier diff --git a/src/interface/IRequest.ts b/src/interface/IRequest.ts index 7715aaf..4f0c540 100644 --- a/src/interface/IRequest.ts +++ b/src/interface/IRequest.ts @@ -1,4 +1,5 @@ import type { IRequestType } from "./IRequestType"; +import type { IAccessType } from "./IAccessType"; /** * @description HTTPメソッドの型 @@ -6,12 +7,6 @@ import type { IRequestType } from "./IRequestType"; */ export type IHttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; -/** - * @description カスタムリクエストのアクセス方法 - * Access method for custom request - */ -export type IAccessType = "static" | "instance"; - /** * @description リクエスト設定のインターフェース * Request configuration interface 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 From 15b325a5a4d628ff7ea71963ec4b37d95c0cde6f Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 19 Jan 2026 21:28:52 +0900 Subject: [PATCH 21/28] =?UTF-8?q?#155=20=E5=88=A9=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 22 +++++----- .../service/QueryStringParserService.test.ts | 38 ++++++++--------- .../service/QueryStringParserService.ts | 9 ---- src/shared/util/ParseQueryString.test.ts | 42 ------------------- src/shared/util/ParseQueryString.ts | 24 ----------- 5 files changed, 30 insertions(+), 105 deletions(-) delete mode 100644 src/shared/util/ParseQueryString.test.ts delete mode 100644 src/shared/util/ParseQueryString.ts diff --git a/package.json b/package.json index e023f52..28aacff 100644 --- a/package.json +++ b/package.json @@ -32,19 +32,19 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", - "@vitest/coverage-v8": "^4.0.15", - "@vitest/web-worker": "^4.0.15", - "eslint": "^9.39.1", + "@eslint/js": "^9.39.2", + "@types/node": "^25.0.9", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", + "@vitest/coverage-v8": "^4.0.17", + "@vitest/web-worker": "^4.0.17", + "eslint": "^9.39.2", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.5.0", - "jsdom": "^27.2.0", + "globals": "^17.0.0", + "jsdom": "^27.4.0", "typescript": "^5.9.3", - "vite": "^7.2.6", - "vitest": "^4.0.15", + "vite": "^7.3.1", + "vitest": "^4.0.17", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { diff --git a/src/application/service/QueryStringParserService.test.ts b/src/application/service/QueryStringParserService.test.ts index e00e675..1fb656e 100644 --- a/src/application/service/QueryStringParserService.test.ts +++ b/src/application/service/QueryStringParserService.test.ts @@ -16,7 +16,7 @@ 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("QueryStringParserService", () => 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("QueryStringParserService", () => 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("QueryStringParserService", () => 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("QueryStringParserService", () => 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("QueryStringParserService", () => 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("QueryStringParserService", () => 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"); }); diff --git a/src/application/service/QueryStringParserService.ts b/src/application/service/QueryStringParserService.ts index 31198d7..2a5f54a 100644 --- a/src/application/service/QueryStringParserService.ts +++ b/src/application/service/QueryStringParserService.ts @@ -1,7 +1,6 @@ import type { IQueryObject } from "../../interface/IQueryObject"; import { $getConfig } from "../variable/Config"; import { query } from "../variable/Query"; -import { parseQueryString } from "../../shared/util/ParseQueryString"; /** * @description 指定されたQueryStringか、URLのQueryStringをquery mapに登録 @@ -27,10 +26,6 @@ export const execute = (name: string = ""): IQueryObject => let queryString = ""; if (!name && location.search) { queryString = location.search; - const parsed = parseQueryString(queryString); - for (const [key, value] of parsed) { - query.set(key, value); - } } const config = $getConfig(); @@ -62,10 +57,6 @@ export const execute = (name: string = ""): IQueryObject => const questionIdx = name.indexOf("?"); if (questionIdx > -1) { queryString = name.slice(questionIdx); - const parsed = parseQueryString(name.slice(questionIdx + 1)); - for (const [key, value] of parsed) { - query.set(key, value); - } name = name.slice(0, questionIdx); } diff --git a/src/shared/util/ParseQueryString.test.ts b/src/shared/util/ParseQueryString.test.ts deleted file mode 100644 index 3858ee5..0000000 --- a/src/shared/util/ParseQueryString.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { parseQueryString } from "./ParseQueryString"; -import { describe, expect, it } from "vitest"; - -describe("ParseQueryString Test", () => -{ - it("should parse single key-value pair", () => - { - const result = parseQueryString("key=value"); - expect(result.get("key")).toBe("value"); - }); - - it("should parse multiple key-value pairs", () => - { - const result = parseQueryString("foo=bar&baz=qux"); - expect(result.get("foo")).toBe("bar"); - expect(result.get("baz")).toBe("qux"); - }); - - it("should handle leading question mark", () => - { - const result = parseQueryString("?key=value"); - expect(result.get("key")).toBe("value"); - }); - - it("should handle empty value", () => - { - const result = parseQueryString("key="); - expect(result.get("key")).toBe(""); - }); - - it("should handle key without value", () => - { - const result = parseQueryString("key"); - expect(result.get("key")).toBe(""); - }); - - it("should return empty map for empty string", () => - { - const result = parseQueryString(""); - expect(result.size).toBe(0); - }); -}); diff --git a/src/shared/util/ParseQueryString.ts b/src/shared/util/ParseQueryString.ts deleted file mode 100644 index e289443..0000000 --- a/src/shared/util/ParseQueryString.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @description QueryStringをパースしてMapに変換 - * Parse QueryString and convert to Map - * - * @param {string} queryString - * @return {Map} - * @method - * @public - */ -export const parseQueryString = (queryString: string): Map => -{ - const result = new Map(); - const startIndex = queryString.charAt(0) === "?" ? 1 : 0; - const parameters = queryString.slice(startIndex).split("&"); - - for (let idx = 0; idx < parameters.length; ++idx) { - const pair = parameters[idx].split("="); - if (pair[0]) { - result.set(pair[0], pair[1] ?? ""); - } - } - - return result; -}; From 3a00511a073abac46bf587fedf9b91e5364242a6 Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 20 Jan 2026 20:57:07 +0900 Subject: [PATCH 22/28] =?UTF-8?q?#155=20App=E3=81=AEUseCase=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3(=E8=B2=A0=E8=8D=B7=E7=AE=87=E6=89=80?= =?UTF-8?q?=E3=81=AE=E8=BB=BD=E6=B8=9B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 45 ++++-- .../ApplicationGotoViewUseCase.test.ts | 16 +- .../usecase/ApplicationGotoViewUseCase.ts | 49 +++--- src/domain/service/ScreenCaptureService.ts | 148 ------------------ ...e.test.ts => ScreenOverlayService.test.ts} | 48 +++--- src/domain/service/ScreenOverlayService.ts | 99 ++++++++++++ src/domain/service/ViewBinderService.test.ts | 6 +- src/domain/service/ViewBinderService.ts | 16 +- tsconfig.json | 1 + 9 files changed, 192 insertions(+), 236 deletions(-) delete mode 100644 src/domain/service/ScreenCaptureService.ts rename src/domain/service/{ScreenCaptureService.test.ts => ScreenOverlayService.test.ts} (87%) create mode 100644 src/domain/service/ScreenOverlayService.ts diff --git a/README.md b/README.md index a7a3e6f..ae84f4d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ src/ │ │ └── DefaultLoader.ts │ ├── service/ # Domain services │ │ ├── LoadingService.ts -│ │ ├── ScreenCaptureService.ts +│ │ ├── ScreenOverlayService.ts │ │ └── ViewBinderService.ts │ └── variable/ # Domain state ├── infrastructure/ # Infrastructure Layer @@ -234,14 +234,19 @@ graph TD GotoView --> LoadingCheck{use loading?
Default: true} - LoadingCheck -->|YES| LoadingStart[Start Loading] - LoadingCheck -->|NO| OnExit - LoadingStart --> OnExit + LoadingCheck -->|YES| ScreenOverlay[Screen Overlay] + LoadingCheck -->|NO| RemoveResponse + ScreenOverlay --> LoadingStart[Start Loading] + LoadingStart --> RemoveResponse - OnExit[Previous View: onExit] --> RemoveViewFromStage[Remove Previous View from Stage] - RemoveViewFromStage --> RemoveResponse[Remove Previous Response Data] + RemoveResponse[Remove Previous Response Data] --> ParseQuery[Parse Query String] + ParseQuery --> UpdateHistory{SPA mode?} - RemoveResponse --> RequestType[Request Type] + 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] @@ -260,23 +265,31 @@ graph TD Cached -->|YES| RegisterResponse GlobalData --> RegisterResponse - RegisterResponse[Register Response Data] --> ViewModelInit[ViewModel: initialize] + 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 --> OnEnter[View: onEnter] + AddToStage --> GotoViewCallback{gotoView callback?} - OnEnter --> CallbackCheck{use callback?
Default: empty} - - CallbackCheck -->|YES| CallbackStart[Start Callback] - CallbackCheck -->|NO| LoadingEndCheck - CallbackStart --> LoadingEndCheck + GotoViewCallback -->|YES| ExecGotoViewCallback[Execute gotoView Callback] + GotoViewCallback -->|NO| LoadingEndCheck + ExecGotoViewCallback --> LoadingEndCheck LoadingEndCheck{use loading?
Default: true} LoadingEndCheck -->|YES| LoadingEnd[End Loading] - LoadingEndCheck -->|NO| StartDrawing - LoadingEnd --> StartDrawing + LoadingEndCheck -->|NO| OnEnter + LoadingEnd --> DisposeOverlay[Dispose Screen Overlay] + DisposeOverlay --> OnEnter + + OnEnter[View: onEnter] --> StartDrawing StartDrawing[Start Drawing] -->|Response| User diff --git a/src/application/usecase/ApplicationGotoViewUseCase.test.ts b/src/application/usecase/ApplicationGotoViewUseCase.test.ts index 9540607..41c8fbc 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.test.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.test.ts @@ -6,8 +6,8 @@ import { $setConfig } from "../variable/Config"; import { $setContext } from "../variable/Context"; import { response } from "../../infrastructure/variable/Response"; -vi.mock("../../domain/service/ScreenCaptureService", () => ({ - ScreenCaptureService: { +vi.mock("../../domain/service/ScreenOverlayService", () => ({ + ScreenOverlayService: { add: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } @@ -40,7 +40,9 @@ vi.mock("./ExecuteCallbackUseCase", () => ({ vi.mock("../../domain/service/ViewBinderService", () => ({ ViewBinderService: { - bind: vi.fn().mockResolvedValue(null), + bind: vi.fn().mockResolvedValue({ + onEnter: vi.fn().mockResolvedValue(undefined) + }), unbind: vi.fn().mockResolvedValue(undefined) } })); @@ -156,7 +158,7 @@ describe("ApplicationGotoViewUseCase Test", () => const { execute: queryStringParserService } = await import("../service/QueryStringParserService"); const { execute: requestUseCase } = await import("../../infrastructure/usecase/RequestUseCase"); const { LoadingService } = await import("../../domain/service/LoadingService"); - const { ScreenCaptureService } = await import("../../domain/service/ScreenCaptureService"); + const { ScreenOverlayService } = await import("../../domain/service/ScreenOverlayService"); $setConfig({ platform: "web", @@ -179,10 +181,10 @@ describe("ApplicationGotoViewUseCase Test", () => await execute(mockApplication, "home"); - expect(ScreenCaptureService.add).toHaveBeenCalled(); + expect(ScreenOverlayService.add).toHaveBeenCalled(); expect(LoadingService.start).toHaveBeenCalled(); expect(LoadingService.end).toHaveBeenCalled(); - expect(ScreenCaptureService.dispose).toHaveBeenCalled(); + expect(ScreenOverlayService.dispose).toHaveBeenCalled(); }); it("execute test case5: response with callback should execute callback", async () => @@ -234,7 +236,7 @@ describe("ApplicationGotoViewUseCase Test", () => }); vi.mocked(requestUseCase).mockResolvedValue([]); - const mockView = { name: "MockView" }; + const mockView = { name: "MockView", onEnter: vi.fn().mockResolvedValue(undefined) }; vi.mocked(ViewBinderService.bind).mockResolvedValue(mockView as any); await execute(mockApplication, "withCallback"); diff --git a/src/application/usecase/ApplicationGotoViewUseCase.ts b/src/application/usecase/ApplicationGotoViewUseCase.ts index d03bdb7..f05073f 100644 --- a/src/application/usecase/ApplicationGotoViewUseCase.ts +++ b/src/application/usecase/ApplicationGotoViewUseCase.ts @@ -11,7 +11,7 @@ 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 { ScreenCaptureService } from "../../domain/service/ScreenCaptureService"; +import { ScreenOverlayService } from "../../domain/service/ScreenOverlayService"; /** * @description 指定されたパス、もしくはURLのクラスを起動 @@ -36,7 +36,7 @@ export const execute = async ( * 現時点の描画をキャプチャーして表示 * Capture and display the current drawing */ - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); /** * ローディング表示を起動 @@ -45,13 +45,6 @@ export const execute = async ( await LoadingService.start(); } - /** - * 現在の画面のViewとViewModelをunbind - * Unbind the View and ViewModel of the current screen - */ - const context = $getContext(); - await ViewBinderService.unbind(context); - /** * 前の画面で取得したレスポンスデータを初期化 * Initialize the response data obtained on the previous screen @@ -89,6 +82,7 @@ export const execute = async ( * Execute request processing set by routing.json */ const responses = await requestUseCase(application.currentName); + // await new Promise((resolve) => setTimeout(resolve, 3000)); /** * レスポンス情報をマップに登録し、コールバックを実行 @@ -110,6 +104,27 @@ export const execute = async ( } } + /** + * 現在の画面の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 ViewBinderService.bind(context, application.currentName); + + /** + * コールバック設定があれば実行 + * Execute callback settings if any. + */ + if (view && config.gotoView) { + await executeCallbackUseCase(config.gotoView.callback, view); + } + if (hasLoading) { /** * ローディング表示を終了 @@ -121,20 +136,12 @@ export const execute = async ( * 前の画面のキャプチャーを終了 * End previous screen capture */ - ScreenCaptureService.dispose(); + ScreenOverlayService.dispose(); } /** - * ViewとViewModelを起動 - * Start View and ViewModel + * 画面表示時の処理を実行 + * Execute processing when the screen is displayed */ - const view = await ViewBinderService.bind(context, application.currentName); - - /** - * コールバック設定があれば実行 - * Execute callback settings if any. - */ - if (view && config.gotoView) { - await executeCallbackUseCase(config.gotoView.callback, view); - } + await view.onEnter(); }; diff --git a/src/domain/service/ScreenCaptureService.ts b/src/domain/service/ScreenCaptureService.ts deleted file mode 100644 index 6541d6c..0000000 --- a/src/domain/service/ScreenCaptureService.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { $getConfig } from "../../application/variable/Config"; -import { $getContext } from "../../application/variable/Context"; -import { Matrix } from "@next2d/geom"; -import { - stage, - BitmapData, - Shape -} from "@next2d/display"; - -/** - * @type {Shape} - * @private - */ -const shape: Shape = new Shape(); - -/** - * @type {number} - * @private - */ -let cacheX: number = 0; - -/** - * @type {number} - * @private - */ -let cacheY: number = 0; - -/** - * @description 画面キャプチャーを管理するドメインサービス - * Domain service for managing screen capture - */ -export const ScreenCaptureService = -{ - /** - * @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 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 - ), - "bgColor": config.stage.options?.bgColor || null, - "bgAlpha": config.stage.options?.bgColor !== "" ? 1 : 0 - }); - - 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.height !== 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); - }, - - /** - * @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) - */ - for (let idx = root.numChildren - 1; idx >= 0; --idx) { - root.removeChildAt(idx); - } - - /** - * マウス操作を有効化 - * Enable Mouse Operation - */ - root.mouseChildren = true; - root.mouseEnabled = true; - } -}; diff --git a/src/domain/service/ScreenCaptureService.test.ts b/src/domain/service/ScreenOverlayService.test.ts similarity index 87% rename from src/domain/service/ScreenCaptureService.test.ts rename to src/domain/service/ScreenOverlayService.test.ts index 421028c..12fc697 100644 --- a/src/domain/service/ScreenCaptureService.test.ts +++ b/src/domain/service/ScreenOverlayService.test.ts @@ -1,4 +1,4 @@ -import { ScreenCaptureService } from "./ScreenCaptureService"; +import { ScreenOverlayService } from "./ScreenOverlayService"; import { MovieClip, Shape, BitmapData, stage } from "@next2d/display"; import { Context } from "../../application/Context"; import { $setContext, $getContext } from "../../application/variable/Context"; @@ -16,7 +16,7 @@ Object.defineProperty(window, "next2d", { }) }); -describe("ScreenCaptureService Test", () => +describe("ScreenOverlayService Test", () => { beforeEach(() => { @@ -33,7 +33,7 @@ describe("ScreenCaptureService Test", () => describe("add", () => { - it("should add capture shape to stage", async () => + it("should add overlay shape to stage", async () => { const root = new MovieClip(); $setContext(new Context(root)); @@ -41,7 +41,7 @@ describe("ScreenCaptureService Test", () => expect(root.numChildren).toBe(0); expect(root.mouseChildren).toBe(true); expect(root.mouseEnabled).toBe(true); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.numChildren).toBe(1); expect(root.mouseChildren).toBe(false); expect(root.mouseEnabled).toBe(false); @@ -56,10 +56,10 @@ describe("ScreenCaptureService Test", () => } as unknown as Context; $setContext(mockContext); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); }); - it("should capture and add bitmap when rectangle has size", async () => + it("should add overlay and disable mouse when rectangle has size", async () => { const root = new MovieClip(); const shape = new Shape(); @@ -67,7 +67,7 @@ describe("ScreenCaptureService Test", () => root.addChild(shape); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.mouseChildren).toBe(false); expect(root.mouseEnabled).toBe(false); @@ -91,7 +91,7 @@ describe("ScreenCaptureService Test", () => const root = new MovieClip(); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.mouseChildren).toBe(false); }); @@ -114,17 +114,17 @@ describe("ScreenCaptureService Test", () => const root = new MovieClip(); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.mouseChildren).toBe(false); }); - it("should draw shape with correct dimensions", async () => + it("should draw overlay shape with correct dimensions", async () => { const root = new MovieClip(); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.numChildren).toBeGreaterThan(0); }); @@ -134,10 +134,10 @@ describe("ScreenCaptureService Test", () => const root = new MovieClip(); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); const firstCount = root.numChildren; - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.numChildren).toBeGreaterThanOrEqual(firstCount); }); @@ -158,8 +158,8 @@ describe("ScreenCaptureService Test", () => expect(root.numChildren).toBe(2); expect(root.mouseChildren).toBe(false); expect(root.mouseEnabled).toBe(false); - ScreenCaptureService.dispose(); - expect(root.numChildren).toBe(0); + ScreenOverlayService.dispose(); + expect(root.numChildren).toBe(2); expect(root.mouseChildren).toBe(true); expect(root.mouseEnabled).toBe(true); }); @@ -173,7 +173,7 @@ describe("ScreenCaptureService Test", () => } as unknown as Context; $setContext(mockContext); - ScreenCaptureService.dispose(); + ScreenOverlayService.dispose(); }); it("should handle already empty root", () => @@ -182,21 +182,21 @@ describe("ScreenCaptureService Test", () => $setContext(new Context(root)); expect(root.numChildren).toBe(0); - ScreenCaptureService.dispose(); + ScreenOverlayService.dispose(); expect(root.numChildren).toBe(0); expect(root.mouseChildren).toBe(true); expect(root.mouseEnabled).toBe(true); }); - it("should remove children added by add method", async () => + it("should remove overlay shape added by add method", async () => { const root = new MovieClip(); $setContext(new Context(root)); - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); expect(root.numChildren).toBeGreaterThan(0); - ScreenCaptureService.dispose(); + ScreenOverlayService.dispose(); expect(root.numChildren).toBe(0); }); }); @@ -209,7 +209,7 @@ describe("ScreenCaptureService Test", () => $setContext(new Context(root)); // First call sets initial cache values - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); // Modify stage properties to create non-zero tx const originalRendererWidth = stage.rendererWidth; @@ -230,7 +230,7 @@ describe("ScreenCaptureService Test", () => }); // This should trigger the tx cache update branch - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); // Restore original values Object.defineProperty(stage, "rendererWidth", { @@ -253,7 +253,7 @@ describe("ScreenCaptureService Test", () => $setContext(new Context(root)); // First call sets initial cache values - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); // Modify stage properties to create non-zero ty const originalRendererHeight = stage.rendererHeight; @@ -274,7 +274,7 @@ describe("ScreenCaptureService Test", () => }); // This should trigger the ty cache update branch - await ScreenCaptureService.add(); + await ScreenOverlayService.add(); // Restore original values Object.defineProperty(stage, "rendererHeight", { 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 index 177f32f..fb1dd4d 100644 --- a/src/domain/service/ViewBinderService.test.ts +++ b/src/domain/service/ViewBinderService.test.ts @@ -26,7 +26,6 @@ describe("ViewBinderService Test", () => let viewModelInitialized = false; let viewInitialized = false; - let viewEntered = false; class TestViewModel extends ViewModel { @@ -45,7 +44,6 @@ describe("ViewBinderService Test", () => async onEnter () { - viewEntered = true; } async onExit () @@ -63,14 +61,12 @@ describe("ViewBinderService Test", () => expect(viewModelInitialized).toBe(false); expect(viewInitialized).toBe(false); - expect(viewEntered).toBe(false); expect(root.numChildren).toBe(0); await ViewBinderService.bind(context, "test"); expect(viewModelInitialized).toBe(true); expect(viewInitialized).toBe(true); - expect(viewEntered).toBe(true); expect(root.numChildren).toBe(1); expect(context.view).toBeInstanceOf(TestView); expect(context.viewModel).toBeInstanceOf(TestViewModel); @@ -135,7 +131,7 @@ describe("ViewBinderService Test", () => await ViewBinderService.bind(context, "test"); - expect(root.numChildren).toBe(1); + expect(root.numChildren).toBe(3); expect(context.view).toBeInstanceOf(TestView); }); }); diff --git a/src/domain/service/ViewBinderService.ts b/src/domain/service/ViewBinderService.ts index e016d49..d19c75b 100644 --- a/src/domain/service/ViewBinderService.ts +++ b/src/domain/service/ViewBinderService.ts @@ -43,27 +43,13 @@ export const ViewBinderService = context.view = new ViewClass(context.viewModel); await context.view.initialize(); - /** - * rootの子要素を全て削除(末尾から削除することでO(n)に最適化) - * Remove all child elements of root (optimized to O(n) by removing from end) - */ - const root = context.root; - for (let idx = root.numChildren - 1; idx >= 0; --idx) { - root.removeChildAt(idx); - } - /** * stageの一番背面にviewをセット * Set the view at the very back of the stage */ + const root = context.root; root.addChildAt(context.view, 0); - /** - * 画面表示時の処理を実行 - * Execute processing when the screen is displayed - */ - await context.view.onEnter(); - return context.view; }, 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" From 1b0cb2f0a4806efe0b8ccb138768f10aca3330bf Mon Sep 17 00:00:00 2001 From: ienaga Date: Tue, 20 Jan 2026 21:10:27 +0900 Subject: [PATCH 23/28] #155 update packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 28aacff..4cd6be9 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@types/node": "^25.0.9", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", "@vitest/coverage-v8": "^4.0.17", "@vitest/web-worker": "^4.0.17", "eslint": "^9.39.2", From 8026113edd629b81e12b3f6a5a98cece1acd6b37 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 4 Feb 2026 00:03:23 +0900 Subject: [PATCH 24/28] =?UTF-8?q?#155=20AI=E3=81=AE=E3=82=B9=E3=82=AD?= =?UTF-8?q?=E3=83=AB=E7=94=A8=E3=81=ABspecs=E3=82=92=E8=BF=BD=E5=8A=A0(WIP?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/en/index.md | 211 ++++++++++++++++++++++++++++++++++++ specs/ja/config.md | 220 +++++++++++++++++++++++++++++++++++++ specs/ja/index.md | 257 ++++++++++++++++++++++++++++++++++++++++++++ specs/ja/routing.md | 243 +++++++++++++++++++++++++++++++++++++++++ specs/ja/view.md | 223 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1154 insertions(+) create mode 100644 specs/en/index.md create mode 100644 specs/ja/config.md create mode 100644 specs/ja/index.md create mode 100644 specs/ja/routing.md create mode 100644 specs/ja/view.md diff --git a/specs/en/index.md b/specs/en/index.md new file mode 100644 index 0000000..78bebe1 --- /dev/null +++ b/specs/en/index.md @@ -0,0 +1,211 @@ +# 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 +- **Single Page Application**: URL-based scene management +- **Open Animation Tool Integration**: Seamless integration with Open Animation Tool assets +- **TypeScript Support**: Type-safe development + +## Quick Start + +### Create Project + +```bash +npx create-next2d-app my-app +cd my-app +npm install +npm run dev +``` + +### Directory Structure + +``` +my-app/ +├── src/ +│ ├── config/ +│ │ └── config.json # Configuration +│ ├── view/ +│ │ └── TopView.ts # View class +│ └── index.ts # Entry point +├── asset/ +│ └── content.json # Open Animation Tool output +└── package.json +``` + +## Core Concepts + +### View + +Views are responsible for screen display and correspond to MovieClips created with Open Animation Tool. + +```typescript +import { View } from "@next2d/framework"; + +export class TopView extends View +{ + constructor() + { + super(); + } + + async $setup(): Promise + { + // Initial setup + } + + $ready(): void + { + // Ready for display + } + + $dispose(): void + { + // Cleanup + } +} +``` + +### ViewModel + +ViewModel handles business logic for the View. + +```typescript +import { ViewModel } from "@next2d/framework"; + +export class TopViewModel extends ViewModel +{ + async $setup(): Promise + { + // Initialize data + } + + $bind(): void + { + // Bind to View + } +} +``` + +### Routing + +Configure routing in config.json: + +```json +{ + "routing": { + "top": { + "path": "/", + "view": "TopView" + }, + "about": { + "path": "/about", + "view": "AboutView" + }, + "detail": { + "path": "/detail/{id}", + "view": "DetailView" + } + } +} +``` + +## 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 | + +## Related Documentation + +### Basics +- [View/ViewModel](./view.md) - Screen display and data binding +- [Routing](./routing.md) - URL-based screen transitions +- [Configuration](./config.md) - Environment and stage settings + +### Next2D Player Integration +- [Next2D Player](../../player/specs/en/index.md) - Rendering engine +- [MovieClip](../../player/specs/en/movie-clip.md) - Timeline animation +- [Event System](../../player/specs/en/events.md) - User interaction diff --git a/specs/ja/config.md b/specs/ja/config.md new file mode 100644 index 0000000..c81d883 --- /dev/null +++ b/specs/ja/config.md @@ -0,0 +1,220 @@ +# 設定ファイル(config.json) + +Next2D Frameworkの設定はconfig.jsonで管理します。 + +## 基本構造 + +```json +{ + "stage": { + "width": 1920, + "height": 1080, + "fps": 60, + "options": {} + }, + "router": { + "mode": "history" + }, + "routing": { + "top": { + "path": "/", + "view": "TopView" + } + }, + "content": { + "base": "/asset/" + }, + "loading": { + "callback": "loadingCallback" + } +} +``` + +## stage設定 + +ステージ(Canvas)の設定: + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| width | Number | 240 | ステージ幅 | +| height | Number | 240 | ステージ高さ | +| fps | Number | 60 | フレームレート | +| options | Object | {} | 追加オプション | + +### optionsの詳細 + +```json +{ + "stage": { + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "bgColor": "#000000", + "fullScreen": true, + "tagId": "canvas-container", + "base": "http://example.com/" + } + } +} +``` + +| プロパティ | 説明 | +|-----------|------| +| bgColor | 背景色 | +| fullScreen | フルスクリーンモード | +| tagId | Canvasを配置するDOM要素のID | +| base | ベースURL | + +## router設定 + +ルーティングの動作設定: + +```json +{ + "router": { + "mode": "history" + } +} +``` + +| プロパティ | 値 | 説明 | +|-----------|------|------| +| mode | "history" | HTML5 History API使用(推奨) | +| mode | "hash" | ハッシュベースのルーティング | + +## routing設定 + +各ルートの定義: + +```json +{ + "routing": { + "routeName": { + "path": "/path/{param}", + "view": "ViewClassName", + "viewModel": "ViewModelClassName", + "transition": { + "type": "fade", + "duration": 0.3 + }, + "content": "content.json", + "private": false + } + } +} +``` + +| プロパティ | 型 | 必須 | 説明 | +|-----------|------|------|------| +| path | String | ○ | URLパス | +| view | String | ○ | Viewクラス名 | +| viewModel | String | | ViewModelクラス名 | +| transition | Object | | 遷移アニメーション | +| content | String | | コンテンツJSONファイル | +| private | Boolean | | 認証必要フラグ | + +## content設定 + +コンテンツの読み込み設定: + +```json +{ + "content": { + "base": "/asset/", + "cache": true + } +} +``` + +| プロパティ | 説明 | +|-----------|------| +| base | コンテンツのベースパス | +| cache | キャッシュを使用するか | + +## loading設定 + +ローディング中の処理: + +```json +{ + "loading": { + "callback": "loadingCallback", + "defaultPercent": 10 + } +} +``` + +グローバル関数を定義: + +```typescript +window.loadingCallback = (percent: number): void => { + console.log(`ロード中: ${percent}%`); + // ローディングバーの更新など +}; +``` + +## 環境変数 + +環境ごとに設定を切り替える: + +```json +// config.json +{ + "development": { + "stage": { "width": 800, "height": 600 }, + "api": { "base": "http://localhost:3000" } + }, + "production": { + "stage": { "width": 1920, "height": 1080 }, + "api": { "base": "https://api.example.com" } + } +} +``` + +ビルド時に環境を指定: + +```bash +npm run build -- --env=production +``` + +## 複数設定ファイル + +機能ごとに分割することも可能: + +``` +config/ +├── config.json # メイン設定 +├── routing.json # ルーティング設定 +└── stage.json # ステージ設定 +``` + +メイン設定でインポート: + +```json +{ + "$import": { + "routing": "./routing.json", + "stage": "./stage.json" + } +} +``` + +## 設定値の取得 + +コード内で設定値を取得: + +```typescript +import { config } from "@next2d/framework"; + +// ステージ設定 +const stageWidth = config.stage.width; + +// カスタム設定 +const apiBase = config.api.base; +``` + +## 関連項目 + +- [ルーティング](./routing.md) +- [View/ViewModel](./view.md) diff --git a/specs/ja/index.md b/specs/ja/index.md new file mode 100644 index 0000000..573e72e --- /dev/null +++ b/specs/ja/index.md @@ -0,0 +1,257 @@ +# Next2D Framework + +Next2D Frameworkは、Next2D Playerを用いたアプリケーション開発のためのMVVMフレームワークです。シングルページアプリケーション(SPA)のためのルーティング、View/ViewModel管理、環境設定管理などの機能を提供します。 + +## 主な特徴 + +- **MVVMパターン**: Model-View-ViewModelパターンによる関心の分離 +- **シングルページアプリケーション**: URLベースのシーン管理 +- **Open Animation Tool連携**: Open Animation Toolで作成したアセットとの連携 +- **TypeScriptサポート**: 型安全な開発が可能 +- **アトミックデザイン**: 再利用可能なコンポーネント設計を推奨 + +## MVVMアーキテクチャ + +Next2D FrameworkはMVVM(Model-View-ViewModel)パターンを採用しています。 + +```mermaid +flowchart TB + subgraph User["User"] + direction TB + end + + subgraph View["View"] + direction TB + MC["MovieClip
(Open Animation Tool)"] + subgraph Components["UI Components"] + BTN["Button"] + TF["TextField"] + SP["Sprite"] + end + end + + subgraph ViewModel["ViewModel"] + direction LR + Props["Properties"] + Methods["Methods"] + Commands["Commands"] + end + + subgraph Model["Model"] + direction LR + API["API Client"] + Store["Data Store"] + Entity["Entities"] + end + + User -->|"Input"| View + View -->|"Data Binding
Commands"| ViewModel + ViewModel -->|"Business Logic
Data Access"| Model + Model -->|"Data"| ViewModel + ViewModel -->|"Update"| View +``` + +### 各レイヤーの役割 + +| レイヤー | 役割 | 担当 | +|----------|------|------| +| **View** | UI表示、ユーザー入力の受付 | デザイナー/アニメーター | +| **ViewModel** | 表示ロジック、状態管理、Viewへのデータ提供 | プログラマー | +| **Model** | ビジネスロジック、データアクセス、API通信 | プログラマー | + +### MVVMの利点 + +1. **関心の分離**: UI(View)とロジック(ViewModel/Model)が分離 +2. **テスト容易性**: ViewModelは単体テストが容易 +3. **チーム開発**: デザイナーとプログラマーが並行作業可能 +4. **再利用性**: ViewModelは異なるViewで再利用可能 + +## アトミックデザイン + +Next2D Frameworkでは、UIコンポーネントの設計にアトミックデザインを推奨しています。 + +```mermaid +flowchart TB + subgraph Pages["Pages (View)"] + subgraph Templates["Templates"] + subgraph Organisms["Organisms"] + subgraph Molecules["Molecules"] + subgraph Atoms["Atoms"] + Btn["Button"] + Txt["Text"] + Img["Image"] + Icon["Icon"] + end + end + end + end + end + + style Pages fill:#e1f5fe + style Templates fill:#b3e5fc + style Organisms fill:#81d4fa + style Molecules fill:#4fc3f7 + style Atoms fill:#29b6f6 +``` + +### 各レベルの説明 + +| レベル | 説明 | 例 | +|--------|------|-----| +| **Atoms** | 最小単位のUI要素 | ボタン、テキストフィールド、アイコン、ラベル | +| **Molecules** | Atomsを組み合わせた機能単位 | 検索フォーム(入力+ボタン)、メニュー項目 | +| **Organisms** | 独立した機能を持つUI領域 | ヘッダー、ナビゲーション、カード一覧 | +| **Templates** | ページのレイアウト構造 | 2カラムレイアウト、ダッシュボードレイアウト | +| **Pages** | 実際のコンテンツを含むページ | トップページ、詳細ページ(= View) | + +### Open Animation Toolでの実装 + +Open Animation Toolでは、シンボルを階層構造で管理することでアトミックデザインを実現: + +``` +Library +├── atoms/ +│ ├── btn_primary.json # プライマリボタン +│ ├── btn_secondary.json # セカンダリボタン +│ ├── input_text.json # テキスト入力 +│ └── icon_*.json # 各種アイコン +├── molecules/ +│ ├── search_form.json # 検索フォーム +│ ├── menu_item.json # メニュー項目 +│ └── card_header.json # カードヘッダー +├── organisms/ +│ ├── header.json # ヘッダー +│ ├── navigation.json # ナビゲーション +│ └── card_list.json # カード一覧 +└── templates/ + ├── layout_main.json # メインレイアウト + └── layout_detail.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** | 画面表示完了後のコールバック | + +## クイックスタート + +### プロジェクトの作成 + +```bash +npx create-next2d-app my-app +cd my-app +npm install +npm run dev +``` + +### ディレクトリ構造 + +``` +my-app/ +├── src/ +│ ├── config/ +│ │ └── config.json # 環境設定 +│ ├── view/ +│ │ └── TopView.ts # Viewクラス +│ ├── viewmodel/ +│ │ └── TopViewModel.ts # ViewModelクラス +│ ├── model/ +│ │ └── UserModel.ts # Modelクラス +│ └── index.ts # エントリーポイント +├── asset/ +│ └── content.json # Open Animation Tool出力 +└── package.json +``` + +## 関連ドキュメント + +### 基本 +- [View/ViewModel](./view.md) - 画面表示とデータバインディング +- [ルーティング](./routing.md) - URLベースの画面遷移 +- [設定ファイル](./config.md) - 環境設定とステージ設定 + +### Next2D Player連携 +- [Next2D Player](../../player/specs/ja/index.md) - レンダリングエンジン +- [MovieClip](../../player/specs/ja/movie-clip.md) - タイムラインアニメーション +- [イベントシステム](../../player/specs/ja/events.md) - ユーザーインタラクション diff --git a/specs/ja/routing.md b/specs/ja/routing.md new file mode 100644 index 0000000..36515b3 --- /dev/null +++ b/specs/ja/routing.md @@ -0,0 +1,243 @@ +# ルーティング + +Next2D Frameworkは、シングルページアプリケーション(SPA)のためのURLベースのルーティングを提供します。 + +## 基本設定 + +config.jsonでルートを定義します: + +```json +{ + "routing": { + "top": { + "path": "/", + "view": "TopView" + }, + "about": { + "path": "/about", + "view": "AboutView", + "viewModel": "AboutViewModel" + } + } +} +``` + +## ルート定義 + +### 基本的なルート + +```json +{ + "routing": { + "home": { + "path": "/", + "view": "HomeView" + } + } +} +``` + +### パラメータ付きルート + +URLパラメータを使用できます: + +```json +{ + "routing": { + "userDetail": { + "path": "/user/{id}", + "view": "UserDetailView" + }, + "articleDetail": { + "path": "/article/{category}/{id}", + "view": "ArticleDetailView" + } + } +} +``` + +Viewでパラメータを取得: + +```typescript +import { router } from "@next2d/framework"; + +export class UserDetailView extends View +{ + $ready(): void + { + // URLパラメータを取得 + const userId = router.params.id; + console.log("ユーザーID:", userId); + } +} +``` + +### クエリパラメータ + +```typescript +// /search?q=next2d&page=1 の場合 +const query = router.query; +console.log(query.q); // "next2d" +console.log(query.page); // "1" +``` + +## 画面遷移 + +### router.push() + +新しい履歴を追加して遷移: + +```typescript +import { router } from "@next2d/framework"; + +// パスで遷移 +router.push("/about"); + +// パラメータ付き +router.push("/user/123"); + +// クエリパラメータ付き +router.push("/search?q=next2d"); +``` + +### router.replace() + +現在の履歴を置き換えて遷移: + +```typescript +// ログイン後にホームに遷移(戻るボタンでログイン画面に戻れなくする) +router.replace("/home"); +``` + +### router.back() + +前の画面に戻る: + +```typescript +router.back(); +``` + +### router.forward() + +次の画面に進む: + +```typescript +router.forward(); +``` + +## ナビゲーションガード + +### beforeEach + +画面遷移前にチェックを行います: + +```typescript +import { app } from "@next2d/framework"; + +app.beforeEach((to, from, next) => { + // 認証チェック + if (to.path !== "/login" && !isAuthenticated()) { + next("/login"); + return; + } + next(); +}); +``` + +### afterEach + +画面遷移後の処理: + +```typescript +app.afterEach((to, from) => { + // アナリティクスに送信 + analytics.page(to.path); +}); +``` + +## 遷移アニメーション + +config.jsonでトランジションを設定: + +```json +{ + "routing": { + "top": { + "path": "/", + "view": "TopView", + "transition": { + "type": "fade", + "duration": 0.3 + } + } + } +} +``` + +View内でカスタムトランジション: + +```typescript +export class SampleView extends View +{ + // 表示アニメーション + async $inTransition(): Promise + { + this.content.alpha = 0; + await this.tween(this.content, { alpha: 1 }, 0.3); + } + + // 非表示アニメーション + async $outTransition(): Promise + { + await this.tween(this.content, { alpha: 0 }, 0.3); + } +} +``` + +## 404エラー処理 + +```json +{ + "routing": { + "notFound": { + "path": "*", + "view": "NotFoundView" + } + } +} +``` + +## プログラムによるルート取得 + +```typescript +import { router } from "@next2d/framework"; + +// 現在のパス +console.log(router.currentPath); // "/user/123" + +// 現在のルート名 +console.log(router.currentRoute); // "userDetail" + +// 全ルートの取得 +const routes = router.routes; +``` + +## ディープリンク対応 + +Next2D Frameworkは、ハッシュモードとヒストリーモードの両方をサポートします: + +```json +{ + "router": { + "mode": "history" // または "hash" + } +} +``` + +- **historyモード**: `/about`, `/user/123` +- **hashモード**: `/#/about`, `/#/user/123` + +## 関連項目 + +- [View/ViewModel](./view.md) +- [設定ファイル](./config.md) diff --git a/specs/ja/view.md b/specs/ja/view.md new file mode 100644 index 0000000..671e10a --- /dev/null +++ b/specs/ja/view.md @@ -0,0 +1,223 @@ +# View と ViewModel + +Next2D FrameworkはMVVM(Model-View-ViewModel)パターンを採用しています。 + +## View + +Viewは画面の表示を担当するクラスです。Open Animation Toolで作成したMovieClipをコンテンツとして持ちます。 + +### 基本構造 + +```typescript +import { View } from "@next2d/framework"; + +export class SampleView extends View +{ + constructor() + { + super(); + } + + // 非同期の初期設定 + async $setup(): Promise + { + // APIからデータを取得など + } + + // 表示準備完了時 + $ready(): void + { + // イベントリスナーの設定など + } + + // 画面遷移前の破棄処理 + $dispose(): void + { + // リソースの解放など + } +} +``` + +### ライフサイクル + +1. `constructor`: インスタンス生成 +2. `$setup()`: 非同期の初期化処理(await可能) +3. `$ready()`: 表示準備完了、イベント設定 +4. `$dispose()`: 画面遷移時の破棄処理 + +### コンテンツへのアクセス + +```typescript +$ready(): void +{ + // contentはOpen Animation Toolで作成したMovieClip + const button = this.content.getChildByName("submitButton"); + button.addEventListener("click", () => { + this.onSubmit(); + }); + + const textField = this.content.getChildByName("nameInput"); + textField.text = "初期値"; +} +``` + +### 画面遷移 + +```typescript +import { router } from "@next2d/framework"; + +// 指定パスに遷移 +router.push("/about"); + +// パラメータ付きで遷移 +router.push("/detail/123"); + +// 履歴を置き換え +router.replace("/home"); + +// 戻る +router.back(); +``` + +## ViewModel + +ViewModelはViewのビジネスロジックとデータバインディングを担当します。 + +### 基本構造 + +```typescript +import { ViewModel } from "@next2d/framework"; + +export class SampleViewModel extends ViewModel +{ + private _name: string = ""; + + async $setup(): Promise + { + // データの初期化 + const response = await fetch("/api/user"); + const data = await response.json(); + this._name = data.name; + } + + $bind(): void + { + // Viewへのデータバインド + const textField = this.view.content.getChildByName("nameText"); + textField.text = this._name; + } + + get name(): string + { + return this._name; + } + + set name(value: string) + { + this._name = value; + // UIの更新 + const textField = this.view.content.getChildByName("nameText"); + textField.text = value; + } +} +``` + +### ViewとViewModelの連携 + +config.jsonでの設定: + +```json +{ + "routing": { + "sample": { + "path": "/sample", + "view": "SampleView", + "viewModel": "SampleViewModel" + } + } +} +``` + +View側からViewModelへのアクセス: + +```typescript +export class SampleView extends View +{ + $ready(): void + { + // ViewModelへの参照 + const vm = this.viewModel as SampleViewModel; + + const button = this.content.getChildByName("updateButton"); + button.addEventListener("click", () => { + vm.name = "新しい名前"; + }); + } +} +``` + +## コンポーネントの再利用 + +### アトミックデザイン + +コンポーネントを以下のように分類することを推奨します: + +- **Atoms**: 最小単位(ボタン、テキストフィールドなど) +- **Molecules**: Atomsの組み合わせ +- **Organisms**: 複雑なUIコンポーネント +- **Templates**: ページのレイアウト +- **Pages**: 実際のページ(View) + +### 共通コンポーネントの例 + +```typescript +// BaseButton.ts +export class BaseButton +{ + private _mc: MovieClip; + + constructor(mc: MovieClip) + { + this._mc = mc; + this.setup(); + } + + private setup(): void + { + this._mc.addEventListener("rollOver", () => { + this._mc.gotoAndStop("over"); + }); + + this._mc.addEventListener("rollOut", () => { + this._mc.gotoAndStop("up"); + }); + + this._mc.addEventListener("mouseDown", () => { + this._mc.gotoAndStop("down"); + }); + } + + onClick(handler: Function): void + { + this._mc.addEventListener("click", handler); + } +} +``` + +使用例: + +```typescript +$ready(): void +{ + const buttonMc = this.content.getChildByName("myButton"); + const button = new BaseButton(buttonMc); + button.onClick(() => { + console.log("ボタンがクリックされました"); + }); +} +``` + +## 関連項目 + +- [ルーティング](./routing.md) +- [設定ファイル](./config.md) From 8f38887b8990c6387caf91215cbce67b0827e9ec Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 5 Feb 2026 08:39:26 +0900 Subject: [PATCH 25/28] =?UTF-8?q?#155=20AI=E3=81=AEskill=E7=94=A8=E3=81=AE?= =?UTF-8?q?specs=E3=82=92=E8=BF=BD=E5=8A=A0(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/cn/animation-tool.md | 140 +++++++++++ specs/cn/config.md | 331 ++++++++++++++++++++++++++ specs/cn/index.md | 345 ++++++++++++++++++++++++++++ specs/cn/routing.md | 388 +++++++++++++++++++++++++++++++ specs/cn/view.md | 459 +++++++++++++++++++++++++++++++++++++ specs/en/animation-tool.md | 140 +++++++++++ specs/en/config.md | 331 ++++++++++++++++++++++++++ specs/en/index.md | 328 ++++++++++++++++++-------- specs/en/routing.md | 388 +++++++++++++++++++++++++++++++ specs/en/view.md | 459 +++++++++++++++++++++++++++++++++++++ specs/ja/animation-tool.md | 140 +++++++++++ specs/ja/config.md | 397 ++++++++++++++++++++------------ specs/ja/index.md | 326 ++++++++++++++++---------- specs/ja/routing.md | 407 +++++++++++++++++++++----------- specs/ja/view.md | 458 +++++++++++++++++++++++++++--------- 15 files changed, 4436 insertions(+), 601 deletions(-) create mode 100644 specs/cn/animation-tool.md create mode 100644 specs/cn/config.md create mode 100644 specs/cn/index.md create mode 100644 specs/cn/routing.md create mode 100644 specs/cn/view.md create mode 100644 specs/en/animation-tool.md create mode 100644 specs/en/config.md create mode 100644 specs/en/routing.md create mode 100644 specs/en/view.md create mode 100644 specs/ja/animation-tool.md 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 index 78bebe1..ea52560 100644 --- a/specs/en/index.md +++ b/specs/en/index.md @@ -5,111 +5,158 @@ Next2D Framework is an MVVM framework for building applications with Next2D Play ## 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 -- **Open Animation Tool Integration**: Seamless integration with Open Animation Tool assets +- **Animation Tool Integration**: Seamless integration with Animation Tool assets - **TypeScript Support**: Type-safe development +- **Atomic Design**: Recommended component design for reusability -## Quick Start - -### Create Project - -```bash -npx create-next2d-app my-app -cd my-app -npm install -npm run dev -``` +## Architecture Overview -### Directory Structure +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 ``` -my-app/ -├── src/ -│ ├── config/ -│ │ └── config.json # Configuration -│ ├── view/ -│ │ └── TopView.ts # View class -│ └── index.ts # Entry point -├── asset/ -│ └── content.json # Open Animation Tool output -└── package.json -``` - -## Core Concepts -### View +### Layer Responsibilities -Views are responsible for screen display and correspond to MovieClips created with Open Animation Tool. +| 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 | -```typescript -import { View } from "@next2d/framework"; +### Dependency Direction -export class TopView extends View -{ - constructor() - { - super(); - } +Following Clean Architecture principles, dependencies always point inward (toward the Domain layer). - async $setup(): Promise - { - // Initial setup - } +- **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 - $ready(): void - { - // Ready for display - } +## Directory Structure - $dispose(): void - { - // Cleanup - } -} ``` - -### ViewModel - -ViewModel handles business logic for the View. - -```typescript -import { ViewModel } from "@next2d/framework"; - -export class TopViewModel extends ViewModel -{ - async $setup(): Promise - { - // Initialize data - } - - $bind(): void - { - // Bind to View - } -} -``` - -### Routing - -Configure routing in config.json: - -```json -{ - "routing": { - "top": { - "path": "/", - "view": "TopView" - }, - "about": { - "path": "/about", - "view": "AboutView" - }, - "detail": { - "path": "/detail/{id}", - "view": "DetailView" - } - } -} +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 @@ -198,14 +245,101 @@ graph TD | **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](./view.md) - Screen display and data binding -- [Routing](./routing.md) - URL-based screen transitions -- [Configuration](./config.md) - Environment and stage settings +- [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](../../player/specs/en/index.md) - Rendering engine -- [MovieClip](../../player/specs/en/movie-clip.md) - Timeline animation -- [Event System](../../player/specs/en/events.md) - User interaction +- [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 index c81d883..9d615c5 100644 --- a/specs/ja/config.md +++ b/specs/ja/config.md @@ -1,220 +1,331 @@ -# 設定ファイル(config.json) +# 設定ファイル -Next2D Frameworkの設定はconfig.jsonで管理します。 +Next2D Frameworkの設定は3つのJSONファイルで管理します。 -## 基本構造 +## ファイル構成 + +``` +src/config/ +├── stage.json # 表示領域の設定 +├── config.json # 環境設定 +└── routing.json # ルーティング設定 +``` + +## stage.json + +表示領域(Stage)の設定を行うJSONファイルです。 ```json { - "stage": { "width": 1920, "height": 1080, "fps": 60, - "options": {} - }, - "router": { - "mode": "history" - }, - "routing": { - "top": { - "path": "/", - "view": "TopView" + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "transparent" } - }, - "content": { - "base": "/asset/" - }, - "loading": { - "callback": "loadingCallback" - } } ``` -## stage設定 +### プロパティ -ステージ(Canvas)の設定: +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `width` | number | 240 | 表示領域の幅 | +| `height` | number | 240 | 表示領域の高さ | +| `fps` | number | 60 | 1秒間に何回描画するか(1〜60) | +| `options` | object | null | オプション設定 | + +### options設定 | プロパティ | 型 | デフォルト | 説明 | |-----------|------|----------|------| -| width | Number | 240 | ステージ幅 | -| height | Number | 240 | ステージ高さ | -| fps | Number | 60 | フレームレート | -| options | Object | {} | 追加オプション | +| `fullScreen` | boolean | false | Stageで設定した幅と高さを超えて画面全体に描画 | +| `tagId` | string | null | IDを指定すると、指定したIDのエレメント内で描画を行う | +| `bgColor` | string | "transparent" | 背景色を16進数で指定。デフォルトは無色透明 | + +## config.json -### optionsの詳細 +環境ごとの設定を管理するファイルです。`local`、`dev`、`stg`、`prd`、`all`と区切られており、`all`以外は任意の環境名です。 ```json { - "stage": { - "width": 1920, - "height": 1080, - "fps": 60, - "options": { - "bgColor": "#000000", - "fullScreen": true, - "tagId": "canvas-container", - "base": "http://example.com/" + "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"] + } } - } } ``` -| プロパティ | 説明 | -|-----------|------| -| bgColor | 背景色 | -| fullScreen | フルスクリーンモード | -| tagId | Canvasを配置するDOM要素のID | -| base | ベースURL | +### all設定 + +`all`はどの環境でも書き出される共通変数です。 -## router設定 +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `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設定 -```json -{ - "router": { - "mode": "history" - } +ビルド時の`--platform`で指定した値がセットされます。 + +対応値: `macos`, `windows`, `linux`, `ios`, `android`, `web` + +```typescript +import { config } from "@/config/Config"; + +if (config.platform === "ios") { + // iOS固有の処理 } ``` -| プロパティ | 値 | 説明 | -|-----------|------|------| -| mode | "history" | HTML5 History API使用(推奨) | -| mode | "hash" | ハッシュベースのルーティング | +## routing.json -## routing設定 - -各ルートの定義: +ルーティングの設定ファイルです。詳細は[ルーティング](/ja/reference/framework/routing)を参照してください。 ```json { - "routing": { - "routeName": { - "path": "/path/{param}", - "view": "ViewClassName", - "viewModel": "ViewModelClassName", - "transition": { - "type": "fade", - "duration": 0.3 - }, - "content": "content.json", - "private": false + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/top.json", + "name": "TopText" + } + ] + }, + "home": { + "requests": [] } - } } ``` -| プロパティ | 型 | 必須 | 説明 | -|-----------|------|------|------| -| path | String | ○ | URLパス | -| view | String | ○ | Viewクラス名 | -| viewModel | String | | ViewModelクラス名 | -| transition | Object | | 遷移アニメーション | -| content | String | | コンテンツJSONファイル | -| private | Boolean | | 認証必要フラグ | +## 設定値の取得 -## content設定 +コード内で設定値を取得するには`config`オブジェクトを使用します。 -コンテンツの読み込み設定: +### Config.tsの例 -```json -{ - "content": { - "base": "/asset/", - "cache": true - } +```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 +}; ``` -| プロパティ | 説明 | -|-----------|------| -| base | コンテンツのベースパス | -| cache | キャッシュを使用するか | +### 使用例 -## loading設定 +```typescript +import { config } from "@/config/Config"; + +// ステージ設定 +const stageWidth = config.stage.width; +const stageHeight = config.stage.height; -ローディング中の処理: +// API設定 +const apiEndPoint = config.api.endPoint; -```json -{ - "loading": { - "callback": "loadingCallback", - "defaultPercent": 10 - } -} +// SPA設定 +const isSpa = config.spa; ``` -グローバル関数を定義: +## ローディング画面 + +`loading.callback`で設定したクラスの`start`関数と`end`関数が呼び出されます。 ```typescript -window.loadingCallback = (percent: number): void => { - console.log(`ロード中: ${percent}%`); - // ローディングバーの更新など -}; +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で順次実行されます。 -```json -// config.json +```typescript +import { app } from "@next2d/framework"; +import { Shape, stage } from "@next2d/display"; + +export class Background { - "development": { - "stage": { "width": 800, "height": 600 }, - "api": { "base": "http://localhost:3000" } - }, - "production": { - "stage": { "width": 1920, "height": 1080 }, - "api": { "base": "https://api.example.com" } - } + 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=production -``` +環境を指定してビルド: -## 複数設定ファイル +```bash +# ローカル環境 +npm run build -- --env=local -機能ごとに分割することも可能: +# 開発環境 +npm run build -- --env=dev +# 本番環境 +npm run build -- --env=prd ``` -config/ -├── config.json # メイン設定 -├── routing.json # ルーティング設定 -└── stage.json # ステージ設定 + +プラットフォームを指定: + +```bash +npm run build -- --platform=web +npm run build -- --platform=ios +npm run build -- --platform=android ``` -メイン設定でインポート: +## 設定例 + +### 完全な設定ファイルの例 + +#### stage.json ```json { - "$import": { - "routing": "./routing.json", - "stage": "./stage.json" - } + "width": 1920, + "height": 1080, + "fps": 60, + "options": { + "fullScreen": true, + "tagId": null, + "bgColor": "#1461A0" + } } ``` -## 設定値の取得 - -コード内で設定値を取得: - -```typescript -import { config } from "@next2d/framework"; - -// ステージ設定 -const stageWidth = config.stage.width; +#### config.json -// カスタム設定 -const apiBase = config.api.base; +```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"] + } + } +} ``` ## 関連項目 -- [ルーティング](./routing.md) -- [View/ViewModel](./view.md) +- [ルーティング](/ja/reference/framework/routing) +- [View/ViewModel](/ja/reference/framework/view) diff --git a/specs/ja/index.md b/specs/ja/index.md index 573e72e..774e9ee 100644 --- a/specs/ja/index.md +++ b/specs/ja/index.md @@ -5,127 +5,158 @@ Next2D Frameworkは、Next2D Playerを用いたアプリケーション開発の ## 主な特徴 - **MVVMパターン**: Model-View-ViewModelパターンによる関心の分離 +- **クリーンアーキテクチャ**: 依存性の逆転と疎結合な設計 - **シングルページアプリケーション**: URLベースのシーン管理 -- **Open Animation Tool連携**: Open Animation Toolで作成したアセットとの連携 +- **Animation Tool連携**: Animation Toolで作成したアセットとの連携 - **TypeScriptサポート**: 型安全な開発が可能 - **アトミックデザイン**: 再利用可能なコンポーネント設計を推奨 -## MVVMアーキテクチャ +## アーキテクチャ概要 -Next2D FrameworkはMVVM(Model-View-ViewModel)パターンを採用しています。 +このプロジェクトはクリーンアーキテクチャとMVVMパターンを組み合わせて実装されています。 ```mermaid -flowchart TB - subgraph User["User"] - direction TB +graph TB + subgraph ViewLayer["View Layer"] + View["View"] + ViewModel["ViewModel"] + UI["UI Components"] end - subgraph View["View"] - direction TB - MC["MovieClip
(Open Animation Tool)"] - subgraph Components["UI Components"] - BTN["Button"] - TF["TextField"] - SP["Sprite"] - end + subgraph InterfaceLayer["Interface Layer"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IResponse"] end - subgraph ViewModel["ViewModel"] - direction LR - Props["Properties"] - Methods["Methods"] - Commands["Commands"] + subgraph ApplicationLayer["Application Layer"] + UseCase["UseCase"] end - subgraph Model["Model"] - direction LR - API["API Client"] - Store["Data Store"] - Entity["Entities"] + subgraph DomainLayer["Domain Layer"] + DomainLogic["Domain Logic"] + DomainService["Service"] end - User -->|"Input"| View - View -->|"Data Binding
Commands"| ViewModel - ViewModel -->|"Business Logic
Data Access"| Model - Model -->|"Data"| ViewModel - ViewModel -->|"Update"| View + 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** | UI表示、ユーザー入力の受付 | デザイナー/アニメーター | -| **ViewModel** | 表示ロジック、状態管理、Viewへのデータ提供 | プログラマー | -| **Model** | ビジネスロジック、データアクセス、API通信 | プログラマー | - -### MVVMの利点 +| **View** | `view/*`, `ui/*` | 画面の構造と表示を担当 | +| **ViewModel** | `view/*` | ViewとModelの橋渡し、イベントハンドリング | +| **Interface** | `interface/*` | 抽象化レイヤー、型定義 | +| **Application** | `model/application/*/usecase/*` | ビジネスロジックの実装(UseCase) | +| **Domain** | `model/domain/*` | コアビジネスルール | +| **Infrastructure** | `model/infrastructure/repository/*` | データアクセス、外部API連携 | -1. **関心の分離**: UI(View)とロジック(ViewModel/Model)が分離 -2. **テスト容易性**: ViewModelは単体テストが容易 -3. **チーム開発**: デザイナーとプログラマーが並行作業可能 -4. **再利用性**: ViewModelは異なるViewで再利用可能 +### 依存関係の方向 -## アトミックデザイン +クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かいます。 -Next2D Frameworkでは、UIコンポーネントの設計にアトミックデザインを推奨しています。 +- **View層**: インターフェースを通じてApplication層を使用 +- **Application層**: インターフェースを通じてDomain層とInfrastructure層を使用 +- **Domain層**: 何にも依存しない(純粋なビジネスロジック) +- **Infrastructure層**: Domain層のインターフェースを実装 -```mermaid -flowchart TB - subgraph Pages["Pages (View)"] - subgraph Templates["Templates"] - subgraph Organisms["Organisms"] - subgraph Molecules["Molecules"] - subgraph Atoms["Atoms"] - Btn["Button"] - Txt["Text"] - Img["Image"] - Icon["Icon"] - end - end - end - end - end +## ディレクトリ構造 - style Pages fill:#e1f5fe - style Templates fill:#b3e5fc - style Organisms fill:#81d4fa - style Molecules fill:#4fc3f7 - style Atoms fill:#29b6f6 ``` - -### 各レベルの説明 - -| レベル | 説明 | 例 | -|--------|------|-----| -| **Atoms** | 最小単位のUI要素 | ボタン、テキストフィールド、アイコン、ラベル | -| **Molecules** | Atomsを組み合わせた機能単位 | 検索フォーム(入力+ボタン)、メニュー項目 | -| **Organisms** | 独立した機能を持つUI領域 | ヘッダー、ナビゲーション、カード一覧 | -| **Templates** | ページのレイアウト構造 | 2カラムレイアウト、ダッシュボードレイアウト | -| **Pages** | 実際のコンテンツを含むページ | トップページ、詳細ページ(= View) | - -### Open Animation Toolでの実装 - -Open Animation Toolでは、シンボルを階層構造で管理することでアトミックデザインを実現: - -``` -Library -├── atoms/ -│ ├── btn_primary.json # プライマリボタン -│ ├── btn_secondary.json # セカンダリボタン -│ ├── input_text.json # テキスト入力 -│ └── icon_*.json # 各種アイコン -├── molecules/ -│ ├── search_form.json # 検索フォーム -│ ├── menu_item.json # メニュー項目 -│ └── card_header.json # カードヘッダー -├── organisms/ -│ ├── header.json # ヘッダー -│ ├── navigation.json # ナビゲーション -│ └── card_list.json # カード一覧 -└── templates/ - ├── layout_main.json # メインレイアウト - └── layout_detail.json # 詳細レイアウト +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 ``` ## フレームワークフローチャート @@ -214,6 +245,65 @@ graph TD | **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; + } + } +} +``` + ## クイックスタート ### プロジェクトの作成 @@ -222,36 +312,34 @@ graph TD npx create-next2d-app my-app cd my-app npm install -npm run dev +npm start ``` -### ディレクトリ構造 +### View/ViewModelの自動生成 +```bash +npm run generate ``` -my-app/ -├── src/ -│ ├── config/ -│ │ └── config.json # 環境設定 -│ ├── view/ -│ │ └── TopView.ts # Viewクラス -│ ├── viewmodel/ -│ │ └── TopViewModel.ts # ViewModelクラス -│ ├── model/ -│ │ └── UserModel.ts # Modelクラス -│ └── index.ts # エントリーポイント -├── asset/ -│ └── content.json # Open Animation Tool出力 -└── package.json -``` + +このコマンドは`routing.json`のトッププロパティを解析し、対応するViewとViewModelクラスを生成します。 + +## ベストプラクティス + +1. **インターフェース優先**: 具象型ではなく、常にインターフェースに依存 +2. **単一責任の原則**: 各クラスは1つの責務のみを持つ +3. **依存性注入**: コンストラクタで依存を注入 +4. **エラーハンドリング**: Repository層で適切にエラーを処理 +5. **型安全性**: `any`型を避け、明示的な型定義を使用 ## 関連ドキュメント ### 基本 -- [View/ViewModel](./view.md) - 画面表示とデータバインディング -- [ルーティング](./routing.md) - URLベースの画面遷移 -- [設定ファイル](./config.md) - 環境設定とステージ設定 +- [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](../../player/specs/ja/index.md) - レンダリングエンジン -- [MovieClip](../../player/specs/ja/movie-clip.md) - タイムラインアニメーション -- [イベントシステム](../../player/specs/ja/events.md) - ユーザーインタラクション +- [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 index 36515b3..28f1f63 100644 --- a/specs/ja/routing.md +++ b/specs/ja/routing.md @@ -1,243 +1,388 @@ # ルーティング -Next2D Frameworkは、シングルページアプリケーション(SPA)のためのURLベースのルーティングを提供します。 +Next2D FrameworkはシングルページアプリケーションとしてURLでシーンを制御できます。ルーティングは`routing.json`で設定します。 ## 基本設定 -config.jsonでルートを定義します: +ルーティングのトッププロパティは英数字とスラッシュが使用できます。スラッシュをキーにCamelCaseでViewクラスにアクセスします。 ```json { - "routing": { "top": { - "path": "/", - "view": "TopView" + "requests": [] }, - "about": { - "path": "/about", - "view": "AboutView", - "viewModel": "AboutViewModel" + "home": { + "requests": [] + }, + "quest/list": { + "requests": [] } - } } ``` +上記の場合: +- `top` → `TopView`クラス +- `home` → `HomeView`クラス +- `quest/list` → `QuestListView`クラス + ## ルート定義 ### 基本的なルート ```json { - "routing": { - "home": { - "path": "/", - "view": "HomeView" + "top": { + "requests": [] } - } } ``` -### パラメータ付きルート +アクセス: `https://example.com/` または `https://example.com/top` + +### セカンドレベルプロパティ + +| プロパティ | 型 | デフォルト | 説明 | +|-----------|------|----------|------| +| `private` | boolean | false | URLでの直接アクセスを制御。trueの場合、URLでアクセスするとTopViewが読み込まれる | +| `requests` | array | null | Viewがbindされる前にリクエストを送信 | -URLパラメータを使用できます: +### プライベートルート + +URLでの直接アクセスを禁止したい場合: ```json { - "routing": { - "userDetail": { - "path": "/user/{id}", - "view": "UserDetailView" - }, - "articleDetail": { - "path": "/article/{category}/{id}", - "view": "ArticleDetailView" + "quest/detail": { + "private": true, + "requests": [] } - } } ``` -Viewでパラメータを取得: +`private: true`の場合、URLで直接アクセスすると`TopView`にリダイレクトされます。プログラムからの`app.gotoView()`でのみアクセス可能です。 -```typescript -import { router } from "@next2d/framework"; +## 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の種類 -export class UserDetailView extends View +#### json + +外部JSONデータを取得: + +```json { - $ready(): void - { - // URLパラメータを取得 - const userId = router.params.id; - console.log("ユーザーID:", userId); + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData" + } + ] } } ``` -### クエリパラメータ +#### content -```typescript -// /search?q=next2d&page=1 の場合 -const query = router.query; -console.log(query.q); // "next2d" -console.log(query.page); // "1" -``` +Animation ToolのJSONを取得: -## 画面遷移 +```json +{ + "top": { + "requests": [ + { + "type": "content", + "path": "{{content.endPoint}}top.json", + "name": "TopContent" + } + ] + } +} +``` -### router.push() +#### custom -新しい履歴を追加して遷移: +カスタムクラスでリクエストを実行: -```typescript -import { router } from "@next2d/framework"; +```json +{ + "user/profile": { + "requests": [ + { + "type": "custom", + "class": "repository.UserRepository", + "access": "static", + "method": "getProfile", + "name": "UserProfile" + } + ] + } +} +``` -// パスで遷移 -router.push("/about"); +### 変数の展開 -// パラメータ付き -router.push("/user/123"); +`{{***}}`で囲むと`config.json`の変数を取得できます: -// クエリパラメータ付き -router.push("/search?q=next2d"); +```json +{ + "path": "{{api.endPoint}}path/to/api" +} ``` -### router.replace() +### キャッシュの利用 -現在の履歴を置き換えて遷移: +`cache: true`を設定すると、データがキャッシュされます。キャッシュしたデータは画面遷移しても初期化されません。 -```typescript -// ログイン後にホームに遷移(戻るボタンでログイン画面に戻れなくする) -router.replace("/home"); +```json +{ + "top": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/master.json", + "name": "MasterData", + "cache": true + } + ] + } +} ``` -### router.back() - -前の画面に戻る: +キャッシュデータの取得: ```typescript -router.back(); +import { app } from "@next2d/framework"; + +const cache = app.getCache(); +if (cache.has("MasterData")) { + const masterData = cache.get("MasterData"); +} ``` -### router.forward() +### コールバック -次の画面に進む: +リクエスト完了後にコールバックを実行: -```typescript -router.forward(); +```json +{ + "home": { + "requests": [ + { + "type": "json", + "path": "{{api.endPoint}}api/home.json", + "name": "HomeData", + "callback": "callback.HomeDataCallback" + } + ] + } +} ``` -## ナビゲーションガード - -### beforeEach - -画面遷移前にチェックを行います: +コールバッククラス: ```typescript -import { app } from "@next2d/framework"; +export class HomeDataCallback +{ + constructor(data: any) + { + // 取得したデータが渡される + } -app.beforeEach((to, from, next) => { - // 認証チェック - if (to.path !== "/login" && !isAuthenticated()) { - next("/login"); - return; + execute(): void + { + // コールバック処理 } - next(); -}); +} ``` -### afterEach +## 画面遷移 + +### app.gotoView() -画面遷移後の処理: +`app.gotoView()`で画面遷移を行います: ```typescript -app.afterEach((to, from) => { - // アナリティクスに送信 - analytics.page(to.path); -}); +import { app } from "@next2d/framework"; + +// 基本的な遷移 +await app.gotoView("home"); + +// パスで遷移 +await app.gotoView("quest/list"); + +// クエリパラメータ付き +await app.gotoView("quest/detail?id=123"); ``` -## 遷移アニメーション +### UseCaseでの画面遷移 -config.jsonでトランジションを設定: +画面遷移はUseCaseで行うことを推奨します: -```json +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase { - "routing": { - "top": { - "path": "/", - "view": "TopView", - "transition": { - "type": "fade", - "duration": 0.3 - } + async execute(viewName: string): Promise + { + await app.gotoView(viewName); } - } } ``` -View内でカスタムトランジション: +ViewModelでの使用: ```typescript -export class SampleView extends View +export class TopViewModel extends ViewModel { - // 表示アニメーション - async $inTransition(): Promise + private readonly navigateToViewUseCase: NavigateToViewUseCase; + + constructor() { - this.content.alpha = 0; - await this.tween(this.content, { alpha: 1 }, 0.3); + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); } - // 非表示アニメーション - async $outTransition(): Promise + async onClickStartButton(): Promise { - await this.tween(this.content, { alpha: 0 }, 0.3); + 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; } } ``` -## 404エラー処理 +**注意:** `response`データは画面遷移すると初期化されます。画面を跨いで保持したいデータは`cache: true`を設定してください。 + +## SPAモード + +`config.json`の`all.spa`で設定します: ```json { - "routing": { - "notFound": { - "path": "*", - "view": "NotFoundView" + "all": { + "spa": true } - } } ``` -## プログラムによるルート取得 +- `true`: URLでシーンを制御(History API使用) +- `false`: URLによるシーン制御を無効化 -```typescript -import { router } from "@next2d/framework"; +## デフォルトのトップページ -// 現在のパス -console.log(router.currentPath); // "/user/123" +`config.json`で設定: -// 現在のルート名 -console.log(router.currentRoute); // "userDetail" +```json +{ + "all": { + "defaultTop": "top" + } +} +``` + +設定がない場合は`TopView`クラスが起動します。 + +## View/ViewModelの自動生成 -// 全ルートの取得 -const routes = router.routes; +`routing.json`の設定から自動生成できます: + +```bash +npm run generate ``` -## ディープリンク対応 +このコマンドは`routing.json`のトッププロパティを解析し、対応するViewとViewModelクラスを生成します。 + +## 設定例 -Next2D Frameworkは、ハッシュモードとヒストリーモードの両方をサポートします: +### 完全な routing.json の例 ```json { - "router": { - "mode": "history" // または "hash" - } + "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" + } + ] + } } ``` -- **historyモード**: `/about`, `/user/123` -- **hashモード**: `/#/about`, `/#/user/123` - ## 関連項目 -- [View/ViewModel](./view.md) -- [設定ファイル](./config.md) +- [View/ViewModel](/ja/reference/framework/view) +- [設定ファイル](/ja/reference/framework/config) diff --git a/specs/ja/view.md b/specs/ja/view.md index 671e10a..14b9b62 100644 --- a/specs/ja/view.md +++ b/specs/ja/view.md @@ -1,223 +1,459 @@ # View と ViewModel -Next2D FrameworkはMVVM(Model-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は画面の表示を担当するクラスです。Open Animation Toolで作成したMovieClipをコンテンツとして持ちます。 +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 SampleView extends View +export class TopView extends View { - constructor() + private readonly _topPage: TopPage; + + constructor(vm: TopViewModel) { - super(); + super(vm); + this._topPage = new TopPage(); + this.addChild(this._topPage); } - // 非同期の初期設定 - async $setup(): Promise + async initialize(): Promise { - // APIからデータを取得など + this._topPage.initialize(this.vm); } - // 表示準備完了時 - $ready(): void + async onEnter(): Promise { - // イベントリスナーの設定など + await this._topPage.onEnter(); } - // 画面遷移前の破棄処理 - $dispose(): void + async onExit(): Promise { - // リソースの解放など + return void 0; } } ``` ### ライフサイクル -1. `constructor`: インスタンス生成 -2. `$setup()`: 非同期の初期化処理(await可能) -3. `$ready()`: 表示準備完了、イベント設定 -4. `$dispose()`: 画面遷移時の破棄処理 +```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 -$ready(): void +async initialize(): Promise { - // contentはOpen Animation Toolで作成したMovieClip - const button = this.content.getChildByName("submitButton"); - button.addEventListener("click", () => { - this.onSubmit(); - }); + const { HomeBtnMolecule } = await import("@/ui/component/molecule/HomeBtnMolecule"); + const { PointerEvent } = next2d.events; + + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; - const textField = this.content.getChildByName("nameInput"); - textField.text = "初期値"; + // イベントをViewModelに委譲 + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); } ``` -### 画面遷移 +#### onEnter() - 画面表示時 + +**呼び出しタイミング:** +- `initialize()`の実行完了後 +- 画面が表示される直前 + +**主な用途:** +- 入場アニメーションの開始 +- タイマーやインターバルの開始 +- フォーカス設定 ```typescript -import { router } from "@next2d/framework"; +async onEnter(): Promise +{ + const topBtn = this.getChildByName("topBtn") as TopBtnMolecule; + topBtn.playEntrance(() => { + console.log("アニメーション完了"); + }); +} +``` -// 指定パスに遷移 -router.push("/about"); +#### onExit() - 画面非表示時 -// パラメータ付きで遷移 -router.push("/detail/123"); +**呼び出しタイミング:** +- 別の画面に遷移する直前 +- Viewが破棄される前 -// 履歴を置き換え -router.replace("/home"); +**主な用途:** +- アニメーションの停止 +- タイマーやインターバルのクリア +- リソースの解放 -// 戻る -router.back(); +```typescript +async onExit(): Promise +{ + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } +} ``` ## ViewModel -ViewModelはViewのビジネスロジックとデータバインディングを担当します。 +ViewModelはViewとModelの橋渡しを行います。UseCaseを保持し、Viewからのイベントを処理してビジネスロジックを実行します。 + +### ViewModelの責務 + +- **イベント処理** - Viewからのイベントを受け取る +- **UseCaseの実行** - ビジネスロジックを呼び出す +- **依存性の管理** - UseCaseのインスタンスを保持 +- **状態管理** - 画面固有の状態を管理 ### 基本構造 ```typescript -import { ViewModel } from "@next2d/framework"; +import { ViewModel, app } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; -export class SampleViewModel extends ViewModel +export class TopViewModel extends ViewModel { - private _name: string = ""; + private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; - async $setup(): Promise + constructor() { - // データの初期化 - const response = await fetch("/api/user"); - const data = await response.json(); - this._name = data.name; + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); } - $bind(): void + async initialize(): Promise { - // Viewへのデータバインド - const textField = this.view.content.getChildByName("nameText"); - textField.text = this._name; + // routing.jsonのrequestsで取得したデータを受け取る + const response = app.getResponse(); + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; } - get name(): string + getTopText(): string { - return this._name; + return this.topText; } - set name(value: string) + async onClickStartButton(): Promise { - this._name = value; - // UIの更新 - const textField = this.view.content.getChildByName("nameText"); - textField.text = value; + await this.navigateToViewUseCase.execute("home"); } } ``` -### ViewとViewModelの連携 +### 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; + } -config.jsonでの設定: + getHomeText(): string + { + return this.homeText; + } +} -```json +// HomeView.ts +export class HomeView extends View { - "routing": { - "sample": { - "path": "/sample", - "view": "SampleView", - "viewModel": "SampleViewModel" + 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); } - } } ``` -View側からViewModelへのアクセス: +## 画面遷移 + +画面遷移には`app.gotoView()`を使用します。 ```typescript -export class SampleView extends View +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 { - $ready(): void + async execute(viewName: string): Promise { - // ViewModelへの参照 - const vm = this.viewModel as SampleViewModel; + await app.gotoView(viewName); + } +} +``` + +## レスポンスデータの取得 + +`routing.json`で設定した`requests`のデータは`app.getResponse()`で取得できます。 + +```typescript +import { app } from "@next2d/framework"; + +async initialize(): Promise +{ + const response = app.getResponse(); - const button = this.content.getChildByName("updateButton"); - button.addEventListener("click", () => { - vm.name = "新しい名前"; - }); + 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"); +} +``` -- **Atoms**: 最小単位(ボタン、テキストフィールドなど) -- **Molecules**: Atomsの組み合わせ -- **Organisms**: 複雑なUIコンポーネント -- **Templates**: ページのレイアウト -- **Pages**: 実際のページ(View) +## 設計原則 -### 共通コンポーネントの例 +### 1. 関心の分離 ```typescript -// BaseButton.ts -export class BaseButton +// 良い例: Viewは表示のみ、ViewModelはロジック +class HomeView extends View { - private _mc: MovieClip; + async initialize(): Promise + { + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick); + } +} - constructor(mc: MovieClip) +class HomeViewModel extends ViewModel +{ + onClick(event: PointerEvent): void { - this._mc = mc; - this.setup(); + this.someUseCase.execute(); } +} +``` + +### 2. 依存性の逆転 - private setup(): void +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) { - this._mc.addEventListener("rollOver", () => { - this._mc.gotoAndStop("over"); - }); + super(vm); + } - this._mc.addEventListener("rollOut", () => { - this._mc.gotoAndStop("up"); - }); + async initialize(): Promise + { + // UIコンポーネントの作成と配置 + } - this._mc.addEventListener("mouseDown", () => { - this._mc.gotoAndStop("down"); - }); + async onEnter(): Promise + { + // 画面表示時の処理 } - onClick(handler: Function): void + async onExit(): Promise { - this._mc.addEventListener("click", handler); + // 画面非表示時の処理 } } ``` -使用例: +### ViewModel ```typescript -$ready(): void +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/your/usecase/YourUseCase"; + +export class YourViewModel extends ViewModel { - const buttonMc = this.content.getChildByName("myButton"); - const button = new BaseButton(buttonMc); - button.onClick(() => { - console.log("ボタンがクリックされました"); - }); + private readonly yourUseCase: YourUseCase; + + constructor() + { + super(); + this.yourUseCase = new YourUseCase(); + } + + async initialize(): Promise + { + return void 0; + } + + yourEventHandler(event: Event): void + { + this.yourUseCase.execute(); + } } ``` ## 関連項目 -- [ルーティング](./routing.md) -- [設定ファイル](./config.md) +- [ルーティング](/ja/reference/framework/routing) +- [設定ファイル](/ja/reference/framework/config) From b40a7a2dd2562f30709b068b32ba4252605c6aeb Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 5 Feb 2026 08:57:57 +0900 Subject: [PATCH 26/28] #155 update packages --- .gitignore | 3 +- package-lock.json | 3972 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 +- 3 files changed, 3981 insertions(+), 10 deletions(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 02dd1b0..93c859f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,4 @@ dist-ssr *.sln *.sw? -coverage -package-lock.json \ No newline at end of file +coverage \ No newline at end of file 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 4cd6be9..8a8f43c 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,18 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", - "@types/node": "^25.0.9", - "@typescript-eslint/eslint-plugin": "^8.53.1", - "@typescript-eslint/parser": "^8.53.1", - "@vitest/coverage-v8": "^4.0.17", - "@vitest/web-worker": "^4.0.17", + "@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.0.0", - "jsdom": "^27.4.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", "typescript": "^5.9.3", "vite": "^7.3.1", - "vitest": "^4.0.17", + "vitest": "^4.0.18", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { From f682292dbe4c78221b64136825475bd96ad9da83 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 11 Feb 2026 08:04:26 +0900 Subject: [PATCH 27/28] #156 update github actions --- .github/workflows/publish.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0ab05ff..ba3b6cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,12 +5,13 @@ 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@v6 - uses: actions/setup-node@v6 @@ -21,6 +22,5 @@ jobs: - 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 From 20acb994062129bc63fee504f8c3e88bc6402ef4 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 11 Feb 2026 10:44:20 +0900 Subject: [PATCH 28/28] #156 update README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ae84f4d..60e6c74 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,21 @@ 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测试。