;
* all" links and buttons are outside scope of a `CheckableController`, i.e. in
* another part of the DOM that is not a descendant.
*
- * @see https://stimulus.hotwired.dev/reference/outlets
- *
* This controller also handles setting `aria-controls` on its HTML element.
*
* Rather than using targets, it is up to the implementer to "wire up" events
* using descriptors. This is designed for maximum flexibility.
*
+ * @see [Stimulus outlets](https://stimulus.hotwired.dev/reference/outlets)
+ *
* @example
* ```html
*
diff --git a/frontend/src/stimulus/controllers/checkable.controller.ts b/frontend/src/stimulus/controllers/checkable.controller.ts
index e40476aef577..7be1f965f3dc 100644
--- a/frontend/src/stimulus/controllers/checkable.controller.ts
+++ b/frontend/src/stimulus/controllers/checkable.controller.ts
@@ -27,6 +27,12 @@
//++
import { Controller, ActionEvent } from '@hotwired/stimulus';
+// Imported for the {@link} reference below; TypeDoc resolves declaration
+// references through scope, and its `module!name` form is rejected by the
+// TSDoc syntax rule while the TSDoc `module#name` form it accepts does not
+// resolve here.
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import type CheckAllController from './check-all.controller';
import invariant from 'tiny-invariant';
/**
@@ -39,7 +45,7 @@ import invariant from 'tiny-invariant';
*
* Rather than defining event handlers within the controller, this controller
* uses Stimulus actions. The implementer is responsible for adding appropriate
- * {@link https://stimulus.hotwired.dev/reference/actions#descriptors action descriptors}
+ * [action descriptors](https://stimulus.hotwired.dev/reference/actions#descriptors)
* to HTML elements that should trigger the controller's methods.
*
* Can be used standalone or in combination with {@link CheckAllController}
@@ -118,11 +124,11 @@ export default class CheckableController extends Controller {
* by `key`) against a value (specified by `value`). Useful for table-like
* UIs where you want to toggle checkboxes by row or column.
*
- * @param event - The ActionEvent containing params
- * @param event.params.key - The data attribute name to filter by (camelCase)
- * @param event.params.value - The value to match (will be converted to string)
+ * @param event - The ActionEvent whose `params.key` names the data attribute
+ * to filter by (camelCase) and whose `params.value` is matched against it
+ * (converted to a string)
*
- * @throws {Error} If key or value params are missing
+ * @throws Error If key or value params are missing
*
* @example Toggle all checkboxes where data-column-id="3"
* ```html
diff --git a/frontend/src/stimulus/controllers/op-application.controller.ts b/frontend/src/stimulus/controllers/op-application.controller.ts
index b559bde9b394..c1e7b5a58f9c 100644
--- a/frontend/src/stimulus/controllers/op-application.controller.ts
+++ b/frontend/src/stimulus/controllers/op-application.controller.ts
@@ -103,7 +103,7 @@ export class OpApplicationController extends ApplicationController {
* We convert these to slashes for the dynamic import.
*
* https://stimulus.hotwired.dev/handbook/installing#controller-filenames-map-to-identifiers
- * @param controller
+ * @param controller - The controller identifier
* @private
*/
private derivePath(controller:string):string {
diff --git a/frontend/src/stimulus/helpers/live-collaboration-helpers.ts b/frontend/src/stimulus/helpers/live-collaboration-helpers.ts
index e69149c9d2e4..1f44fe9cfe10 100644
--- a/frontend/src/stimulus/helpers/live-collaboration-helpers.ts
+++ b/frontend/src/stimulus/helpers/live-collaboration-helpers.ts
@@ -63,9 +63,9 @@ class LiveCollaborationManagerClass {
* existing session rather than calling this with a fresh provider, since
* this method unconditionally tears down the previous provider/doc.
*
- * @param provider The provider to use
- * @param doc The Y.Doc instance to use
- * @param documentName Logical identifier of the document being edited
+ * @param provider - The provider to use
+ * @param doc - The Y.Doc instance to use
+ * @param documentName - Logical identifier of the document being edited
* @returns void
*/
initializeYjsProvider(provider:HocuspocusProvider, doc:Doc, documentName:string) {
@@ -86,7 +86,7 @@ class LiveCollaborationManagerClass {
* controller's connect(). Without an ownership check, the old controller would destroy the
* new provider, causing a spurious "connection error" banner.
*
- * @param provider The provider instance requesting destruction; treated as the
+ * @param provider - The provider instance requesting destruction; treated as the
* candidate owner of the current collaboration session.
* @returns `true` if the given provider was the current owner and the internal
* provider/doc instances were destroyed; `false` otherwise.
@@ -143,7 +143,7 @@ class LiveCollaborationManagerClass {
* with the current {@link HocuspocusProvider} instance. Otherwise, the
* listener is stored and invoked later once {@link initializeYjsProvider} is called.
*
- * @param listener Callback that receives the ready { @link HocuspocusProvider }
+ * @param listener - Callback that receives the ready {@link HocuspocusProvider}
*
*/
onReady(listener:Listener) {
@@ -155,7 +155,7 @@ class LiveCollaborationManagerClass {
/**
* Unregisters a previously registered ready listener.
- * @param listener The listener function to remove
+ * @param listener - The listener function to remove
*/
offReady(listener:Listener):void {
const index = this.listeners.indexOf(listener);
diff --git a/frontend/src/stimulus/helpers/url-helpers.ts b/frontend/src/stimulus/helpers/url-helpers.ts
index 0f63d2a8be03..4cfff748e2bf 100644
--- a/frontend/src/stimulus/helpers/url-helpers.ts
+++ b/frontend/src/stimulus/helpers/url-helpers.ts
@@ -29,9 +29,9 @@
/**
* Extend a given URL (string or URL object) with the provided search parameters.
*
- * @param base The base URL to extend
- * @param params A record of key-value pairs to add as search parameters
- * @param addCurrentSearch Whether to include the current window's search parameters (default: true)
+ * @param base - The base URL to extend
+ * @param params - A record of key-value pairs to add as search parameters
+ * @param addCurrentSearch - Whether to include the current window's search parameters (default: true)
*/
export function extendSearchParams(
base:string,
diff --git a/frontend/src/stimulus/mixins/use-angular-services.ts b/frontend/src/stimulus/mixins/use-angular-services.ts
index 4f3a88f6246e..e340574babfc 100644
--- a/frontend/src/stimulus/mixins/use-angular-services.ts
+++ b/frontend/src/stimulus/mixins/use-angular-services.ts
@@ -44,20 +44,22 @@ interface ServiceConsumer {
*
* Usage:
*
- * export default class ListRefreshController extends Controller {
- * static services:ServiceKey[] = ['halEvents'];
- * declare halEvents:HalEventsService;
+ * ```ts
+ * export default class ListRefreshController extends Controller {
+ * static services:ServiceKey[] = ['halEvents'];
+ * declare halEvents:HalEventsService;
*
- * initialize() {
- * useAngularServices(this);
- * }
+ * initialize() {
+ * useAngularServices(this);
+ * }
*
- * // Fires after every connect(), once the context has resolved and the
- * // element is still connected.
- * servicesConnected() {
- * this.subscription = this.halEvents.aggregated$('WorkPackage')...
- * }
- * }
+ * // Fires after every connect(), once the context has resolved and the
+ * // element is still connected.
+ * servicesConnected() {
+ * this.subscription = this.halEvents.aggregated$('WorkPackage')...
+ * }
+ * }
+ * ```
*
* For use outside `servicesConnected()` (e.g. event handlers), the mixin also
* defines two promise properties on the controller (add matching `declare`
diff --git a/frontend/src/stimulus/openproject-stimulus-application.ts b/frontend/src/stimulus/openproject-stimulus-application.ts
index 61e469eb7c4d..aef79e67be05 100644
--- a/frontend/src/stimulus/openproject-stimulus-application.ts
+++ b/frontend/src/stimulus/openproject-stimulus-application.ts
@@ -44,8 +44,8 @@ export class OpenProjectStimulusApplication extends Application {
*
* This is useful for plugins that execute code before we call setup.ts
*
- * @param name the name/identifier of the controller
- * @param controller the controller class
+ * @param name - The name/identifier of the controller
+ * @param controller - The controller class
*/
static preregister(name:string, controller:ControllerConstructor) {
this.controllers.set(name, controller);
@@ -57,15 +57,17 @@ export class OpenProjectStimulusApplication extends Application {
*
* This is useful for plugins that want to define new dynamic controllers.
* How to use this: In your plugin's main.ts, call this
+ *
* @example
+ * ```ts
* OpenProjectStimulusApplication.preregisterDynamic(
* 'test',
* () => import('./test.controller')
* );
* ```
*
- * @param name the name/identifier of the controller
- * @param loader A callback to provide the controller asynchronously.
+ * @param name - The name/identifier of the controller
+ * @param loader - A callback to provide the controller asynchronously.
*/
static preregisterDynamic(name:string, loader:DynamicControllerLoader) {
this.dynamicImports.set(name, loader);
diff --git a/frontend/tooling/typedoc/__fixtures__/simple/sample.ts b/frontend/tooling/typedoc/__fixtures__/simple/sample.ts
new file mode 100644
index 000000000000..9a7cf933acb6
--- /dev/null
+++ b/frontend/tooling/typedoc/__fixtures__/simple/sample.ts
@@ -0,0 +1,37 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+/**
+ * Adds two numbers.
+ *
+ * @param a - The first addend
+ * @param b - The second addend
+ */
+export function add(a:number, b:number):number {
+ return a + b;
+}
diff --git a/frontend/tooling/typedoc/__fixtures__/vendored/uses-vendored.ts b/frontend/tooling/typedoc/__fixtures__/vendored/uses-vendored.ts
new file mode 100644
index 000000000000..d7f1ca7837af
--- /dev/null
+++ b/frontend/tooling/typedoc/__fixtures__/vendored/uses-vendored.ts
@@ -0,0 +1,36 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { Controller } from '@hotwired/stimulus';
+
+/** A controller inheriting members from the vendored Stimulus base class. */
+export default class SampleController extends Controller {
+ connect():void {
+ this.element.dataset.connected = 'true';
+ }
+}
diff --git a/frontend/tooling/typedoc/openproject-plugin.mjs b/frontend/tooling/typedoc/openproject-plugin.mjs
new file mode 100644
index 000000000000..f91b313a7d78
--- /dev/null
+++ b/frontend/tooling/typedoc/openproject-plugin.mjs
@@ -0,0 +1,68 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { Converter } from 'typedoc';
+
+/**
+ * Members inherited from vendored base classes carry the source path of their
+ * `.d.ts` file. With `sourceLinkTemplate` configured those render as links to
+ * paths that do not exist in the repository, so they are dropped entirely.
+ *
+ * @param project - The converted project reflection
+ * @returns The number of source entries removed
+ */
+function stripVendoredSources(project) {
+ let stripped = 0;
+
+ for (const reflection of Object.values(project.reflections)) {
+ const { sources } = reflection;
+ if (!sources) {
+ continue;
+ }
+
+ const kept = sources.filter((source) => !source.fileName.includes('node_modules'));
+ if (kept.length !== sources.length) {
+ stripped += sources.length - kept.length;
+ reflection.sources = kept.length > 0 ? kept : undefined;
+ }
+ }
+
+ return stripped;
+}
+
+/**
+ * TypeDoc plugin entry point.
+ *
+ * @param app - The TypeDoc application to extend
+ */
+export function load(app) {
+ app.converter.on(Converter.EVENT_END, (context) => {
+ const stripped = stripVendoredSources(context.project);
+ app.logger.verbose(`Stripped ${stripped} vendored source entries`);
+ });
+}
diff --git a/frontend/tooling/typedoc/openproject-plugin.spec.mjs b/frontend/tooling/typedoc/openproject-plugin.spec.mjs
new file mode 100644
index 000000000000..cffb4ebac234
--- /dev/null
+++ b/frontend/tooling/typedoc/openproject-plugin.spec.mjs
@@ -0,0 +1,89 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import { buildFixtureProject, buildFromRepoConfig } from './run-typedoc.mjs';
+
+const plugin = fileURLToPath(new URL('./openproject-plugin.mjs', import.meta.url));
+
+function allSources(project) {
+ const sources = [];
+ for (const reflection of Object.values(project.reflections)) {
+ sources.push(...(reflection.sources ?? []));
+ }
+ return sources;
+}
+
+describe('vendored source stripping', () => {
+ it('leaves no source entries pointing into node_modules', async () => {
+ const project = await buildFixtureProject({ fixture: 'vendored', plugins: [plugin] });
+ const vendored = allSources(project).filter((s) => s.fileName.includes('node_modules'));
+
+ expect(vendored).toHaveLength(0);
+ });
+
+ it('keeps source entries for first-party code', async () => {
+ const project = await buildFixtureProject({ fixture: 'vendored', plugins: [plugin] });
+ const firstParty = allSources(project).filter((s) => s.fileName.includes('uses-vendored'));
+
+ expect(firstParty.length).toBeGreaterThan(0);
+ });
+});
+
+describe('shipped typedoc.json', () => {
+ const revision = '0123456789abcdef0123456789abcdef01234567';
+ // Two entry points in different subdirectories, so TypeDoc infers the same
+ // base path (`src/stimulus`) that the full build does. A single entry point
+ // would shift it and silently drop a path segment from every source link.
+ const entryPoints = ['src/stimulus/controllers/check-all.controller.ts', 'src/stimulus/helpers/url-helpers.ts'];
+
+ it('names default-exported controllers after their class', async () => {
+ const project = await buildFromRepoConfig({ entryPoints, gitRevision: revision });
+ const names = Object.values(project.reflections).map((reflection) => reflection.name);
+
+ expect(names).toContain('CheckAllController');
+ expect(names).not.toContain('default');
+ });
+
+ it('builds source links from the given revision and repository-relative path', async () => {
+ const project = await buildFromRepoConfig({ entryPoints, gitRevision: revision });
+ const urls = allSources(project).map((source) => source.url).filter(Boolean);
+
+ expect(urls.length).toBeGreaterThan(0);
+ for (const url of urls) {
+ expect(url).toMatch(
+ new RegExp(`/blob/${revision}/frontend/src/stimulus/[\\w./-]+\\.ts#L\\d+$`),
+ );
+ }
+
+ expect(urls).toContainEqual(
+ expect.stringContaining('frontend/src/stimulus/controllers/check-all.controller.ts'),
+ );
+ });
+});
diff --git a/frontend/tooling/typedoc/run-typedoc.mjs b/frontend/tooling/typedoc/run-typedoc.mjs
new file mode 100644
index 000000000000..a5a7c418c31c
--- /dev/null
+++ b/frontend/tooling/typedoc/run-typedoc.mjs
@@ -0,0 +1,90 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { fileURLToPath } from 'node:url';
+import { Application, PackageJsonReader, TSConfigReader, TypeDocReader } from 'typedoc';
+
+const fixturesRoot = fileURLToPath(new URL('./__fixtures__/', import.meta.url));
+const toolingTsconfig = fileURLToPath(new URL('./tsconfig.json', import.meta.url));
+const frontendRoot = fileURLToPath(new URL('../../', import.meta.url));
+
+// Skip TypeDocReader so the repo's own `typedoc.json` (scoped to
+// `src/stimulus/**`) never leaks into fixture conversions.
+const readers = [new PackageJsonReader(), new TSConfigReader()];
+
+/**
+ * Converts a fixture directory with TypeDoc and returns the reflection model.
+ *
+ * @param options - Fixture name, plugins to load, and TypeDoc option overrides
+ * @returns The converted project reflection
+ */
+export async function buildFixtureProject({ fixture, plugins = [], options = {} }) {
+ const app = await Application.bootstrapWithPlugins({
+ entryPoints: [`${fixturesRoot}${fixture}`],
+ entryPointStrategy: 'expand',
+ plugin: plugins,
+ logLevel: 'Error',
+ tsconfig: toolingTsconfig,
+ ...options,
+ }, readers);
+
+ const project = await app.convert();
+ if (!project) {
+ throw new Error(`TypeDoc failed to convert fixture "${fixture}"`);
+ }
+
+ return project;
+}
+
+/**
+ * Converts real sources using the repository's own `typedoc.json`.
+ *
+ * `buildFixtureProject` deliberately omits `TypeDocReader`, so a test using it
+ * proves nothing about the shipped configuration — a plugin dropped from
+ * `typedoc.json` would still be loaded if the test passed it explicitly. This
+ * helper reads that file the way CI and `npm run generate-docs` do, overriding
+ * only what a test needs to stay fast and deterministic.
+ *
+ * @param options - Entry points to convert and the revision for source links
+ * @returns The converted project reflection
+ */
+export async function buildFromRepoConfig({ entryPoints, gitRevision }) {
+ const app = await Application.bootstrapWithPlugins({
+ options: frontendRoot,
+ entryPoints: entryPoints.map((entry) => `${frontendRoot}${entry}`),
+ gitRevision,
+ logLevel: 'Error',
+ }, [new TypeDocReader(), new PackageJsonReader(), new TSConfigReader()]);
+
+ const project = await app.convert();
+ if (!project) {
+ throw new Error(`TypeDoc failed to convert ${entryPoints.join(', ')}`);
+ }
+
+ return project;
+}
diff --git a/frontend/tooling/typedoc/run-typedoc.spec.mjs b/frontend/tooling/typedoc/run-typedoc.spec.mjs
new file mode 100644
index 000000000000..83167105a87f
--- /dev/null
+++ b/frontend/tooling/typedoc/run-typedoc.spec.mjs
@@ -0,0 +1,39 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { describe, expect, it } from 'vitest';
+import { buildFixtureProject } from './run-typedoc.mjs';
+
+describe('buildFixtureProject', () => {
+ it('converts a fixture into a reflection model', async () => {
+ const project = await buildFixtureProject({ fixture: 'simple' });
+ const names = Object.values(project.reflections).map((r) => r.name);
+
+ expect(names).toContain('add');
+ });
+});
diff --git a/frontend/tooling/typedoc/tsconfig.json b/frontend/tooling/typedoc/tsconfig.json
new file mode 100644
index 000000000000..610b1a61b22c
--- /dev/null
+++ b/frontend/tooling/typedoc/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/tooling",
+ "types": ["node"]
+ },
+ "files": [],
+ "include": ["**/*.ts"]
+}
diff --git a/frontend/tsdoc.json b/frontend/tsdoc.json
new file mode 100644
index 000000000000..b89839ca227a
--- /dev/null
+++ b/frontend/tsdoc.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+ "extends": ["typedoc/tsdoc.json"]
+}
diff --git a/frontend/typedoc.json b/frontend/typedoc.json
new file mode 100644
index 000000000000..fd1f8d6fac4e
--- /dev/null
+++ b/frontend/typedoc.json
@@ -0,0 +1,23 @@
+{
+ "entryPoints": ["src/stimulus/**/*.ts"],
+ "entryPointStrategy": "expand",
+ "exclude": [
+ "**/*.spec.ts",
+ "**/*.spec.tsx",
+ "src/stimulus/setup.ts",
+ "src/stimulus/openproject-stimulus-application.ts",
+ "src/stimulus/test-helpers.ts"
+ ],
+ "plugin": [
+ "typedoc-github-theme",
+ "typedoc-plugin-rename-defaults",
+ "./tooling/typedoc/openproject-plugin.mjs"
+ ],
+ "excludeExternals": true,
+ "out": "./generated-docs",
+ "disableGit": true,
+ "gitRevision": "dev",
+ "sourceLinkTemplate": "https://github.com/opf/openproject/blob/{gitRevision}/frontend/src/stimulus/{path}#L{line}",
+ "projectDocuments": ["doc/**/*.md"],
+ "tsconfig": "tsconfig.app.json"
+}
diff --git a/frontend/vitest.tooling.config.ts b/frontend/vitest.tooling.config.ts
new file mode 100644
index 000000000000..9eb1a9583e17
--- /dev/null
+++ b/frontend/vitest.tooling.config.ts
@@ -0,0 +1,39 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { defineConfig } from 'vitest/config';
+
+// The app's specs run in the browser via the Angular builder. Documentation
+// tooling is Node-side and cannot run there, so it gets its own project.
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['tooling/**/*.spec.mjs'],
+ testTimeout: 60_000,
+ },
+});