diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md
new file mode 100644
index 0000000000..8ee1ad8375
--- /dev/null
+++ b/.changeset/chilly-terms-behave.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-backend-module-stack-overflow-collator': minor
+---
+
+Extract a package for the Stack Overflow new backend system plugin.
diff --git a/.changeset/flat-ducks-buy.md b/.changeset/flat-ducks-buy.md
new file mode 100644
index 0000000000..5c952faaf1
--- /dev/null
+++ b/.changeset/flat-ducks-buy.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-backend-node': patch
+---
+
+Fix highlighting for non-string fields on the `Lunr` search engine implementation.
diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md
new file mode 100644
index 0000000000..f69938ea25
--- /dev/null
+++ b/.changeset/giant-cycles-end.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-stack-overflow-backend': patch
+---
+
+Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow-collator` module.
+
+The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions).
diff --git a/.changeset/nice-pillows-poke.md b/.changeset/nice-pillows-poke.md
new file mode 100644
index 0000000000..64034c39d2
--- /dev/null
+++ b/.changeset/nice-pillows-poke.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-stack-overflow': patch
+---
+
+Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath.
diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md
index 7fb8e40b20..28c8b410e3 100644
--- a/docs/plugins/integrating-search-into-plugins.md
+++ b/docs/plugins/integrating-search-into-plugins.md
@@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data
The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each.
-> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices.
+> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices.
#### 1. Install collator interface dependencies
diff --git a/packages/app-next/package.json b/packages/app-next/package.json
index 71d888ee50..5437c66272 100644
--- a/packages/app-next/package.json
+++ b/packages/app-next/package.json
@@ -68,7 +68,6 @@
"@backstage/plugin-search-react": "workspace:^",
"@backstage/plugin-sentry": "workspace:^",
"@backstage/plugin-shortcuts": "workspace:^",
- "@backstage/plugin-stack-overflow": "workspace:^",
"@backstage/plugin-stackstorm": "workspace:^",
"@backstage/plugin-tech-insights": "workspace:^",
"@backstage/plugin-tech-radar": "workspace:^",
diff --git a/plugins/search-backend-module-stack-overflow-collator/.eslintrc.js b/plugins/search-backend-module-stack-overflow-collator/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md
new file mode 100644
index 0000000000..7df3229eeb
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/README.md
@@ -0,0 +1,95 @@
+# Stack Overflow Search Backend Module
+
+A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App.
+
+## Getting started
+
+Before we begin, make sure:
+
+- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/).
+
+To use any of the functionality this plugin provides, you need to start by configuring your App with the following config:
+
+```yaml
+stackoverflow:
+ baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
+```
+
+### Stack Overflow for Teams
+
+If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api).
+
+The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details.
+
+```yaml
+stackoverflow:
+ baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
+ apiKey: $STACK_OVERFLOW_API_KEY
+ apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
+```
+
+```yaml
+stackoverflow:
+ baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance
+ teamName: $STACK_OVERFLOW_TEAM_NAME
+ apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
+```
+
+## Areas of Responsibility
+
+This stack overflow backend plugin is primarily responsible for the following:
+
+- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search.
+
+### Index Stack Overflow Questions to search
+
+Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started).
+
+When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions).
+
+> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions)
+
+```ts
+indexBuilder.addCollator({
+ schedule,
+ factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, {
+ logger: env.logger,
+ requestParams: {
+ tagged: ['backstage'],
+ site: 'stackoverflow',
+ pagesize: 100,
+ },
+ }),
+});
+```
+
+## New Backend System
+
+> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below.
+
+This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions).
+
+### Installation
+
+Add the module package as a dependency:
+
+```bash
+# From your Backstage root directory
+yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow-collator
+```
+
+Add the collator to your backend instance, along with the search plugin itself:
+
+```tsx
+// packages/backend/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+
+const backend = createBackend();
+backend.add(import('@backstage/plugin-search-backend/alpha'));
+backend.add(
+ import('@backstage/plugin-search-backend-module-stack-overflow-collator'),
+);
+backend.start();
+```
+
+You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/config.d.ts) for more details.
diff --git a/plugins/search-backend-module-stack-overflow-collator/api-report.md b/plugins/search-backend-module-stack-overflow-collator/api-report.md
new file mode 100644
index 0000000000..9243e786ac
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/api-report.md
@@ -0,0 +1,61 @@
+## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { Config } from '@backstage/config';
+import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
+import { IndexableDocument } from '@backstage/plugin-search-common';
+import { Logger } from 'winston';
+import { Readable } from 'stream';
+
+// @public
+const searchStackOverflowCollatorModule: () => BackendFeature;
+export default searchStackOverflowCollatorModule;
+
+// @public
+export interface StackOverflowDocument extends IndexableDocument {
+ // (undocumented)
+ answers: number;
+ // (undocumented)
+ tags: string[];
+}
+
+// @public
+export class StackOverflowQuestionsCollatorFactory
+ implements DocumentCollatorFactory
+{
+ // (undocumented)
+ execute(): AsyncGenerator;
+ // (undocumented)
+ static fromConfig(
+ config: Config,
+ options: StackOverflowQuestionsCollatorFactoryOptions,
+ ): StackOverflowQuestionsCollatorFactory;
+ // (undocumented)
+ getCollator(): Promise;
+ // (undocumented)
+ protected requestParams: StackOverflowQuestionsRequestParams;
+ // (undocumented)
+ readonly type: string;
+}
+
+// @public
+export type StackOverflowQuestionsCollatorFactoryOptions = {
+ baseUrl?: string;
+ maxPage?: number;
+ apiKey?: string;
+ apiAccessToken?: string;
+ teamName?: string;
+ requestParams?: StackOverflowQuestionsRequestParams;
+ logger: Logger;
+};
+
+// @public
+export type StackOverflowQuestionsRequestParams = {
+ [key: string]: string | string[] | number;
+};
+```
diff --git a/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml
new file mode 100644
index 0000000000..ed73bd0eca
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml
@@ -0,0 +1,10 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: backstage-plugin-search-backend-module-stack-overflow-collator
+ title: '@backstage/plugin-search-backend-module-stack-overflow-collator'
+ description: A module for the search backend that exports stack overflow modules
+spec:
+ lifecycle: experimental
+ type: backstage-backend-plugin-module
+ owner: discoverability-maintainers
diff --git a/plugins/search-backend-module-stack-overflow-collator/config.d.ts b/plugins/search-backend-module-stack-overflow-collator/config.d.ts
new file mode 100644
index 0000000000..d615179ed0
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/config.d.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface Config {
+ /**
+ * Configuration options for the stack overflow plugin
+ */
+ stackoverflow?: {
+ /**
+ * The base url of the Stack Overflow API used for the plugin
+ */
+ baseUrl?: string;
+
+ /**
+ * The API key to authenticate to Stack Overflow API
+ * @visibility secret
+ */
+ apiKey?: string;
+
+ /**
+ * The name of the team for a Stack Overflow for Teams account
+ */
+ teamName?: string;
+
+ /**
+ * The API Access Token to authenticate to Stack Overflow API
+ * @visibility secret
+ */
+ apiAccessToken?: string;
+
+ /**
+ * Type representing the request parameters.
+ */
+ requestParams?: {
+ [key: string]: string | string[] | number;
+ };
+ };
+}
diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json
new file mode 100644
index 0000000000..14e547de8e
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@backstage/plugin-search-backend-module-stack-overflow-collator",
+ "description": "A module for the search backend that exports stack overflow modules",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/search-backend-module-stack-overflow"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/backend-tasks": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/plugin-search-backend-node": "workspace:^",
+ "@backstage/plugin-search-common": "workspace:^",
+ "node-fetch": "^2.6.7",
+ "qs": "^6.9.4",
+ "winston": "^3.2.1"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "msw": "^1.2.1"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts
similarity index 100%
rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts
rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts
diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts
similarity index 90%
rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts
rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts
index c5f8a8c16b..3c97937365 100644
--- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts
+++ b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts
@@ -54,7 +54,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = {
apiKey?: string;
apiAccessToken?: string;
teamName?: string;
- requestParams: StackOverflowQuestionsRequestParams;
+ requestParams?: StackOverflowQuestionsRequestParams;
logger: Logger;
};
@@ -81,7 +81,14 @@ export class StackOverflowQuestionsCollatorFactory
this.apiAccessToken = options.apiAccessToken;
this.teamName = options.teamName;
this.maxPage = options.maxPage;
- this.requestParams = options.requestParams;
+ // Sets the same default request parameters as the official API documentation
+ // See https://api.stackexchange.com/docs/questions
+ this.requestParams = options.requestParams ?? {
+ order: 'desc',
+ sort: 'activity',
+ site: 'stackoverflow',
+ ...(options.requestParams ?? {}),
+ };
this.logger = options.logger.child({ documentType: this.type });
}
@@ -98,12 +105,16 @@ export class StackOverflowQuestionsCollatorFactory
config.getOptionalString('stackoverflow.baseUrl') ||
'https://api.stackexchange.com/2.3';
const maxPage = options.maxPage || 100;
+ const requestParams = config
+ .getOptionalConfig('stackoverflow.requestParams')
+ ?.get();
return new StackOverflowQuestionsCollatorFactory({
baseUrl,
maxPage,
apiKey,
apiAccessToken,
teamName,
+ requestParams,
...options,
});
}
@@ -175,7 +186,7 @@ export class StackOverflowQuestionsCollatorFactory
);
const data = await res.json();
- for (const question of data.items) {
+ for (const question of data.items ?? []) {
yield {
title: question.title,
location: question.link,
diff --git a/plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts
new file mode 100644
index 0000000000..1170b52423
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {
+ type StackOverflowDocument,
+ type StackOverflowQuestionsRequestParams,
+ type StackOverflowQuestionsCollatorFactoryOptions,
+ StackOverflowQuestionsCollatorFactory,
+} from './StackOverflowQuestionsCollatorFactory';
diff --git a/plugins/search-backend-module-stack-overflow-collator/src/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/index.ts
new file mode 100644
index 0000000000..00d57a7b27
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/src/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports Stack Overflow modules.
+ */
+
+export * from './collators';
+export { searchStackOverflowCollatorModule as default } from './module';
diff --git a/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts
new file mode 100644
index 0000000000..a785ead6eb
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule';
+
+describe('searchStackOverflowCollatorModule', () => {
+ const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ it('should register the stack overflow collator to the search index registry extension point with factory and schedule', async () => {
+ const extensionPointMock = {
+ addCollator: jest.fn(),
+ };
+
+ await startTestBackend({
+ extensionPoints: [
+ [searchIndexRegistryExtensionPoint, extensionPointMock],
+ ],
+ features: [
+ searchStackOverflowCollatorModule(),
+ mockServices.rootConfig.factory({
+ data: {
+ stackoverflow: {
+ schedule,
+ },
+ },
+ }),
+ ],
+ });
+
+ expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1);
+ expect(extensionPointMock.addCollator).toHaveBeenCalledWith({
+ factory: expect.objectContaining({ type: 'stack-overflow' }),
+ schedule: expect.objectContaining({ run: expect.any(Function) }),
+ });
+ });
+});
diff --git a/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts
new file mode 100644
index 0000000000..3ef3d72c32
--- /dev/null
+++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks';
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { StackOverflowQuestionsCollatorFactory } from '../collators';
+
+/**
+ * @public
+ * Search backend module for the Stack Overflow index.
+ */
+export const searchStackOverflowCollatorModule = createBackendModule({
+ moduleId: 'stackOverflowCollator',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ config: coreServices.rootConfig,
+ logger: coreServices.logger,
+ discovery: coreServices.discovery,
+ scheduler: coreServices.scheduler,
+ indexRegistry: searchIndexRegistryExtensionPoint,
+ },
+ async init({ config, logger, scheduler, indexRegistry }) {
+ const defaultSchedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ const schedule = config.has('stackoverflow.schedule')
+ ? readTaskScheduleDefinitionFromConfig(
+ config.getConfig('stackoverflow.schedule'),
+ )
+ : defaultSchedule;
+
+ indexRegistry.addCollator({
+ schedule: scheduler.createScheduledTaskRunner(schedule),
+ factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, {
+ logger: loggerToWinstonLogger(logger),
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/stack-overflow-backend/src/search/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts
similarity index 81%
rename from plugins/stack-overflow-backend/src/search/index.ts
rename to plugins/search-backend-module-stack-overflow-collator/src/module/index.ts
index ed3e05cb28..ab424398e0 100644
--- a/plugins/stack-overflow-backend/src/search/index.ts
+++ b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Backstage Authors
+ * Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export * from './StackOverflowQuestionsCollatorFactory';
+export { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule';
diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts
index ae5a541e81..27cfae3fdf 100644
--- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts
+++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts
@@ -329,11 +329,11 @@ export function parseHighlightFields({
const highlightedField = positions.reduce((content, pos) => {
return (
- `${content.substring(0, pos[0])}${preTag}` +
- `${content.substring(pos[0], pos[0] + pos[1])}` +
- `${postTag}${content.substring(pos[0] + pos[1])}`
+ `${String(content).substring(0, pos[0])}${preTag}` +
+ `${String(content).substring(pos[0], pos[0] + pos[1])}` +
+ `${postTag}${String(content).substring(pos[0] + pos[1])}`
);
- }, doc[field]);
+ }, doc[field] ?? '');
return [field, highlightedField];
}),
diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md
index 4575ab58c0..8129d36e38 100644
--- a/plugins/stack-overflow-backend/README.md
+++ b/plugins/stack-overflow-backend/README.md
@@ -1,64 +1,3 @@
-# Stack Overflow
+# Stack Overflow Backend
-A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App.
-
-## Getting started
-
-Before we begin, make sure:
-
-- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/).
-
-To use any of the functionality this plugin provides, you need to start by configuring your App with the following config:
-
-```yaml
-stackoverflow:
- baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
-```
-
-### Stack Overflow for Teams
-
-If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api).
-
-The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details.
-
-```yaml
-stackoverflow:
- baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
- apiKey: $STACK_OVERFLOW_API_KEY
- apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
-```
-
-```yaml
-stackoverflow:
- baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance
- teamName: $STACK_OVERFLOW_TEAM_NAME
- apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
-```
-
-## Areas of Responsibility
-
-This stack overflow backend plugin is primarily responsible for the following:
-
-- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search.
-
-### Index Stack Overflow Questions to search
-
-Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started).
-
-When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can modify the `requestParams`.
-
-> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions)
-
-```ts
-indexBuilder.addCollator({
- schedule,
- factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, {
- logger: env.logger,
- requestParams: {
- tagged: ['backstage'],
- site: 'stackoverflow',
- pagesize: 100,
- },
- }),
-});
-```
+Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md
index 8c601399c1..14dcdf2007 100644
--- a/plugins/stack-overflow-backend/api-report.md
+++ b/plugins/stack-overflow-backend/api-report.md
@@ -3,54 +3,21 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
-///
+import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator';
+import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator';
+import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator';
-import { Config } from '@backstage/config';
-import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
-import { IndexableDocument } from '@backstage/plugin-search-common';
-import { Logger } from 'winston';
-import { Readable } from 'stream';
+// @public @deprecated (undocumented)
+export type StackOverflowDocument = StackOverflowDocument_2;
-// @public
-export interface StackOverflowDocument extends IndexableDocument {
- // (undocumented)
- answers: number;
- // (undocumented)
- tags: string[];
-}
+// @public @deprecated (undocumented)
+export const StackOverflowQuestionsCollatorFactory: typeof StackOverflowQuestionsCollatorFactory_2;
-// @public
-export class StackOverflowQuestionsCollatorFactory
- implements DocumentCollatorFactory
-{
- // (undocumented)
- execute(): AsyncGenerator;
- // (undocumented)
- static fromConfig(
- config: Config,
- options: StackOverflowQuestionsCollatorFactoryOptions,
- ): StackOverflowQuestionsCollatorFactory;
- // (undocumented)
- getCollator(): Promise;
- // (undocumented)
- protected requestParams: StackOverflowQuestionsRequestParams;
- // (undocumented)
- readonly type: string;
-}
+// @public @deprecated (undocumented)
+export type StackOverflowQuestionsCollatorFactoryOptions =
+ StackOverflowQuestionsCollatorFactory_2;
-// @public
-export type StackOverflowQuestionsCollatorFactoryOptions = {
- baseUrl?: string;
- maxPage?: number;
- apiKey?: string;
- apiAccessToken?: string;
- teamName?: string;
- requestParams: StackOverflowQuestionsRequestParams;
- logger: Logger;
-};
-
-// @public
-export type StackOverflowQuestionsRequestParams = {
- [key: string]: string | string[] | number;
-};
+// @public @deprecated (undocumented)
+export type StackOverflowQuestionsRequestParams =
+ StackOverflowQuestionsRequestParams_2;
```
diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts
index b97f237c19..613a7aa887 100644
--- a/plugins/stack-overflow-backend/config.d.ts
+++ b/plugins/stack-overflow-backend/config.d.ts
@@ -40,5 +40,12 @@ export interface Config {
* @visibility secret
*/
apiAccessToken?: string;
+
+ /**
+ * Type representing the request parameters.
+ */
+ requestParams?: {
+ [key: string]: string | string[] | number;
+ };
};
}
diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json
index 320d35d3b6..77ebd8c69e 100644
--- a/plugins/stack-overflow-backend/package.json
+++ b/plugins/stack-overflow-backend/package.json
@@ -1,5 +1,6 @@
{
"name": "@backstage/plugin-stack-overflow-backend",
+ "description": "Deprecated, consider using @backstage/plugin-search-backend-module-stack-overflow-collator instead",
"version": "0.2.10",
"main": "src/index.ts",
"types": "src/index.ts",
@@ -34,6 +35,7 @@
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
+ "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"node-fetch": "^2.6.7",
"qs": "^6.9.4",
diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts
index 6e461b8183..5ca1c8d283 100644
--- a/plugins/stack-overflow-backend/src/index.ts
+++ b/plugins/stack-overflow-backend/src/index.ts
@@ -15,9 +15,46 @@
*/
/**
- * Stack Overflow backend plugin
- *
* @packageDocumentation
+ * Stack Overflow backend plugin
+ * @deprecated
+ * Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
*/
-export * from './search';
+import {
+ StackOverflowDocument as _StackOverflowDocument,
+ StackOverflowQuestionsRequestParams as _StackOverflowQuestionsRequestParams,
+ StackOverflowQuestionsCollatorFactory as _StackOverflowQuestionsCollatorFactory,
+ StackOverflowQuestionsCollatorFactoryOptions as _StackOverflowQuestionsCollatorFactoryOptions,
+} from '@backstage/plugin-search-backend-module-stack-overflow-collator';
+
+/**
+ * @public
+ * @deprecated
+ * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
+ */
+export type StackOverflowDocument = _StackOverflowDocument;
+
+/**
+ * @public
+ * @deprecated
+ * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
+ */
+export type StackOverflowQuestionsRequestParams =
+ _StackOverflowQuestionsRequestParams;
+
+/**
+ * @public
+ * @deprecated
+ * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
+ */
+export type StackOverflowQuestionsCollatorFactoryOptions =
+ _StackOverflowQuestionsCollatorFactory;
+
+/**
+ * @public
+ * @deprecated
+ * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead.
+ */
+export const StackOverflowQuestionsCollatorFactory =
+ _StackOverflowQuestionsCollatorFactory;
diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md
new file mode 100644
index 0000000000..167f8e1736
--- /dev/null
+++ b/plugins/stack-overflow/alpha-api-report.md
@@ -0,0 +1,13 @@
+## API Report File for "@backstage/plugin-stack-overflow"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackstagePlugin } from '@backstage/frontend-plugin-api';
+
+// @alpha (undocumented)
+const _default: BackstagePlugin<{}, {}>;
+export default _default;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json
index f60800d1f9..431f8a5b80 100644
--- a/plugins/stack-overflow/package.json
+++ b/plugins/stack-overflow/package.json
@@ -5,9 +5,22 @@
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
- "access": "public",
- "main": "dist/index.esm.js",
- "types": "dist/index.d.ts"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.tsx",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.tsx"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "frontend-plugin"
@@ -32,6 +45,7 @@
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-home-react": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx
new file mode 100644
index 0000000000..3708271ef1
--- /dev/null
+++ b/plugins/stack-overflow/src/alpha.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { configApiRef, createApiFactory } from '@backstage/core-plugin-api';
+import {
+ createApiExtension,
+ createPlugin,
+} from '@backstage/frontend-plugin-api';
+import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
+import { StackOverflowClient, stackOverflowApiRef } from './api';
+
+/** @alpha */
+const StackOverflowApi = createApiExtension({
+ factory: createApiFactory({
+ api: stackOverflowApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => StackOverflowClient.fromConfig(configApi),
+ }),
+});
+
+/** @alpha */
+const StackOverflowSearchResultListItem = createSearchResultListItemExtension({
+ id: 'stack-overflow',
+ predicate: result => result.type === 'stack-overflow',
+ component: () =>
+ import('./search/StackOverflowSearchResultListItem').then(
+ m => m.StackOverflowSearchResultListItem,
+ ),
+});
+
+/** @alpha */
+export default createPlugin({
+ id: 'stack-overflow',
+ // TODO: Migrate homepage cards when the declarative homepage plugin supports them
+ extensions: [StackOverflowApi, StackOverflowSearchResultListItem],
+});
diff --git a/yarn.lock b/yarn.lock
index cfe5779097..868d2e7ef2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8846,6 +8846,25 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:^, @backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator"
+ dependencies:
+ "@backstage/backend-common": "workspace:^"
+ "@backstage/backend-plugin-api": "workspace:^"
+ "@backstage/backend-tasks": "workspace:^"
+ "@backstage/backend-test-utils": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/config": "workspace:^"
+ "@backstage/plugin-search-backend-node": "workspace:^"
+ "@backstage/plugin-search-common": "workspace:^"
+ msw: ^1.2.1
+ node-fetch: ^2.6.7
+ qs: ^6.9.4
+ winston: ^3.2.1
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-search-backend-module-techdocs@workspace:^, @backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs":
version: 0.0.0-use.local
resolution: "@backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs"
@@ -9189,6 +9208,7 @@ __metadata:
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
+ "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^"
"@backstage/plugin-search-backend-node": "workspace:^"
"@backstage/plugin-search-common": "workspace:^"
msw: ^1.0.0
@@ -9208,6 +9228,7 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-home-react": "workspace:^"
"@backstage/plugin-search-common": "workspace:^"
"@backstage/plugin-search-react": "workspace:^"
@@ -25313,7 +25334,6 @@ __metadata:
"@backstage/plugin-search-react": "workspace:^"
"@backstage/plugin-sentry": "workspace:^"
"@backstage/plugin-shortcuts": "workspace:^"
- "@backstage/plugin-stack-overflow": "workspace:^"
"@backstage/plugin-stackstorm": "workspace:^"
"@backstage/plugin-tech-insights": "workspace:^"
"@backstage/plugin-tech-radar": "workspace:^"