create-content-sdk-app 2.3.0 → 2.3.1
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/dist/templates/angular/README.md +119 -4
- package/dist/templates/angular/src/app/components/content-block.component.ts +1 -1
- package/dist/templates/angular/src/app/components/content-sdk/json-ld.ts +1 -1
- package/dist/templates/angular/src/app/components/content-sdk/utils.ts +1 -1
- package/dist/templates/angular/src/app/components/partial-design-dynamic-placeholder.component.ts +1 -1
- package/dist/templates/angular/src/app/components/structured-data.component.ts +1 -1
- package/dist/templates/angular/src/app/pages/error.component.ts +1 -1
- package/dist/templates/angular/src/app/pages/not-found.component.ts +1 -1
- package/dist/templates/angular/src/server.ts +41 -7
- package/dist/templates/nextjs-app-router/sitecore.config.ts +8 -1
- package/dist/templates/nextjs-app-router/src/i18n/routing.ts +3 -2
- package/dist/templates/nextjs-app-router-cache-components/sitecore.config.ts +6 -0
- package/dist/templates/nextjs-app-router-cache-components/src/app/[site]/[locale]/[[...path]]/page.tsx +3 -10
- package/dist/templates/nextjs-app-router-cache-components/src/i18n/routing.ts +3 -2
- package/package.json +6 -6
|
@@ -10,7 +10,6 @@ The functionality available in Content SDK Angular right now includes:
|
|
|
10
10
|
|
|
11
11
|
What can you expect coming in the future ahead of the 1.0 release:
|
|
12
12
|
- SXA Redirects support
|
|
13
|
-
- More varied hosting options
|
|
14
13
|
- Ongoing optimizations and bug fixes
|
|
15
14
|
|
|
16
15
|
## Deployment
|
|
@@ -32,10 +31,126 @@ But, temporarily, we ask you to perform some additional actions post-deploy in y
|
|
|
32
31
|
|
|
33
32
|
These variables are needed to ensure SAI backend infrastructure correctly sets up and uses your Angular sample for Pages editing and preview. They will be switched to be set automatically between now and the 1.0 release.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
## Non-Sitecore AI deployments
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
For both deploying to Netlify and Vercel, ensure your `src/server.ts` file has the default export for the request handler:
|
|
37
|
+
```
|
|
38
|
+
export default reqHandler;
|
|
39
|
+
```
|
|
38
40
|
|
|
39
41
|
### Deploying to Vercel
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Content SDK for Angular sample uses ExpressJS extensively to enhance the base Angular capabilities with Sitecore AI features.
|
|
44
|
+
Since Express is being used, and since Vercel has no Angular SSR support, you need to create a Vercel function file in order for Vercel to correctly serve the application.
|
|
45
|
+
For example, create a `vercel/index.mjs` file and import the Angular's compiled `server.mjs`output:
|
|
46
|
+
```
|
|
47
|
+
export { default } from '../dist/<your_app_name>/server/server.mjs';
|
|
48
|
+
```
|
|
49
|
+
where `<your_app_name>` is the name of application set in `package.json`.
|
|
50
|
+
|
|
51
|
+
After that, configure redirect rules for Vercel request to reach your app code, via `vercel.json` file:
|
|
52
|
+
```
|
|
53
|
+
{
|
|
54
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
55
|
+
"buildCommand": "npm run build",
|
|
56
|
+
"outputDirectory": "dist/<your_app_name>/browser",
|
|
57
|
+
"rewrites": [
|
|
58
|
+
{
|
|
59
|
+
"source": "/(.*)",
|
|
60
|
+
"destination": "/vercel/index"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"functions": {
|
|
64
|
+
"vercel/index.mjs": {
|
|
65
|
+
"includeFiles": "dist/<your_app_name>/**"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. In Vercel portal, set the required Content SDK environment variables. Additionally, set `NG_ALLOWED_HOSTS` environment variable to allow Vercel URLs:
|
|
72
|
+
```
|
|
73
|
+
NG_ALLOWED_HOSTS=*.vercel.app`
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Deploying your app to Vercel should now result in the site being correctly served.
|
|
77
|
+
|
|
78
|
+
### Deploying to Netlify
|
|
79
|
+
|
|
80
|
+
As ExpressJS is being used by Content SDK for Angular, the natural instinct would be to follow the [express guide from Netlify](https://docs.netlify.com/build/frameworks/framework-setup-guides/express/#deploy-your-express-app-with-netlify-cli)
|
|
81
|
+
Unfortunately, this will result in your Netlify functions being compiled as CJS and some built in functionality of Angular (like `import.meta` values) being unavailable.
|
|
82
|
+
In order to build the function with Netlify's latest v2 API and have it compiled as ESM, some hoop jumps are required, using the express deployment guide as basis.
|
|
83
|
+
|
|
84
|
+
1. `serverless-http` module provides the required bridge between Express and Netlify, so you should install it alongside few additional dependencies that your Netlify function will use:
|
|
85
|
+
```
|
|
86
|
+
npm i express serverless-http @netlify/functions @types/express
|
|
87
|
+
```
|
|
88
|
+
2. Create a function implementation file that will import Angular's SSR artifacts and transform them to be used by `serverless-http` and v2 functions. These transformations are needed for Netlify CLI to correctly identify the function as ESM-compatible. The below example is for a TypeScript implementaion. We assume this function is located in the `netlify/functions/server.mts` file:
|
|
89
|
+
```
|
|
90
|
+
import serverless from 'serverless-http';
|
|
91
|
+
import type { Context } from '@netlify/functions';
|
|
92
|
+
import { default as app } from '../../dist/<your_app_name>/server/server.mjs';
|
|
93
|
+
|
|
94
|
+
const handler = serverless(app);
|
|
95
|
+
const FUNCTION_BASE = '/.netlify/functions/server';
|
|
96
|
+
|
|
97
|
+
/** serverless-http's result for a Lambda Function URL ("2.0") event. */
|
|
98
|
+
interface LambdaResult {
|
|
99
|
+
statusCode: number;
|
|
100
|
+
headers: Record<string, string>;
|
|
101
|
+
cookies: string[];
|
|
102
|
+
body: string;
|
|
103
|
+
isBase64Encoded: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default async (request: Request, context: Context) => {
|
|
107
|
+
const url = new URL(request.url);
|
|
108
|
+
// The netlify.toml rewrite passes the original path; direct invocations
|
|
109
|
+
// arrive prefixed with the function base path — strip it.
|
|
110
|
+
const path = url.pathname.startsWith(FUNCTION_BASE)
|
|
111
|
+
? url.pathname.slice(FUNCTION_BASE.length) || '/'
|
|
112
|
+
: url.pathname;
|
|
113
|
+
|
|
114
|
+
// Lambda Function URL ("2.0") event — the leanest shape serverless-http accepts
|
|
115
|
+
const lambdaRequest = {
|
|
116
|
+
version: '2.0',
|
|
117
|
+
rawPath: path,
|
|
118
|
+
rawQueryString: url.searchParams.toString(),
|
|
119
|
+
headers: Object.fromEntries(request.headers),
|
|
120
|
+
requestContext: { http: { method: request.method } },
|
|
121
|
+
body: Buffer.from(await request.arrayBuffer()),
|
|
122
|
+
};
|
|
123
|
+
const result = (await handler(lambdaRequest, context)) as LambdaResult;
|
|
124
|
+
|
|
125
|
+
// set-cookie comes back separately; `|| null` keeps 204/304 responses body-less.
|
|
126
|
+
const headers = new Headers(result.headers);
|
|
127
|
+
for (const cookie of result.cookies) headers.append('set-cookie', cookie);
|
|
128
|
+
return new Response(
|
|
129
|
+
result.isBase64Encoded ? Buffer.from(result.body, 'base64') : result.body || null,
|
|
130
|
+
{ status: result.statusCode, headers }
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
where `<your_app_name>` is the name of application set in `package.json`.
|
|
135
|
+
|
|
136
|
+
3. Finally, create the `netlify.toml` file to correctly configure Netlify in handling request to your function:
|
|
137
|
+
```
|
|
138
|
+
[build]
|
|
139
|
+
command = "npm run build"
|
|
140
|
+
publish = "dist/content-sdk-angular/browser"
|
|
141
|
+
|
|
142
|
+
[functions.server]
|
|
143
|
+
# Ship the full dist folder so express.static and any runtime file access work.
|
|
144
|
+
included_files = ["dist/content-sdk-angular/**"]
|
|
145
|
+
|
|
146
|
+
[[redirects]]
|
|
147
|
+
from = "/*"
|
|
148
|
+
status = 200
|
|
149
|
+
to = "/.netlify/functions/server/:splat"
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
4. In Netlify portal, set the required Content SDK environment variables. Additionally, set `NG_ALLOWED_HOSTS` environment variable to allow netlify URLs:
|
|
154
|
+
```
|
|
155
|
+
NG_ALLOWED_HOSTS=*.netlify.app`
|
|
156
|
+
```
|
|
@@ -4,7 +4,7 @@ import { ScTextDirective, ScRichTextDirective, TextField } from '@sitecore-conte
|
|
|
4
4
|
import { SxaComponent } from './content-sdk/sxa.component';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Minimal content section (no Sitecore chrome wrapper).
|
|
8
8
|
*/
|
|
9
9
|
@Component({
|
|
10
10
|
selector: 'app-content-block',
|
|
@@ -3,7 +3,7 @@ import { computed, type Signal } from '@angular/core';
|
|
|
3
3
|
type LayoutParams = { RenderingIdentifier?: string; Styles?: string };
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* CSS class/id helpers
|
|
6
|
+
* CSS class/id helpers for component wrappers.
|
|
7
7
|
*/
|
|
8
8
|
export function computedRenderingId(
|
|
9
9
|
params: () => { [key: string]: string } | undefined,
|
package/dist/templates/angular/src/app/components/partial-design-dynamic-placeholder.component.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Component, computed, input } from '@angular/core';
|
|
|
2
2
|
import { ComponentRendering, ScPlaceholderComponent } from '@sitecore-content-sdk/angular';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Partial design dynamic placeholder:
|
|
6
6
|
* renders a single placeholder whose name comes from rendering.params.sig.
|
|
7
7
|
*/
|
|
8
8
|
@Component({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component, ElementRef, Renderer2, effect, inject, input } from '@angular/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Emits a JSON-LD script tag
|
|
4
|
+
* Emits a JSON-LD script tag.
|
|
5
5
|
* Uses the DOM API so Angular does not strip script tags from templates.
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
@@ -6,7 +6,7 @@ import { LayoutComponent } from '../shared/layout.component';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 500 error page. Uses the Sitecore server-error page when the loader returns layout data;
|
|
9
|
-
* otherwise shows a static fallback
|
|
9
|
+
* otherwise shows a static fallback.
|
|
10
10
|
*/
|
|
11
11
|
@Component({
|
|
12
12
|
selector: 'app-error',
|
|
@@ -7,7 +7,7 @@ import { LayoutComponent } from '../shared/layout.component';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 404 page. Uses the Sitecore not-found page when the loader returns layout data;
|
|
10
|
-
* otherwise shows a static fallback
|
|
10
|
+
* otherwise shows a static fallback.
|
|
11
11
|
*/
|
|
12
12
|
@Component({
|
|
13
13
|
selector: 'app-404',
|
|
@@ -9,7 +9,7 @@ import express from 'express';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import memoryDriver from 'unstorage/drivers/memory';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
createBotTrackingMiddleware,
|
|
13
13
|
createEditingConfigMiddleware,
|
|
14
14
|
createEditingRenderMiddleware,
|
|
15
15
|
createLoaderCache,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createRobotsMiddleware,
|
|
18
18
|
createMultisiteMiddleware,
|
|
19
19
|
createPersonalizeMiddleware,
|
|
20
|
+
createRedirectsMiddleware,
|
|
20
21
|
createSitecoreRevalidateMiddleware,
|
|
21
22
|
createSitemapMiddleware,
|
|
22
23
|
} from '@sitecore-content-sdk/angular';
|
|
@@ -76,9 +77,6 @@ app.use(
|
|
|
76
77
|
})
|
|
77
78
|
);
|
|
78
79
|
|
|
79
|
-
/** Admin endpoints for cache inspection and invalidation (see `/api/_cache`). */
|
|
80
|
-
app.use(createCacheAdminMiddleware({ cache: loaderCache, endpoint: '/api/_cache' }));
|
|
81
|
-
|
|
82
80
|
/**
|
|
83
81
|
* Editing config endpoint (`/api/editing/config`). Replies with the registered
|
|
84
82
|
* component map keys and `editMode: 'metadata'` so Sitecore Pages can negotiate
|
|
@@ -99,8 +97,8 @@ app.use(
|
|
|
99
97
|
app.use(createEditingRenderMiddleware());
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
* Shared path matcher for the request-scoped middlewares (multisite
|
|
103
|
-
*
|
|
100
|
+
* Shared path matcher for the request-scoped middlewares (multisite, redirects and personalize).
|
|
101
|
+
* It decides which requests these middlewares act on.
|
|
104
102
|
*
|
|
105
103
|
* Patterns are exact strings or RegExp. The SDK already skips API routes (`/api/*`), Sitecore
|
|
106
104
|
* routes (`/sitecore/*`), static files (any path whose last segment has an extension) and
|
|
@@ -129,10 +127,45 @@ app.use(
|
|
|
129
127
|
})
|
|
130
128
|
);
|
|
131
129
|
|
|
130
|
+
/*
|
|
131
|
+
* Bot tracking middleware. Detects bots by User-Agent, sets the `sc_bot` cookie, and sends a
|
|
132
|
+
* dedicated bot page-view event. Must run before personalize so the bot cookie is set before
|
|
133
|
+
* personalize decides whether to skip. Does not run in dev/localhost environments.
|
|
134
|
+
*/
|
|
135
|
+
app.use(
|
|
136
|
+
createBotTrackingMiddleware({
|
|
137
|
+
...config.api.edge,
|
|
138
|
+
locales: config.angular.locales,
|
|
139
|
+
defaultLanguage: config.defaultLanguage,
|
|
140
|
+
defaultSite: config.defaultSite,
|
|
141
|
+
matcher: middlewareMatcher,
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Redirects middleware. Matches each request against the site's Sitecore redirects (locale,
|
|
147
|
+
* static and regex rules) and issues a 301/302 redirect or an internal server-transfer rewrite.
|
|
148
|
+
* Runs after multisite (which resolves the site it fetches redirects for) and before personalize
|
|
149
|
+
* so a redirect short-circuits the request before a CDP call is made.
|
|
150
|
+
*/
|
|
151
|
+
app.use(
|
|
152
|
+
createRedirectsMiddleware({
|
|
153
|
+
...config.redirects,
|
|
154
|
+
...config.api.edge,
|
|
155
|
+
...(config.api.local ?? {}),
|
|
156
|
+
...config.angular,
|
|
157
|
+
sites,
|
|
158
|
+
defaultLanguage: config.defaultLanguage,
|
|
159
|
+
defaultSite: config.defaultSite,
|
|
160
|
+
matcher: middlewareMatcher,
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
132
164
|
/**
|
|
133
165
|
* Personalize middleware. Identifies page/component variants for the request via
|
|
134
166
|
* Sitecore CDP and writes them onto `req.scParams` so the page loader fetches the
|
|
135
|
-
* personalized layout and the loader cache keys per variant.
|
|
167
|
+
* personalized layout and the loader cache keys per variant. Skips bot requests marked
|
|
168
|
+
* by the bot tracking middleware (`skipForBot`, default true).
|
|
136
169
|
*
|
|
137
170
|
* NOTE: Personalize requires Edge configuration (contextId/clientContextId) and
|
|
138
171
|
* cannot work with local containers
|
|
@@ -198,3 +231,4 @@ if (isMainModule(import.meta.url)) {
|
|
|
198
231
|
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
|
|
199
232
|
*/
|
|
200
233
|
export const reqHandler = createNodeRequestHandler(app);
|
|
234
|
+
export default reqHandler;
|
|
@@ -4,4 +4,11 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config';
|
|
|
4
4
|
* See the documentation for `defineConfig`:
|
|
5
5
|
* https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html
|
|
6
6
|
*/
|
|
7
|
-
export default defineConfig({
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
redirects: {
|
|
9
|
+
// Locale prefix strategy for App Router redirect targets, matching next-intl's
|
|
10
|
+
// `localePrefix` in src/i18n/routing.ts. `as-needed` keeps the default locale bare
|
|
11
|
+
// and only prefixes non-default locales. Use `always` if routing prefixes every locale.
|
|
12
|
+
appLocalePrefix: 'as-needed',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -8,8 +8,9 @@ export const routing = defineRouting({
|
|
|
8
8
|
// Used when no locale matches
|
|
9
9
|
defaultLocale: sitecoreConfig.defaultLanguage,
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Out of the box, syncs with the `appLocalePrefix` redirects setting for consistent behavior
|
|
12
|
+
// For "as-needed" value, no prefix is added for the default locale .
|
|
12
13
|
// For other configuration options, refer to the next-intl documentation:
|
|
13
14
|
// https://next-intl.dev/docs/routing/configuration
|
|
14
|
-
localePrefix: 'as-needed',
|
|
15
|
+
localePrefix: sitecoreConfig.redirects?.appLocalePrefix || 'as-needed',
|
|
15
16
|
});
|
|
@@ -14,4 +14,10 @@ export default defineConfig({
|
|
|
14
14
|
enabled: false,
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
|
+
redirects: {
|
|
18
|
+
// Locale prefix strategy for App Router redirect targets, matching next-intl's
|
|
19
|
+
// `localePrefix` in src/i18n/routing.ts. `as-needed` keeps the default locale bare
|
|
20
|
+
// and only prefixes non-default locales. Use `always` if routing prefixes every locale.
|
|
21
|
+
appLocalePrefix: 'as-needed',
|
|
22
|
+
},
|
|
17
23
|
});
|
|
@@ -29,20 +29,13 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|
|
29
29
|
notFound();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// Cached fetch first so missing routes can notFound() without dynamic APIs in the ancestor tree.
|
|
33
|
-
const cachedPage = await getSitecorePage({ site, locale, path: path ?? [] });
|
|
34
|
-
|
|
35
|
-
if (!cachedPage) {
|
|
36
|
-
setCachedPageParams({ site, locale });
|
|
37
|
-
notFound();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
// Set site and locale to be available in src/i18n/request.ts for fetching the dictionary
|
|
41
33
|
setRequestLocale(`${site}_${locale}`);
|
|
42
34
|
|
|
35
|
+
// Draft/preview first so editing is not blocked by locale-dependent cached lookups.
|
|
36
|
+
// Editing often resolves language via query string, while [locale] may fall back to defaultLanguage.
|
|
43
37
|
const draft = await draftMode();
|
|
44
38
|
|
|
45
|
-
// Fetch the page data from Sitecore
|
|
46
39
|
let page;
|
|
47
40
|
if (draft.isEnabled) {
|
|
48
41
|
const editingParams = await searchParams;
|
|
@@ -52,7 +45,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|
|
52
45
|
page = await client.getPreview(editingParams);
|
|
53
46
|
}
|
|
54
47
|
} else {
|
|
55
|
-
page =
|
|
48
|
+
page = await getSitecorePage({ site, locale, path: path ?? [] });
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
// If the page is not found, return a 404
|
|
@@ -8,8 +8,9 @@ export const routing = defineRouting({
|
|
|
8
8
|
// Used when no locale matches
|
|
9
9
|
defaultLocale: sitecoreConfig.defaultLanguage,
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Out of the box, syncs with the `appLocalePrefix` redirects setting for consistent behavior
|
|
12
|
+
// For "as-needed" value, no prefix is added for the default locale .
|
|
12
13
|
// For other configuration options, refer to the next-intl documentation:
|
|
13
14
|
// https://next-intl.dev/docs/routing/configuration
|
|
14
|
-
localePrefix: 'as-needed',
|
|
15
|
+
localePrefix: sitecoreConfig.redirects?.appLocalePrefix || 'as-needed',
|
|
15
16
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-content-sdk-app",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Sitecore Content SDK initializer",
|
|
5
5
|
"bin": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"minimist": "^1.2.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@sitecore-content-sdk/analytics-core": "^2.1.
|
|
42
|
-
"@sitecore-content-sdk/angular": "~0.
|
|
43
|
-
"@sitecore-content-sdk/cli": "^2.2.
|
|
44
|
-
"@sitecore-content-sdk/events": "^2.1.
|
|
45
|
-
"@sitecore-content-sdk/nextjs": "^2.
|
|
41
|
+
"@sitecore-content-sdk/analytics-core": "^2.1.2",
|
|
42
|
+
"@sitecore-content-sdk/angular": "~0.2.0",
|
|
43
|
+
"@sitecore-content-sdk/cli": "^2.2.1",
|
|
44
|
+
"@sitecore-content-sdk/events": "^2.1.2",
|
|
45
|
+
"@sitecore-content-sdk/nextjs": "^2.3.0",
|
|
46
46
|
"@sitecore-content-sdk/personalize": "^2.1.0",
|
|
47
47
|
"@stylistic/eslint-plugin": "^5.2.2",
|
|
48
48
|
"@types/chai": "^5.2.2",
|