@abgov/nx-adsp 12.15.0 → 12.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generators.json +12 -0
- package/package.json +1 -1
- package/src/generators/pean/pean.d.ts +3 -0
- package/src/generators/pean/pean.js +80 -0
- package/src/generators/pean/pean.js.map +1 -0
- package/src/generators/pean/pean.spec.ts +46 -0
- package/src/generators/pean/schema.d.ts +14 -0
- package/src/generators/pean/schema.json +60 -0
- package/src/generators/pern/pern.d.ts +3 -0
- package/src/generators/pern/pern.js +80 -0
- package/src/generators/pern/pern.js.map +1 -0
- package/src/generators/pern/pern.spec.ts +46 -0
- package/src/generators/pern/schema.d.ts +14 -0
- package/src/generators/pern/schema.json +60 -0
- package/src/utils/agent.d.ts +1 -1
package/generators.json
CHANGED
|
@@ -49,6 +49,18 @@
|
|
|
49
49
|
"schema": "./src/generators/mean/schema.json",
|
|
50
50
|
"description": "Generator that creates a MEAN fullstack solution.",
|
|
51
51
|
"hidden": true
|
|
52
|
+
},
|
|
53
|
+
"pern": {
|
|
54
|
+
"factory": "./src/generators/pern/pern",
|
|
55
|
+
"schema": "./src/generators/pern/schema.json",
|
|
56
|
+
"description": "Generator that creates a PERN fullstack solution.",
|
|
57
|
+
"hidden": true
|
|
58
|
+
},
|
|
59
|
+
"pean": {
|
|
60
|
+
"factory": "./src/generators/pean/pean",
|
|
61
|
+
"schema": "./src/generators/pean/schema.json",
|
|
62
|
+
"description": "Generator that creates a PEAN fullstack solution.",
|
|
63
|
+
"hidden": true
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = default_1;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const nx_oc_1 = require("@abgov/nx-oc");
|
|
7
|
+
const angular_app_1 = require("../angular-app/angular-app");
|
|
8
|
+
const express_service_1 = require("../express-service/express-service");
|
|
9
|
+
const agent_1 = require("../../utils/agent");
|
|
10
|
+
const keycloak_admin_1 = require("../../utils/keycloak-admin");
|
|
11
|
+
const plugin_version_1 = require("../../utils/plugin-version");
|
|
12
|
+
function normalizeOptions(host, options) {
|
|
13
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
var _a;
|
|
15
|
+
const adsp = yield (0, nx_oc_1.getAdspConfiguration)(host, options);
|
|
16
|
+
// Propagate the token from adsp to the Schema-level accessToken so that
|
|
17
|
+
// sub-generators (express-service, angular-app) that check options.accessToken
|
|
18
|
+
// see it and skip their own realmLogin fallback.
|
|
19
|
+
return Object.assign(Object.assign({}, options), { accessToken: (_a = adsp.accessToken) !== null && _a !== void 0 ? _a : options.accessToken, adsp });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function default_1(host, options) {
|
|
23
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
25
|
+
const normalizedOptions = yield normalizeOptions(host, options);
|
|
26
|
+
const projectName = (0, devkit_1.names)(options.name).fileName;
|
|
27
|
+
const serviceName = `${projectName}-service`;
|
|
28
|
+
const appName = `${projectName}-app`;
|
|
29
|
+
const appsDir = (0, devkit_1.getWorkspaceLayout)(host).appsDir;
|
|
30
|
+
const serviceRoot = `${appsDir}/${serviceName}`;
|
|
31
|
+
const appRoot = `${appsDir}/${appName}`;
|
|
32
|
+
// Scaffold both projects before the agent interaction so files exist to upload.
|
|
33
|
+
yield (0, express_service_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: serviceName, skipAgent: true, database: 'postgres' }));
|
|
34
|
+
yield (0, angular_app_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: appName, proxy: {
|
|
35
|
+
location: '/api/',
|
|
36
|
+
proxyPass: `http://${serviceName}:3333/${serviceName}/`,
|
|
37
|
+
}, skipAgent: true }));
|
|
38
|
+
if (normalizedOptions.adsp) {
|
|
39
|
+
const accessToken = (_a = normalizedOptions.adsp.accessToken) !== null && _a !== void 0 ? _a : normalizedOptions.accessToken;
|
|
40
|
+
const serviceClientId = `urn:ads:${normalizedOptions.adsp.tenant}:${serviceName}`;
|
|
41
|
+
const appClientId = `urn:ads:${normalizedOptions.adsp.tenant}:${appName}`;
|
|
42
|
+
yield (0, keycloak_admin_1.ensureAudienceMapper)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, appClientId, serviceClientId, accessToken);
|
|
43
|
+
yield (0, keycloak_admin_1.ensureClientRoleScope)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, appClientId, serviceClientId, 'example-role', accessToken);
|
|
44
|
+
}
|
|
45
|
+
if (normalizedOptions.adsp && !normalizedOptions.skipAgent) {
|
|
46
|
+
// Single conversation covering the full stack. Files from both projects are
|
|
47
|
+
// uploaded with service/ and app/ prefixes so the agent can write to both,
|
|
48
|
+
// and are routed to the correct project root when applied.
|
|
49
|
+
// Use the token from --tenant login if available; fall back to a realm login
|
|
50
|
+
// when the interactive flow was used (which only obtains a core-realm token).
|
|
51
|
+
const accessToken = (_b = normalizedOptions.adsp.accessToken) !== null && _b !== void 0 ? _b : (yield (0, nx_oc_1.realmLogin)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm).catch((err) => {
|
|
52
|
+
var _a;
|
|
53
|
+
process.stdout.write(`\n[nx-adsp] Agent sign-in failed (${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}) — skipping agent interaction.\n`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}));
|
|
56
|
+
yield (0, agent_1.confirmAfterAgentInterrupt)(yield (0, agent_1.consultAgent)(normalizedOptions.adsp.directoryServiceUrl, accessToken, {
|
|
57
|
+
projectName,
|
|
58
|
+
projectType: 'pean',
|
|
59
|
+
tenant: normalizedOptions.adsp.tenant,
|
|
60
|
+
pluginVersion: plugin_version_1.PLUGIN_VERSION,
|
|
61
|
+
existingFiles: {
|
|
62
|
+
'service/src/main.ts': (_d = (_c = host.read(`${serviceRoot}/src/main.ts`)) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '',
|
|
63
|
+
'service/src/environment.ts': (_f = (_e = host.read(`${serviceRoot}/src/environment.ts`)) === null || _e === void 0 ? void 0 : _e.toString()) !== null && _f !== void 0 ? _f : '',
|
|
64
|
+
'service/src/database.ts': (_h = (_g = host.read(`${serviceRoot}/src/database.ts`)) === null || _g === void 0 ? void 0 : _g.toString()) !== null && _h !== void 0 ? _h : '',
|
|
65
|
+
'service/src/events.ts': (_k = (_j = host.read(`${serviceRoot}/src/events.ts`)) === null || _j === void 0 ? void 0 : _j.toString()) !== null && _k !== void 0 ? _k : '',
|
|
66
|
+
'app/src/app/app.component.ts': (_m = (_l = host.read(`${appRoot}/src/app/app.component.ts`)) === null || _l === void 0 ? void 0 : _l.toString()) !== null && _m !== void 0 ? _m : '',
|
|
67
|
+
'app/src/app/app.component.html': (_p = (_o = host.read(`${appRoot}/src/app/app.component.html`)) === null || _o === void 0 ? void 0 : _o.toString()) !== null && _p !== void 0 ? _p : '',
|
|
68
|
+
'app/src/app/app.config.ts': (_r = (_q = host.read(`${appRoot}/src/app/app.config.ts`)) === null || _q === void 0 ? void 0 : _q.toString()) !== null && _r !== void 0 ? _r : '',
|
|
69
|
+
'app/src/app/app.routes.ts': (_t = (_s = host.read(`${appRoot}/src/app/app.routes.ts`)) === null || _s === void 0 ? void 0 : _s.toString()) !== null && _t !== void 0 ? _t : '',
|
|
70
|
+
'app/src/environments/environment.ts': (_v = (_u = host.read(`${appRoot}/src/environments/environment.ts`)) === null || _u === void 0 ? void 0 : _u.toString()) !== null && _v !== void 0 ? _v : '',
|
|
71
|
+
},
|
|
72
|
+
}, host, serviceRoot, { additionalRoots: { 'app': appRoot } }));
|
|
73
|
+
}
|
|
74
|
+
yield (0, devkit_1.formatFiles)(host);
|
|
75
|
+
return () => {
|
|
76
|
+
(0, devkit_1.installPackagesTask)(host);
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=pean.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pean.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/pean/pean.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readProjectConfiguration } from '@nx/devkit';
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
3
|
+
|
|
4
|
+
import * as utils from '@abgov/nx-oc';
|
|
5
|
+
import { environments } from '@abgov/nx-oc';
|
|
6
|
+
import { Schema } from './schema';
|
|
7
|
+
import generator from './pean';
|
|
8
|
+
|
|
9
|
+
jest.mock('@nx/devkit', () => ({ ...jest.requireActual('@nx/devkit'), formatFiles: jest.fn().mockResolvedValue(undefined) }));
|
|
10
|
+
jest.mock('@abgov/nx-oc');
|
|
11
|
+
jest.mock('../../utils/agent', () => ({
|
|
12
|
+
consultAgent: jest.fn().mockResolvedValue(null),
|
|
13
|
+
confirmAfterAgentInterrupt: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
}));
|
|
15
|
+
const utilsMock = utils as jest.Mocked<typeof utils>;
|
|
16
|
+
utilsMock.getAdspConfiguration.mockResolvedValue({
|
|
17
|
+
tenant: 'test',
|
|
18
|
+
tenantRealm: 'test',
|
|
19
|
+
accessServiceUrl: environments.test.accessServiceUrl,
|
|
20
|
+
directoryServiceUrl: environments.test.directoryServiceUrl,
|
|
21
|
+
});
|
|
22
|
+
utilsMock.realmLogin.mockResolvedValue('test-token');
|
|
23
|
+
|
|
24
|
+
describe('PEAN Generator', () => {
|
|
25
|
+
const options: Schema = {
|
|
26
|
+
name: 'test',
|
|
27
|
+
env: 'dev',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('can run', async () => {
|
|
31
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
32
|
+
await generator(host, options);
|
|
33
|
+
|
|
34
|
+
const appConfig = readProjectConfiguration(host, 'test-app');
|
|
35
|
+
expect(appConfig.root).toBe('apps/test-app');
|
|
36
|
+
|
|
37
|
+
const serviceConfig = readProjectConfiguration(host, 'test-service');
|
|
38
|
+
expect(serviceConfig.root).toBe('apps/test-service');
|
|
39
|
+
|
|
40
|
+
expect(host.exists('apps/test-app/nginx.conf')).toBeTruthy();
|
|
41
|
+
const nginxConf = host.read('apps/test-app/nginx.conf').toString();
|
|
42
|
+
expect(nginxConf).toContain('http://test-service:3333/');
|
|
43
|
+
|
|
44
|
+
expect(host.exists('apps/test-service/prisma/schema.prisma')).toBeTruthy();
|
|
45
|
+
}, 30000);
|
|
46
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdspConfiguration, EnvironmentName } from '@abgov/nx-oc';
|
|
2
|
+
|
|
3
|
+
export interface Schema {
|
|
4
|
+
name: string;
|
|
5
|
+
env: EnvironmentName;
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
tenant?: string;
|
|
8
|
+
tenantRealm?: string;
|
|
9
|
+
skipAgent?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NormalizedSchema extends Schema {
|
|
13
|
+
adsp: AdspConfiguration;
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"id": "NxAdspPean",
|
|
4
|
+
"title": "PEAN Stack ADSP Solution",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Name of the PEAN stack solution.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "argv",
|
|
12
|
+
"index": 0
|
|
13
|
+
},
|
|
14
|
+
"x-prompt": "What name would you like to use?"
|
|
15
|
+
},
|
|
16
|
+
"env": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Environment to target.",
|
|
19
|
+
"$default": {
|
|
20
|
+
"$source": "argv",
|
|
21
|
+
"index": 1
|
|
22
|
+
},
|
|
23
|
+
"alias": "e",
|
|
24
|
+
"x-prompt": {
|
|
25
|
+
"message": "Which ADSP environment do you want to target?",
|
|
26
|
+
"type": "list",
|
|
27
|
+
"items": [
|
|
28
|
+
"dev",
|
|
29
|
+
"test",
|
|
30
|
+
"prod"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"tenant": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "ADSP tenant name. Enables a single browser login for the full stack — service and frontend agent interactions both use the same tenant-realm token.",
|
|
37
|
+
"alias": "t"
|
|
38
|
+
},
|
|
39
|
+
"tenantRealm": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Keycloak realm UUID. Optional when --tenant is provided — overrides the realm looked up from the tenant service.",
|
|
42
|
+
"alias": "tr"
|
|
43
|
+
},
|
|
44
|
+
"accessToken": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Access token for retrieving configuration from ADSP APIs.",
|
|
47
|
+
"alias": "at"
|
|
48
|
+
},
|
|
49
|
+
"skipAgent": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"description": "Skip the consultAgent interaction and generate base scaffolding only.",
|
|
52
|
+
"default": false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"required": [
|
|
56
|
+
"name",
|
|
57
|
+
"env"
|
|
58
|
+
],
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = default_1;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const nx_oc_1 = require("@abgov/nx-oc");
|
|
7
|
+
const express_service_1 = require("../express-service/express-service");
|
|
8
|
+
const react_app_1 = require("../react-app/react-app");
|
|
9
|
+
const agent_1 = require("../../utils/agent");
|
|
10
|
+
const keycloak_admin_1 = require("../../utils/keycloak-admin");
|
|
11
|
+
const plugin_version_1 = require("../../utils/plugin-version");
|
|
12
|
+
function normalizeOptions(host, options) {
|
|
13
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
var _a;
|
|
15
|
+
const adsp = yield (0, nx_oc_1.getAdspConfiguration)(host, options);
|
|
16
|
+
// Propagate the token from adsp to the Schema-level accessToken so that
|
|
17
|
+
// sub-generators (express-service, react-app) that check options.accessToken
|
|
18
|
+
// see it and skip their own realmLogin fallback.
|
|
19
|
+
return Object.assign(Object.assign({}, options), { accessToken: (_a = adsp.accessToken) !== null && _a !== void 0 ? _a : options.accessToken, adsp });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function default_1(host, options) {
|
|
23
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
25
|
+
const normalizedOptions = yield normalizeOptions(host, options);
|
|
26
|
+
const projectName = (0, devkit_1.names)(options.name).fileName;
|
|
27
|
+
const serviceName = `${projectName}-service`;
|
|
28
|
+
const appName = `${projectName}-app`;
|
|
29
|
+
const appsDir = (0, devkit_1.getWorkspaceLayout)(host).appsDir;
|
|
30
|
+
const serviceRoot = `${appsDir}/${serviceName}`;
|
|
31
|
+
const appRoot = `${appsDir}/${appName}`;
|
|
32
|
+
// Scaffold both projects before the agent interaction so files exist to upload.
|
|
33
|
+
yield (0, express_service_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: serviceName, skipAgent: true, database: 'postgres' }));
|
|
34
|
+
yield (0, react_app_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: appName, proxy: {
|
|
35
|
+
location: '/api/',
|
|
36
|
+
proxyPass: `http://${serviceName}:3333/${serviceName}/`,
|
|
37
|
+
}, skipAgent: true }));
|
|
38
|
+
if (normalizedOptions.adsp) {
|
|
39
|
+
const accessToken = (_a = normalizedOptions.adsp.accessToken) !== null && _a !== void 0 ? _a : normalizedOptions.accessToken;
|
|
40
|
+
const serviceClientId = `urn:ads:${normalizedOptions.adsp.tenant}:${serviceName}`;
|
|
41
|
+
const appClientId = `urn:ads:${normalizedOptions.adsp.tenant}:${appName}`;
|
|
42
|
+
yield (0, keycloak_admin_1.ensureAudienceMapper)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, appClientId, serviceClientId, accessToken);
|
|
43
|
+
yield (0, keycloak_admin_1.ensureClientRoleScope)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, appClientId, serviceClientId, 'example-role', accessToken);
|
|
44
|
+
}
|
|
45
|
+
if (normalizedOptions.adsp && !normalizedOptions.skipAgent) {
|
|
46
|
+
// Single conversation covering the full stack. Files from both projects are
|
|
47
|
+
// uploaded with service/ and app/ prefixes so the agent can write to both,
|
|
48
|
+
// and are routed to the correct project root when applied.
|
|
49
|
+
// Use the token from --tenant login if available; fall back to a realm login
|
|
50
|
+
// when the interactive flow was used (which only obtains a core-realm token).
|
|
51
|
+
const accessToken = (_b = normalizedOptions.adsp.accessToken) !== null && _b !== void 0 ? _b : (yield (0, nx_oc_1.realmLogin)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm).catch((err) => {
|
|
52
|
+
var _a;
|
|
53
|
+
process.stdout.write(`\n[nx-adsp] Agent sign-in failed (${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}) — skipping agent interaction.\n`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}));
|
|
56
|
+
yield (0, agent_1.confirmAfterAgentInterrupt)(yield (0, agent_1.consultAgent)(normalizedOptions.adsp.directoryServiceUrl, accessToken, {
|
|
57
|
+
projectName,
|
|
58
|
+
projectType: 'pern',
|
|
59
|
+
tenant: normalizedOptions.adsp.tenant,
|
|
60
|
+
pluginVersion: plugin_version_1.PLUGIN_VERSION,
|
|
61
|
+
existingFiles: {
|
|
62
|
+
'service/src/main.ts': (_d = (_c = host.read(`${serviceRoot}/src/main.ts`)) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '',
|
|
63
|
+
'service/src/environment.ts': (_f = (_e = host.read(`${serviceRoot}/src/environment.ts`)) === null || _e === void 0 ? void 0 : _e.toString()) !== null && _f !== void 0 ? _f : '',
|
|
64
|
+
'service/src/database.ts': (_h = (_g = host.read(`${serviceRoot}/src/database.ts`)) === null || _g === void 0 ? void 0 : _g.toString()) !== null && _h !== void 0 ? _h : '',
|
|
65
|
+
'service/src/events.ts': (_k = (_j = host.read(`${serviceRoot}/src/events.ts`)) === null || _j === void 0 ? void 0 : _j.toString()) !== null && _k !== void 0 ? _k : '',
|
|
66
|
+
'app/src/app/app.tsx': (_m = (_l = host.read(`${appRoot}/src/app/app.tsx`)) === null || _l === void 0 ? void 0 : _l.toString()) !== null && _m !== void 0 ? _m : '',
|
|
67
|
+
'app/src/store.ts': (_p = (_o = host.read(`${appRoot}/src/store.ts`)) === null || _o === void 0 ? void 0 : _o.toString()) !== null && _p !== void 0 ? _p : '',
|
|
68
|
+
'app/src/environments/environment.ts': (_r = (_q = host.read(`${appRoot}/src/environments/environment.ts`)) === null || _q === void 0 ? void 0 : _q.toString()) !== null && _r !== void 0 ? _r : '',
|
|
69
|
+
'app/src/app/config.slice.ts': (_t = (_s = host.read(`${appRoot}/src/app/config.slice.ts`)) === null || _s === void 0 ? void 0 : _s.toString()) !== null && _t !== void 0 ? _t : '',
|
|
70
|
+
'app/src/app/intake.slice.ts': (_v = (_u = host.read(`${appRoot}/src/app/intake.slice.ts`)) === null || _u === void 0 ? void 0 : _u.toString()) !== null && _v !== void 0 ? _v : '',
|
|
71
|
+
},
|
|
72
|
+
}, host, serviceRoot, { additionalRoots: { 'app': appRoot } }));
|
|
73
|
+
}
|
|
74
|
+
yield (0, devkit_1.formatFiles)(host);
|
|
75
|
+
return () => {
|
|
76
|
+
(0, devkit_1.installPackagesTask)(host);
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=pern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pern.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/pern/pern.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readProjectConfiguration } from '@nx/devkit';
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
3
|
+
|
|
4
|
+
import * as utils from '@abgov/nx-oc';
|
|
5
|
+
import { environments } from '@abgov/nx-oc';
|
|
6
|
+
import { Schema } from './schema';
|
|
7
|
+
import generator from './pern';
|
|
8
|
+
|
|
9
|
+
jest.mock('@nx/devkit', () => ({ ...jest.requireActual('@nx/devkit'), formatFiles: jest.fn().mockResolvedValue(undefined) }));
|
|
10
|
+
jest.mock('@abgov/nx-oc');
|
|
11
|
+
jest.mock('../../utils/agent', () => ({
|
|
12
|
+
consultAgent: jest.fn().mockResolvedValue(null),
|
|
13
|
+
confirmAfterAgentInterrupt: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
}));
|
|
15
|
+
const utilsMock = utils as jest.Mocked<typeof utils>;
|
|
16
|
+
utilsMock.getAdspConfiguration.mockResolvedValue({
|
|
17
|
+
tenant: 'test',
|
|
18
|
+
tenantRealm: 'test',
|
|
19
|
+
accessServiceUrl: environments.test.accessServiceUrl,
|
|
20
|
+
directoryServiceUrl: environments.test.directoryServiceUrl,
|
|
21
|
+
});
|
|
22
|
+
utilsMock.realmLogin.mockResolvedValue('test-token');
|
|
23
|
+
|
|
24
|
+
describe('PERN Generator', () => {
|
|
25
|
+
const options: Schema = {
|
|
26
|
+
name: 'test',
|
|
27
|
+
env: 'dev',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('can run', async () => {
|
|
31
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
32
|
+
await generator(host, options);
|
|
33
|
+
|
|
34
|
+
const appConfig = readProjectConfiguration(host, 'test-app');
|
|
35
|
+
expect(appConfig.root).toBe('apps/test-app');
|
|
36
|
+
|
|
37
|
+
const serviceConfig = readProjectConfiguration(host, 'test-service');
|
|
38
|
+
expect(serviceConfig.root).toBe('apps/test-service');
|
|
39
|
+
|
|
40
|
+
expect(host.exists('apps/test-app/nginx.conf')).toBeTruthy();
|
|
41
|
+
const nginxConf = host.read('apps/test-app/nginx.conf').toString();
|
|
42
|
+
expect(nginxConf).toContain('http://test-service:3333/');
|
|
43
|
+
|
|
44
|
+
expect(host.exists('apps/test-service/prisma/schema.prisma')).toBeTruthy();
|
|
45
|
+
}, 30000);
|
|
46
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdspConfiguration, EnvironmentName } from '@abgov/nx-oc';
|
|
2
|
+
|
|
3
|
+
export interface Schema {
|
|
4
|
+
name: string;
|
|
5
|
+
env: EnvironmentName;
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
tenant?: string;
|
|
8
|
+
tenantRealm?: string;
|
|
9
|
+
skipAgent?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NormalizedSchema extends Schema {
|
|
13
|
+
adsp: AdspConfiguration;
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"id": "NxAdspPern",
|
|
4
|
+
"title": "PERN Stack ADSP Solution",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Name of the PERN stack solution.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "argv",
|
|
12
|
+
"index": 0
|
|
13
|
+
},
|
|
14
|
+
"x-prompt": "What name would you like to use?"
|
|
15
|
+
},
|
|
16
|
+
"env": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Environment to target.",
|
|
19
|
+
"$default": {
|
|
20
|
+
"$source": "argv",
|
|
21
|
+
"index": 1
|
|
22
|
+
},
|
|
23
|
+
"alias": "e",
|
|
24
|
+
"x-prompt": {
|
|
25
|
+
"message": "Which ADSP environment do you want to target?",
|
|
26
|
+
"type": "list",
|
|
27
|
+
"items": [
|
|
28
|
+
"dev",
|
|
29
|
+
"test",
|
|
30
|
+
"prod"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"tenant": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "ADSP tenant name. Enables a single browser login for the full stack — service and frontend agent interactions both use the same tenant-realm token.",
|
|
37
|
+
"alias": "t"
|
|
38
|
+
},
|
|
39
|
+
"tenantRealm": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Keycloak realm UUID. Optional when --tenant is provided — overrides the realm looked up from the tenant service.",
|
|
42
|
+
"alias": "tr"
|
|
43
|
+
},
|
|
44
|
+
"accessToken": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Access token for retrieving configuration from ADSP APIs.",
|
|
47
|
+
"alias": "at"
|
|
48
|
+
},
|
|
49
|
+
"skipAgent": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"description": "Skip the consultAgent interaction and generate base scaffolding only.",
|
|
52
|
+
"default": false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"required": [
|
|
56
|
+
"name",
|
|
57
|
+
"env"
|
|
58
|
+
],
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
}
|
package/src/utils/agent.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function confirmAfterAgentInterrupt(result: AgentResult | null):
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function consultAgent(directoryServiceUrl: string, accessToken: string, projectContext: {
|
|
43
43
|
projectName: string;
|
|
44
|
-
projectType: 'express-service' | 'react-app' | 'angular-app' | 'mern' | 'mean';
|
|
44
|
+
projectType: 'express-service' | 'react-app' | 'angular-app' | 'mern' | 'mean' | 'pern' | 'pean';
|
|
45
45
|
tenant: string;
|
|
46
46
|
pluginVersion: string;
|
|
47
47
|
/** Content of key integration files for the agent to read and potentially modify. */
|