@abgov/nx-adsp 12.16.0 → 12.17.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 +17 -0
- package/package.json +5 -1
- package/src/generators/angular-app/angular-app.js +15 -8
- package/src/generators/angular-app/angular-app.js.map +1 -1
- package/src/generators/angular-app/files/src/app/app.component.ts__tmpl__ +2 -2
- package/src/generators/angular-app/schema.json +5 -0
- package/src/generators/mevn/mevn.d.ts +3 -0
- package/src/generators/mevn/mevn.js +70 -0
- package/src/generators/mevn/mevn.js.map +1 -0
- package/src/generators/mevn/mevn.spec.ts +47 -0
- package/src/generators/mevn/schema.d.ts +14 -0
- package/src/generators/mevn/schema.json +60 -0
- package/src/generators/pevn/pevn.d.ts +3 -0
- package/src/generators/pevn/pevn.js +70 -0
- package/src/generators/pevn/pevn.js.map +1 -0
- package/src/generators/pevn/pevn.spec.ts +47 -0
- package/src/generators/pevn/schema.d.ts +14 -0
- package/src/generators/pevn/schema.json +60 -0
- package/src/generators/react-app/schema.json +5 -0
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +97 -0
- package/src/generators/vue-app/files/nginx.conf__tmpl__ +45 -0
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +39 -0
- package/src/generators/vue-app/files/src/assets/banner.jpg +0 -0
- package/src/generators/vue-app/files/src/environments/environment.ts__tmpl__ +11 -0
- package/src/generators/vue-app/files/src/index.html__tmpl__ +22 -0
- package/src/generators/vue-app/files/src/main.ts__tmpl__ +28 -0
- package/src/generators/vue-app/files/src/router/index.ts__tmpl__ +29 -0
- package/src/generators/vue-app/files/src/silent-check-sso.html__tmpl__ +5 -0
- package/src/generators/vue-app/files/src/views/HomeView.vue__tmpl__ +49 -0
- package/src/generators/vue-app/files/src/views/ProtectedView.vue__tmpl__ +14 -0
- package/src/generators/vue-app/files/vite.config.ts__tmpl__ +50 -0
- package/src/generators/vue-app/schema.d.ts +22 -0
- package/src/generators/vue-app/schema.json +85 -0
- package/src/generators/vue-app/vue-app.d.ts +3 -0
- package/src/generators/vue-app/vue-app.js +140 -0
- package/src/generators/vue-app/vue-app.js.map +1 -0
- package/src/generators/vue-app/vue-app.spec.ts +93 -0
- package/src/utils/agent.d.ts +1 -1
- package/src/utils/agent.js +5 -3
- package/src/utils/agent.js.map +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# AGENTS.md — <%= projectName %>
|
|
2
|
+
|
|
3
|
+
Vue 3 frontend for the Alberta Digital Service Platform (ADSP).
|
|
4
|
+
Generated by `nx g @abgov/nx-adsp:vue-app`.
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
|
|
8
|
+
- **UI**: Vue 3 (Composition API) + GoA design system (`@abgov/web-components` — `goa-*` custom elements)
|
|
9
|
+
- **Auth**: `@dsb-norge/vue-keycloak-js` (wraps `keycloak-js`)
|
|
10
|
+
- **State**: Pinia
|
|
11
|
+
- **Router**: Vue Router v4
|
|
12
|
+
|
|
13
|
+
## Key files
|
|
14
|
+
|
|
15
|
+
| File | Purpose |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `src/main.ts` | Entry — registers Pinia, Router, and Keycloak plugin |
|
|
18
|
+
| `src/App.vue` | Shell — nav header with sign-in/out, hero banner, `<RouterView>` |
|
|
19
|
+
| `src/router/index.ts` | Routes — `/protected` guarded with `requiresAuth` meta |
|
|
20
|
+
| `src/views/HomeView.vue` | Public page — calls public and private APIs |
|
|
21
|
+
| `src/views/ProtectedView.vue` | Authenticated page — shows user info from token |
|
|
22
|
+
| `src/environments/environment.ts` | Access URL, realm, client ID — pre-set from ADSP tenant |
|
|
23
|
+
| `vite.config.ts` | Vite config — `isCustomElement` marks `goa-*` as web components |
|
|
24
|
+
|
|
25
|
+
## Auth pattern
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
29
|
+
|
|
30
|
+
// useKeycloak() returns a DeepReadonly<VueKeycloakInstance>
|
|
31
|
+
const { authenticated, fullName, ready, token, tokenParsed, keycloak } = useKeycloak();
|
|
32
|
+
|
|
33
|
+
authenticated // Ref<boolean> — true after sign-in
|
|
34
|
+
fullName // Ref<string | undefined> — display name from token
|
|
35
|
+
ready // Ref<boolean> — true once Keycloak init has settled
|
|
36
|
+
token // Ref<string | undefined> — current access token
|
|
37
|
+
|
|
38
|
+
// Call Keycloak actions via the underlying keycloak-js instance:
|
|
39
|
+
keycloak?.login()
|
|
40
|
+
keycloak?.logout({ redirectUri: window.location.origin })
|
|
41
|
+
|
|
42
|
+
// Refresh token before an authenticated API call:
|
|
43
|
+
await keycloak?.updateToken(30);
|
|
44
|
+
const bearer = keycloak?.token;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Route guard (in `router/index.ts`):
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
router.beforeEach((to) => {
|
|
51
|
+
if (to.meta.requiresAuth) {
|
|
52
|
+
const { authenticated, ready, keycloak } = useKeycloak();
|
|
53
|
+
if (!ready) return true; // let Keycloak init settle first
|
|
54
|
+
if (!authenticated) {
|
|
55
|
+
keycloak?.login({ redirectUri: window.location.origin + to.fullPath });
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## GoA design system
|
|
64
|
+
|
|
65
|
+
Use `goa-*` web components directly in Vue templates. The vite.config.ts
|
|
66
|
+
`isCustomElement` option prevents Vue from warning about unknown elements.
|
|
67
|
+
|
|
68
|
+
**Event names**: GoA web component interactive elements (buttons, inputs, etc.)
|
|
69
|
+
emit a `_click` custom event (not `click`) to avoid conflicts with native browser
|
|
70
|
+
events. Use `@_click` in Vue templates:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<goa-button type="primary" @_click="handleSave">Save</goa-button>
|
|
74
|
+
<goa-input @_change="handleChange" />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See [GoA web components docs](https://ui-components.alberta.ca) for the full
|
|
78
|
+
component and event reference.
|
|
79
|
+
|
|
80
|
+
## Backend API calls (proxy setup)
|
|
81
|
+
|
|
82
|
+
Use relative `/api/` paths — they route through Vite's dev proxy and nginx in production:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// ✓ correct
|
|
86
|
+
const res = await fetch('/api/v1/my-resource');
|
|
87
|
+
|
|
88
|
+
// ✗ wrong — bypasses proxy, won't work in production
|
|
89
|
+
const res = await fetch('http://localhost:3333/my-service/v1/my-resource');
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## What NOT to change
|
|
93
|
+
|
|
94
|
+
- `src/main.ts` — Keycloak plugin is registered once here; do not call `new Keycloak()` elsewhere
|
|
95
|
+
- `environments/environment.ts` — access URL and realm are pre-configured for the ADSP tenant
|
|
96
|
+
- `silent-check-sso.html` — required for keycloak-js silent SSO; must be served from the app root
|
|
97
|
+
- `vite.config.ts` — the `isCustomElement` predicate must stay to suppress Vue warnings for `goa-*` elements
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
events {
|
|
2
|
+
worker_connections 1024;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
http {
|
|
6
|
+
sendfile on;
|
|
7
|
+
include mime.types;
|
|
8
|
+
default_type application/octet-stream;
|
|
9
|
+
|
|
10
|
+
gzip on;
|
|
11
|
+
gzip_types text/plain text/css application/javascript application/json image/svg+xml font/woff2;
|
|
12
|
+
gzip_min_length 1000;
|
|
13
|
+
|
|
14
|
+
server {
|
|
15
|
+
listen 8080;
|
|
16
|
+
root /opt/app-root/src;
|
|
17
|
+
index index.html;
|
|
18
|
+
|
|
19
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
20
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
21
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
22
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
23
|
+
|
|
24
|
+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
25
|
+
expires 30d;
|
|
26
|
+
add_header Cache-Control "public, no-transform";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
location = /silent-check-sso.html {
|
|
30
|
+
add_header Cache-Control "no-store";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
location / {
|
|
34
|
+
try_files $uri /index.html;
|
|
35
|
+
}
|
|
36
|
+
<% nginxProxies.forEach(function(nginxProxy){ %>
|
|
37
|
+
location <%= nginxProxy.location %> {
|
|
38
|
+
proxy_pass <%= nginxProxy.proxyPass %>;
|
|
39
|
+
proxy_set_header Host $host;
|
|
40
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
41
|
+
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
|
42
|
+
}
|
|
43
|
+
<% }); %>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
3
|
+
import { RouterView } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
const { authenticated, fullName, ready, keycloak } = useKeycloak();
|
|
6
|
+
|
|
7
|
+
function login() { keycloak?.login(); }
|
|
8
|
+
function logout() { keycloak?.logout({ redirectUri: window.location.origin }); }
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<goa-microsite-header type="alpha" />
|
|
13
|
+
<goa-app-header url="/" heading="<%= projectName %>">
|
|
14
|
+
<span v-if="ready && fullName">{{ fullName }}</span>
|
|
15
|
+
<goa-button v-if="!authenticated && ready" type="tertiary" @_click="login">
|
|
16
|
+
Sign in
|
|
17
|
+
</goa-button>
|
|
18
|
+
<goa-button v-if="authenticated" type="tertiary" @_click="logout">
|
|
19
|
+
Sign out
|
|
20
|
+
</goa-button>
|
|
21
|
+
</goa-app-header>
|
|
22
|
+
<goa-hero-banner heading="<%= projectName %>" backgroundurl="/assets/banner.jpg" />
|
|
23
|
+
<main>
|
|
24
|
+
<RouterView />
|
|
25
|
+
</main>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
body { margin: 0; }
|
|
30
|
+
main {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: repeat(6, auto);
|
|
33
|
+
}
|
|
34
|
+
main > section {
|
|
35
|
+
grid-column: 2 / span 2;
|
|
36
|
+
margin-top: 40px;
|
|
37
|
+
margin-bottom: 40px;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title><%= projectName %></title>
|
|
6
|
+
<base href="/" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
9
|
+
<script
|
|
10
|
+
type="module"
|
|
11
|
+
src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"
|
|
12
|
+
></script>
|
|
13
|
+
<script
|
|
14
|
+
nomodule
|
|
15
|
+
src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"
|
|
16
|
+
></script>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div id="app"></div>
|
|
20
|
+
<script type="module" src="/src/main.ts"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import '@abgov/web-components';
|
|
2
|
+
import '@abgov/web-components/index.css';
|
|
3
|
+
import { createApp } from 'vue';
|
|
4
|
+
import { createPinia } from 'pinia';
|
|
5
|
+
import keycloakPlugin from '@dsb-norge/vue-keycloak-js';
|
|
6
|
+
|
|
7
|
+
import App from './App.vue';
|
|
8
|
+
import router from './router';
|
|
9
|
+
import { environment } from './environments/environment';
|
|
10
|
+
|
|
11
|
+
const app = createApp(App);
|
|
12
|
+
|
|
13
|
+
app.use(createPinia());
|
|
14
|
+
app.use(router);
|
|
15
|
+
app.use(keycloakPlugin, {
|
|
16
|
+
config: {
|
|
17
|
+
url: environment.access.url,
|
|
18
|
+
realm: environment.access.realm,
|
|
19
|
+
clientId: environment.access.client_id,
|
|
20
|
+
},
|
|
21
|
+
init: {
|
|
22
|
+
onLoad: 'check-sso',
|
|
23
|
+
pkceMethod: 'S256',
|
|
24
|
+
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.mount('#app');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
2
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
3
|
+
import HomeView from '../views/HomeView.vue';
|
|
4
|
+
|
|
5
|
+
const router = createRouter({
|
|
6
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
7
|
+
routes: [
|
|
8
|
+
{ path: '/', component: HomeView },
|
|
9
|
+
{
|
|
10
|
+
path: '/protected',
|
|
11
|
+
component: () => import('../views/ProtectedView.vue'),
|
|
12
|
+
meta: { requiresAuth: true },
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
router.beforeEach((to) => {
|
|
18
|
+
if (to.meta.requiresAuth) {
|
|
19
|
+
const { authenticated, ready, keycloak } = useKeycloak();
|
|
20
|
+
if (!ready) return true;
|
|
21
|
+
if (!authenticated) {
|
|
22
|
+
keycloak?.login({ redirectUri: window.location.origin + to.fullPath });
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default router;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
4
|
+
|
|
5
|
+
const { authenticated, keycloak } = useKeycloak();
|
|
6
|
+
const publicResource = ref('Not retrieved — is the backend service running?');
|
|
7
|
+
const privateResource = ref('Not retrieved — sign in first.');
|
|
8
|
+
|
|
9
|
+
onMounted(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch('/api/v1/public');
|
|
12
|
+
const data = await res.json();
|
|
13
|
+
publicResource.value = data.message;
|
|
14
|
+
} catch {
|
|
15
|
+
publicResource.value = 'Error loading public resource.';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (authenticated) {
|
|
19
|
+
try {
|
|
20
|
+
await keycloak?.updateToken(30);
|
|
21
|
+
const res = await fetch('/api/v1/private', {
|
|
22
|
+
headers: { Authorization: `Bearer ${keycloak?.token}` },
|
|
23
|
+
});
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
privateResource.value = data.message;
|
|
26
|
+
} catch {
|
|
27
|
+
privateResource.value = 'Error loading private resource.';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<section>
|
|
35
|
+
<h2>Welcome to <%= projectName %>!</h2>
|
|
36
|
+
<p>Don't panic. Start editing the project to build your digital service.</p>
|
|
37
|
+
<h3>A few things you might want to do next:</h3>
|
|
38
|
+
<ul>
|
|
39
|
+
<li>Public API: <strong>{{ publicResource }}</strong></li>
|
|
40
|
+
<li>Private API (sign in to access): <strong>{{ privateResource }}</strong></li>
|
|
41
|
+
<li>
|
|
42
|
+
Extend the API: add routes to <code>/<%= projectName %>/v1</code> in the backend service.
|
|
43
|
+
</li>
|
|
44
|
+
<li>
|
|
45
|
+
<router-link to="/protected">View the protected page</router-link>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</section>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
3
|
+
|
|
4
|
+
const { fullName, tokenParsed } = useKeycloak();
|
|
5
|
+
const userEmail = tokenParsed?.email ?? '';
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<section>
|
|
10
|
+
<h2>Protected page</h2>
|
|
11
|
+
<p>You are signed in as <strong>{{ fullName }}</strong> ({{ userEmail }}).</p>
|
|
12
|
+
<router-link to="/">← Back to home</router-link>
|
|
13
|
+
</section>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/// <reference types='vitest' />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import vue from '@vitejs/plugin-vue';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: __dirname,
|
|
7
|
+
cacheDir: '<%= offsetFromRoot %>node_modules/.vite/<%= projectName %>',
|
|
8
|
+
|
|
9
|
+
server: {
|
|
10
|
+
port: 4200,
|
|
11
|
+
host: 'localhost',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
preview: {
|
|
15
|
+
port: 4300,
|
|
16
|
+
host: 'localhost',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
plugins: [
|
|
20
|
+
vue({
|
|
21
|
+
template: {
|
|
22
|
+
compilerOptions: {
|
|
23
|
+
// Treat goa-* custom elements as web components, not Vue components.
|
|
24
|
+
isCustomElement: (tag) => tag.startsWith('goa-'),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
test: {
|
|
31
|
+
watch: false,
|
|
32
|
+
globals: true,
|
|
33
|
+
environment: 'jsdom',
|
|
34
|
+
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'],
|
|
35
|
+
reporters: ['default'],
|
|
36
|
+
coverage: {
|
|
37
|
+
reportsDirectory: '<%= offsetFromRoot %>coverage/<%= projectRoot %>',
|
|
38
|
+
provider: 'v8',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
build: {
|
|
43
|
+
outDir: '<%= offsetFromRoot %>dist/<%= projectRoot %>',
|
|
44
|
+
emptyOutDir: true,
|
|
45
|
+
reportCompressedSize: true,
|
|
46
|
+
commonjsOptions: {
|
|
47
|
+
transformMixedEsModules: true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AdspConfiguration, EnvironmentName } from '@abgov/nx-oc';
|
|
2
|
+
import { NginxProxyConfiguration } from '../../utils/nginx';
|
|
3
|
+
|
|
4
|
+
export interface Schema {
|
|
5
|
+
name: string;
|
|
6
|
+
env: EnvironmentName;
|
|
7
|
+
accessToken?: string;
|
|
8
|
+
tenant?: string;
|
|
9
|
+
tenantRealm?: string;
|
|
10
|
+
serviceClientId?: string;
|
|
11
|
+
proxy?: NginxProxyConfiguration | NginxProxyConfiguration[];
|
|
12
|
+
/** When true, skip the agent interaction. Used by composite generators that run the agent themselves. */
|
|
13
|
+
skipAgent?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface NormalizedSchema extends Schema {
|
|
17
|
+
projectName: string;
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
openshiftDirectory: string;
|
|
20
|
+
adsp: AdspConfiguration;
|
|
21
|
+
nginxProxies: NginxProxyConfiguration[];
|
|
22
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"id": "NxAdspVueApp",
|
|
4
|
+
"title": "Vue ADSP Application",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Name of the application.",
|
|
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. Looks up the tenant realm and opens a single browser login, avoiding a separate interactive tenant selection.",
|
|
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
|
+
"serviceClientId": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Client ID of a paired backend service (e.g. urn:ads:my-tenant:my-svc). When provided with --tenant, configures audience mapping and example-role scope on the frontend client.",
|
|
52
|
+
"alias": "sc"
|
|
53
|
+
},
|
|
54
|
+
"proxy": {
|
|
55
|
+
"oneOf": [
|
|
56
|
+
{
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"location": { "type": "string" },
|
|
62
|
+
"proxyPass": { "type": "string" }
|
|
63
|
+
},
|
|
64
|
+
"required": ["location", "proxyPass"]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"location": { "type": "string" },
|
|
71
|
+
"proxyPass": { "type": "string" }
|
|
72
|
+
},
|
|
73
|
+
"required": ["location", "proxyPass"]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"skipAgent": {
|
|
78
|
+
"type": "boolean",
|
|
79
|
+
"description": "Skip the consultAgent interaction and generate base scaffolding only.",
|
|
80
|
+
"default": false
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"required": ["name", "env"],
|
|
84
|
+
"additionalProperties": false
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = default_1;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const nx_oc_1 = require("@abgov/nx-oc");
|
|
6
|
+
const agent_1 = require("../../utils/agent");
|
|
7
|
+
const keycloak_admin_1 = require("../../utils/keycloak-admin");
|
|
8
|
+
const plugin_version_1 = require("../../utils/plugin-version");
|
|
9
|
+
const quality_1 = require("../../utils/quality");
|
|
10
|
+
const devkit_1 = require("@nx/devkit");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
function normalizeOptions(host, options) {
|
|
13
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const projectName = (0, devkit_1.names)(options.name).fileName;
|
|
15
|
+
const projectRoot = `${(0, devkit_1.getWorkspaceLayout)(host).appsDir}/${projectName}`;
|
|
16
|
+
const openshiftDirectory = `.openshift/${projectName}`;
|
|
17
|
+
const adsp = yield (0, nx_oc_1.getAdspConfiguration)(host, options);
|
|
18
|
+
const nginxProxies = Array.isArray(options.proxy)
|
|
19
|
+
? [...options.proxy]
|
|
20
|
+
: options.proxy
|
|
21
|
+
? [options.proxy]
|
|
22
|
+
: [];
|
|
23
|
+
return Object.assign(Object.assign({}, options), { projectName, projectRoot, openshiftDirectory, adsp, nginxProxies });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function addFiles(host, options) {
|
|
27
|
+
const templateOptions = Object.assign(Object.assign(Object.assign({}, options), options.adsp), { offsetFromRoot: (0, devkit_1.offsetFromRoot)(options.projectRoot), tmpl: '' });
|
|
28
|
+
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), options.projectRoot, templateOptions);
|
|
29
|
+
const addProxyConf = options.nginxProxies.length > 0;
|
|
30
|
+
if (addProxyConf) {
|
|
31
|
+
const devProxyConf = options.nginxProxies.reduce((proxyConf, nginxProxy) => {
|
|
32
|
+
const upstreamUrl = new URL(nginxProxy.proxyPass);
|
|
33
|
+
const proxy = {
|
|
34
|
+
target: `${upstreamUrl.protocol}//localhost${upstreamUrl.port ? ':' + upstreamUrl.port : ''}`,
|
|
35
|
+
secure: upstreamUrl.protocol === 'https:',
|
|
36
|
+
changeOrigin: false,
|
|
37
|
+
pathRewrite: {},
|
|
38
|
+
};
|
|
39
|
+
if (upstreamUrl.pathname.length > 1) {
|
|
40
|
+
proxy.pathRewrite = { [`^${nginxProxy.location}`]: upstreamUrl.pathname };
|
|
41
|
+
}
|
|
42
|
+
return Object.assign(Object.assign({}, proxyConf), { [nginxProxy.location]: proxy });
|
|
43
|
+
}, {});
|
|
44
|
+
(0, devkit_1.writeJson)(host, `${options.projectRoot}/vite.proxy.json`, devProxyConf);
|
|
45
|
+
}
|
|
46
|
+
return addProxyConf;
|
|
47
|
+
}
|
|
48
|
+
function default_1(host, options) {
|
|
49
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
51
|
+
const normalizedOptions = yield normalizeOptions(host, options);
|
|
52
|
+
const { applicationGenerator: initVue } = yield Promise.resolve().then(() => require('@nx/vue'));
|
|
53
|
+
yield initVue(host, {
|
|
54
|
+
name: options.name,
|
|
55
|
+
style: 'css',
|
|
56
|
+
skipFormat: true,
|
|
57
|
+
linter: 'eslint',
|
|
58
|
+
unitTestRunner: 'vitest',
|
|
59
|
+
e2eTestRunner: 'none',
|
|
60
|
+
routing: true,
|
|
61
|
+
directory: normalizedOptions.projectRoot,
|
|
62
|
+
});
|
|
63
|
+
(0, devkit_1.addDependenciesToPackageJson)(host, {
|
|
64
|
+
'@abgov/design-tokens': '1.8.0',
|
|
65
|
+
'@abgov/web-components': '1.39.3',
|
|
66
|
+
'@dsb-norge/vue-keycloak-js': '^3.0.0',
|
|
67
|
+
'keycloak-js': '^23.0.7',
|
|
68
|
+
'pinia': '^2.0.0',
|
|
69
|
+
'vue-router': '^4.0.0',
|
|
70
|
+
}, {
|
|
71
|
+
'eslint-plugin-security': '^3.0.0',
|
|
72
|
+
'eslint-plugin-no-secrets': '^2.0.0',
|
|
73
|
+
});
|
|
74
|
+
// Remove Nx scaffold files replaced by our templates.
|
|
75
|
+
for (const f of [
|
|
76
|
+
'src/App.vue',
|
|
77
|
+
'src/components/HelloWorld.vue',
|
|
78
|
+
'src/views/AboutView.vue',
|
|
79
|
+
]) {
|
|
80
|
+
if (host.exists(`${normalizedOptions.projectRoot}/${f}`)) {
|
|
81
|
+
host.delete(`${normalizedOptions.projectRoot}/${f}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const addedProxy = addFiles(host, normalizedOptions);
|
|
85
|
+
(0, quality_1.addJestCoverageConfig)(host, normalizedOptions.projectRoot);
|
|
86
|
+
(0, quality_1.addVsCodeSettings)(host);
|
|
87
|
+
const config = (0, devkit_1.readProjectConfiguration)(host, options.name);
|
|
88
|
+
// Wire the vite dev-server proxy when nginx proxy locations are configured.
|
|
89
|
+
if (addedProxy && ((_a = config.targets.serve) === null || _a === void 0 ? void 0 : _a.options)) {
|
|
90
|
+
config.targets.serve.options = Object.assign(Object.assign({}, config.targets.serve.options), { proxyConfig: `${normalizedOptions.projectRoot}/vite.proxy.json` });
|
|
91
|
+
}
|
|
92
|
+
// Ensure silent-check-sso.html is served as a static asset.
|
|
93
|
+
if ((_b = config.targets.build) === null || _b === void 0 ? void 0 : _b.options) {
|
|
94
|
+
config.targets.build.options = Object.assign(Object.assign({}, config.targets.build.options), { assets: [
|
|
95
|
+
...((_c = config.targets.build.options.assets) !== null && _c !== void 0 ? _c : []),
|
|
96
|
+
{
|
|
97
|
+
glob: 'nginx.conf',
|
|
98
|
+
input: normalizedOptions.projectRoot,
|
|
99
|
+
output: './',
|
|
100
|
+
},
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
(0, devkit_1.updateProjectConfiguration)(host, options.name, config);
|
|
104
|
+
(0, quality_1.addSemgrepTarget)(host, options.name);
|
|
105
|
+
yield (0, devkit_1.formatFiles)(host);
|
|
106
|
+
if (normalizedOptions.adsp) {
|
|
107
|
+
const accessToken = (_d = normalizedOptions.adsp.accessToken) !== null && _d !== void 0 ? _d : options.accessToken;
|
|
108
|
+
const clientId = `urn:ads:${normalizedOptions.adsp.tenant}:${normalizedOptions.projectName}`;
|
|
109
|
+
yield (0, keycloak_admin_1.ensurePublicClient)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, clientId, accessToken);
|
|
110
|
+
if (options.serviceClientId) {
|
|
111
|
+
yield (0, keycloak_admin_1.ensureAudienceMapper)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, clientId, options.serviceClientId, accessToken);
|
|
112
|
+
yield (0, keycloak_admin_1.ensureClientRoleScope)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm, clientId, options.serviceClientId, 'example-role', accessToken);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (normalizedOptions.adsp && !options.skipAgent) {
|
|
116
|
+
const accessToken = (_e = normalizedOptions.adsp.accessToken) !== null && _e !== void 0 ? _e : options.accessToken;
|
|
117
|
+
const appVue = (_g = (_f = host.read(`${normalizedOptions.projectRoot}/src/App.vue`)) === null || _f === void 0 ? void 0 : _f.toString()) !== null && _g !== void 0 ? _g : '';
|
|
118
|
+
const mainTs = (_j = (_h = host.read(`${normalizedOptions.projectRoot}/src/main.ts`)) === null || _h === void 0 ? void 0 : _h.toString()) !== null && _j !== void 0 ? _j : '';
|
|
119
|
+
const routerTs = (_l = (_k = host.read(`${normalizedOptions.projectRoot}/src/router/index.ts`)) === null || _k === void 0 ? void 0 : _k.toString()) !== null && _l !== void 0 ? _l : '';
|
|
120
|
+
const environmentTs = (_o = (_m = host.read(`${normalizedOptions.projectRoot}/src/environments/environment.ts`)) === null || _m === void 0 ? void 0 : _m.toString()) !== null && _o !== void 0 ? _o : '';
|
|
121
|
+
yield (0, agent_1.confirmAfterAgentInterrupt)(yield (0, agent_1.consultAgent)(normalizedOptions.adsp.directoryServiceUrl, accessToken, {
|
|
122
|
+
projectName: normalizedOptions.projectName,
|
|
123
|
+
projectType: 'vue-app',
|
|
124
|
+
tenant: normalizedOptions.adsp.tenant,
|
|
125
|
+
pluginVersion: plugin_version_1.PLUGIN_VERSION,
|
|
126
|
+
existingFiles: {
|
|
127
|
+
'src/App.vue': appVue,
|
|
128
|
+
'src/main.ts': mainTs,
|
|
129
|
+
'src/router/index.ts': routerTs,
|
|
130
|
+
'src/environments/environment.ts': environmentTs,
|
|
131
|
+
},
|
|
132
|
+
}, host, normalizedOptions.projectRoot));
|
|
133
|
+
}
|
|
134
|
+
yield (0, nx_oc_1.deploymentGenerator)(host, Object.assign(Object.assign({}, normalizedOptions), { appType: 'frontend', project: normalizedOptions.projectName }));
|
|
135
|
+
return () => {
|
|
136
|
+
(0, devkit_1.installPackagesTask)(host);
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=vue-app.js.map
|