Skip to content

Commit 6b21b19

Browse files
committed
feat(app-defaults): add app-auth and app-integration
This change adds an 'app-auth' plugin which provides the sign-in page and related authentiction module API factories to an app using the new frontend system. This change also adds a second 'app-integration' plugin which provides a default ScmAuth configuration. In both cases these plugins bring code migrated from the old frontend system setup in the RHDH repository. This change also adds a change to cleanup some noisy non-outcome altering output on the console when running the tests, along with some related updates to the eslint config so that the lint run by the pre-commit hook still works. This change also contains some updates to the app-defaults workspace README. Assisted-By: Cursor Desktop
1 parent c45aa19 commit 6b21b19

33 files changed

+1429
-132
lines changed

.eslintrc.cjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,15 @@ module.exports = {
243243
].flat(),
244244
},
245245
overrides: [
246+
{
247+
// .cjs is not matched by **/*.[jt]s?(x); without this, espree defaults to ES5 and
248+
// modern syntax (const, class fields) in e.g. jest-environment-*.cjs fails parsing.
249+
files: ['**/*.cjs'],
250+
parserOptions: {
251+
ecmaVersion: 'latest',
252+
sourceType: 'script',
253+
},
254+
},
246255
{
247256
files: ['**/*.[jt]s?(x)'],
248257
excludedFiles: '**/*.{test,spec}.[jt]s?(x)',
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
const path = require('path');
20+
21+
/**
22+
* ESLint ignores config-style dotfiles by default; passing them on the CLI only
23+
* produces noise. Skip eslint/prettier for those; still format via other globs if needed.
24+
*/
25+
function skipLintStagedEslintPrettier(file) {
26+
const base = path.basename(file.replace(/\\/g, '/'));
27+
return base === '.eslintrc.js' || base === '.lintstagedrc.cjs';
28+
}
29+
30+
module.exports = {
31+
'*.{js,jsx,ts,tsx,mjs,cjs}': filenames => {
32+
const filtered = filenames.filter(f => !skipLintStagedEslintPrettier(f));
33+
if (!filtered.length) {
34+
return [];
35+
}
36+
const quoted = filtered.map(f => JSON.stringify(f));
37+
return [
38+
`eslint --fix ${quoted.join(' ')}`,
39+
`prettier --write ${quoted.join(' ')}`,
40+
];
41+
},
42+
'*.{json,md}': ['prettier --write'],
43+
};

workspaces/app-defaults/README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
# [Backstage](https://backstage.io)
22

3-
This is your newly scaffolded Backstage App, Good Luck!
3+
RHDH **app-defaults** workspace: sample new-frontend-system app plus shared plugins (`app-react`, `app-auth`, `app-integrations`).
44

5-
To start the app, run:
5+
## Local app shell
6+
7+
To start the app from this directory:
68

79
```sh
810
yarn install
9-
yarn dev
11+
yarn start
1012
```
1113

14+
### Static `app-auth` wiring
15+
16+
The sample app in [`packages/app`](packages/app) loads RHDH sign-in and OIDC/Auth0/SAML frontend APIs by importing **`appAuthModule`** from `@red-hat-developer-hub/backstage-plugin-app-auth/alpha` and passing it to `createApp({ features: [...] })`. That mirrors how you can mount the module **statically** for local development and tests.
17+
18+
In **RHDH**, the same module is intended to be loaded **dynamically** via `@backstage/frontend-dynamic-feature-loader` and your export/overlays pipeline—not by editing the product `app-next` shell.
19+
20+
### Static `app-integrations` wiring
21+
22+
The sample app imports **`appIntegrationsModule`** from `@red-hat-developer-hub/backstage-plugin-app-integrations/alpha` and adds it to `createApp({ features: [...] })` (after `appAuthModule`). It registers **`scmIntegrationsApiRef`** and **`scmAuthApiRef`** on `pluginId: 'app'`, matching the classic RHDH [`packages/app` `apis.ts`](https://github.com/redhat-developer/rhdh/blob/main/packages/app/src/apis.ts) SCM factories—so catalog import, scaffolder, and similar features get the same default SCM auth behavior.
23+
24+
Deployments that want different SCM wiring can omit this module and supply their own dynamic (or static) module that registers those refs instead.
25+
26+
In **RHDH**, this module is expected to ship and load **dynamically** alongside `app-auth`, not via edits to `app-next`.
27+
28+
### Config for `app-auth`
29+
30+
- **`auth.environment`**: set to `development` locally so the RHDH sign-in page includes the **guest** provider (needed for [`App.test.tsx`](packages/app/src/App.test.tsx) inline `APP_CONFIG`, [`app-config.yaml`](app-config.yaml) for `yarn start` / Playwright, and typical dev flows).
31+
- **`signInPage`** (optional, root key): string or list of provider ids (e.g. `github`, `oidc`) for the RHDH multi-provider sign-in page. Schema lives on the `backstage-plugin-app-auth` package.
32+
33+
## Other commands
34+
1235
To generate knip reports for this app, run:
1336

1437
```sh

workspaces/app-defaults/app-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ techdocs:
7272
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
7373

7474
auth:
75+
# Required for RHDH app-auth SignInPage (guest in dev) and local e2e
76+
environment: development
7577
# see https://backstage.io/docs/auth/ to learn about auth providers
7678
providers:
7779
# See https://backstage.io/docs/auth/guest/provider
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
/**
20+
* Same as jest-environment-jsdom, but does not forward jsdom CSS parse errors to
21+
* Jest's console. Those come from modern @backstage/ui CSS (@layer, etc.) that jsdom's
22+
* parser cannot handle; they are harmless for unit tests.
23+
*
24+
* Derived from jest-environment-jsdom@29.7.0 (MIT).
25+
*/
26+
function isString(value) {
27+
return typeof value === 'string';
28+
}
29+
30+
function _jsdom() {
31+
const data = require('jsdom');
32+
_jsdom = function () {
33+
return data;
34+
};
35+
return data;
36+
}
37+
function _fakeTimers() {
38+
const data = require('@jest/fake-timers');
39+
_fakeTimers = function () {
40+
return data;
41+
};
42+
return data;
43+
}
44+
function _jestMock() {
45+
const data = require('jest-mock');
46+
_jestMock = function () {
47+
return data;
48+
};
49+
return data;
50+
}
51+
function _jestUtil() {
52+
const data = require('jest-util');
53+
_jestUtil = function () {
54+
return data;
55+
};
56+
return data;
57+
}
58+
59+
class JSDOMEnvironmentSuppressCss {
60+
dom;
61+
fakeTimers;
62+
fakeTimersModern;
63+
global;
64+
errorEventListener;
65+
moduleMocker;
66+
customExportConditions = ['browser'];
67+
_configuredExportConditions;
68+
constructor(config, context) {
69+
const { projectConfig } = config;
70+
const virtualConsole = new (_jsdom().VirtualConsole)();
71+
// jsdom 21+ uses forwardTo; older stacks (jest-environment-jsdom 29 + jsdom 20) use sendTo.
72+
if (typeof virtualConsole.forwardTo === 'function') {
73+
virtualConsole.forwardTo(context.console, { jsdomErrors: 'none' });
74+
} else if (typeof virtualConsole.sendTo === 'function') {
75+
virtualConsole.sendTo(context.console, { omitJSDOMErrors: true });
76+
} else {
77+
throw new TypeError(
78+
'Unsupported jsdom VirtualConsole: expected forwardTo or sendTo',
79+
);
80+
}
81+
virtualConsole.on('jsdomError', error => {
82+
if (
83+
error &&
84+
typeof error.message === 'string' &&
85+
error.message.includes('Could not parse CSS stylesheet')
86+
) {
87+
return;
88+
}
89+
context.console.error(error);
90+
});
91+
this.dom = new (_jsdom().JSDOM)(
92+
typeof projectConfig.testEnvironmentOptions.html === 'string'
93+
? projectConfig.testEnvironmentOptions.html
94+
: '<!DOCTYPE html>',
95+
{
96+
pretendToBeVisual: true,
97+
resources:
98+
typeof projectConfig.testEnvironmentOptions.userAgent === 'string'
99+
? new (_jsdom().ResourceLoader)({
100+
userAgent: projectConfig.testEnvironmentOptions.userAgent,
101+
})
102+
: undefined,
103+
runScripts: 'dangerously',
104+
url: 'http://localhost/',
105+
virtualConsole,
106+
...projectConfig.testEnvironmentOptions,
107+
},
108+
);
109+
const global = (this.global = this.dom.window);
110+
if (global == null) {
111+
throw new TypeError('JSDOM did not return a Window object');
112+
}
113+
114+
global.global = global;
115+
116+
this.global.Error.stackTraceLimit = 100;
117+
(0, _jestUtil().installCommonGlobals)(global, projectConfig.globals);
118+
119+
global.Buffer = Buffer;
120+
121+
this.errorEventListener = event => {
122+
if (userErrorListenerCount === 0 && event.error != null) {
123+
process.emit('uncaughtException', event.error);
124+
}
125+
};
126+
global.addEventListener('error', this.errorEventListener);
127+
128+
const originalAddListener = global.addEventListener.bind(global);
129+
const originalRemoveListener = global.removeEventListener.bind(global);
130+
let userErrorListenerCount = 0;
131+
global.addEventListener = function (...args) {
132+
if (args[0] === 'error') {
133+
userErrorListenerCount++;
134+
}
135+
return originalAddListener.apply(this, args);
136+
};
137+
global.removeEventListener = function (...args) {
138+
if (args[0] === 'error') {
139+
userErrorListenerCount--;
140+
}
141+
return originalRemoveListener.apply(this, args);
142+
};
143+
if ('customExportConditions' in projectConfig.testEnvironmentOptions) {
144+
const { customExportConditions } = projectConfig.testEnvironmentOptions;
145+
if (
146+
Array.isArray(customExportConditions) &&
147+
customExportConditions.every(isString)
148+
) {
149+
this._configuredExportConditions = customExportConditions;
150+
} else {
151+
throw new TypeError(
152+
'Custom export conditions specified but they are not an array of strings',
153+
);
154+
}
155+
}
156+
this.moduleMocker = new (_jestMock().ModuleMocker)(global);
157+
this.fakeTimers = new (_fakeTimers().LegacyFakeTimers)({
158+
config: projectConfig,
159+
global: global,
160+
moduleMocker: this.moduleMocker,
161+
timerConfig: {
162+
idToRef: id => id,
163+
refToId: ref => ref,
164+
},
165+
});
166+
this.fakeTimersModern = new (_fakeTimers().ModernFakeTimers)({
167+
config: projectConfig,
168+
global: global,
169+
});
170+
}
171+
172+
setup() {
173+
return Promise.resolve();
174+
}
175+
async teardown() {
176+
if (this.fakeTimers) {
177+
this.fakeTimers.dispose();
178+
}
179+
if (this.fakeTimersModern) {
180+
this.fakeTimersModern.dispose();
181+
}
182+
if (this.global != null) {
183+
if (this.errorEventListener) {
184+
this.global.removeEventListener('error', this.errorEventListener);
185+
}
186+
this.global.close();
187+
}
188+
this.errorEventListener = null;
189+
this.global = null;
190+
this.dom = null;
191+
this.fakeTimers = null;
192+
this.fakeTimersModern = null;
193+
}
194+
exportConditions() {
195+
return this._configuredExportConditions ?? this.customExportConditions;
196+
}
197+
getVmContext() {
198+
if (this.dom) {
199+
return this.dom.getInternalVMContext();
200+
}
201+
return null;
202+
}
203+
}
204+
205+
module.exports = JSDOMEnvironmentSuppressCss;
206+
module.exports.default = JSDOMEnvironmentSuppressCss;
207+
module.exports.TestEnvironment = JSDOMEnvironmentSuppressCss;

workspaces/app-defaults/package.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"fix": "backstage-cli repo fix",
2323
"lint": "backstage-cli repo lint --since origin/main",
2424
"lint:all": "backstage-cli repo lint",
25+
"prettier:fix": "prettier --write .",
2526
"prettier:check": "prettier --check .",
2627
"new": "backstage-cli new --scope @red-hat-developer-hub",
2728
"postinstall": "cd ../../ && yarn install"
@@ -51,14 +52,5 @@
5152
"@types/react": "^18",
5253
"@types/react-dom": "^18"
5354
},
54-
"prettier": "@spotify/prettier-config",
55-
"lint-staged": {
56-
"*.{js,jsx,ts,tsx,mjs,cjs}": [
57-
"eslint --fix",
58-
"prettier --write"
59-
],
60-
"*.{json,md}": [
61-
"prettier --write"
62-
]
63-
}
55+
"prettier": "@spotify/prettier-config"
6456
}

workspaces/app-defaults/packages/app/package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"backstage": {
1212
"role": "frontend"
1313
},
14+
"jest": {
15+
"testEnvironment": "../../../jest-environment-jsdom-suppress-css.cjs"
16+
},
1417
"scripts": {
1518
"start": "backstage-cli package start",
1619
"build": "backstage-cli package build",
@@ -44,6 +47,8 @@
4447
"@material-ui/core": "^4.12.2",
4548
"@material-ui/icons": "^4.9.1",
4649
"@mui/material": "^5.18.0",
50+
"@red-hat-developer-hub/backstage-plugin-app-auth": "workspace:^",
51+
"@red-hat-developer-hub/backstage-plugin-app-integrations": "workspace:^",
4752
"@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^",
4853
"react": "^18.0.2",
4954
"react-dom": "^18.0.2",
@@ -52,13 +57,14 @@
5257
},
5358
"devDependencies": {
5459
"@backstage/frontend-test-utils": "^0.5.1",
55-
"@playwright/test": "^1.32.3",
56-
"@testing-library/dom": "^9.0.0",
60+
"@playwright/test": "^1.58.2",
61+
"@testing-library/dom": "^10.0.0",
5762
"@testing-library/jest-dom": "^6.0.0",
58-
"@testing-library/react": "^14.0.0",
63+
"@testing-library/react": "^16.0.0",
5964
"@testing-library/user-event": "^14.0.0",
6065
"@types/react-dom": "*",
61-
"cross-env": "^7.0.0"
66+
"cross-env": "^10.0.0",
67+
"cross-fetch": "^4.0.0"
6268
},
6369
"browserslist": {
6470
"production": [

workspaces/app-defaults/packages/app/src/App.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('App', () => {
2525
{
2626
data: {
2727
app: { title: 'Test' },
28+
auth: { environment: 'development' },
2829
backend: { baseUrl: 'http://localhost:7007' },
2930
techdocs: {
3031
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',

0 commit comments

Comments
 (0)