@backstage/create-app 0.4.11 → 0.4.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,588 @@
1
1
  # @backstage/create-app
2
2
 
3
+ ## 0.4.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 01b27d547c: Added three additional required properties to the search-backend `createRouter` function to support filtering search results based on permissions. To make this change to an existing app, add the required parameters to the `createRouter` call in `packages/backend/src/plugins/search.ts`:
8
+
9
+ ```diff
10
+ export default async function createPlugin({
11
+ logger,
12
+ + permissions,
13
+ discovery,
14
+ config,
15
+ tokenManager,
16
+ }: PluginEnvironment) {
17
+ /* ... */
18
+
19
+ return await createRouter({
20
+ engine: indexBuilder.getSearchEngine(),
21
+ + types: indexBuilder.getDocumentTypes(),
22
+ + permissions,
23
+ + config,
24
+ logger,
25
+ });
26
+ }
27
+ ```
28
+
29
+ - a0d446c8ec: Replaced EntitySystemDiagramCard with EntityCatalogGraphCard
30
+
31
+ To make this change to an existing app:
32
+
33
+ Add `@backstage/catalog-graph-plugin` as a `dependency` in `packages/app/package.json`
34
+
35
+ Apply the following changes to the `packages/app/src/components/catalog/EntityPage.tsx` file:
36
+
37
+ ```diff
38
+ + import {
39
+ + Direction,
40
+ + EntityCatalogGraphCard,
41
+ + } from '@backstage/plugin-catalog-graph';
42
+ + import {
43
+ + RELATION_API_CONSUMED_BY,
44
+ + RELATION_API_PROVIDED_BY,
45
+ + RELATION_CONSUMES_API,
46
+ + RELATION_DEPENDENCY_OF,
47
+ + RELATION_DEPENDS_ON,
48
+ + RELATION_HAS_PART,
49
+ + RELATION_PART_OF,
50
+ + RELATION_PROVIDES_API,
51
+ + } from '@backstage/catalog-model';
52
+ ```
53
+
54
+ ```diff
55
+ <EntityLayout.Route path="/diagram" title="Diagram">
56
+ - <EntitySystemDiagramCard />
57
+ + <EntityCatalogGraphCard
58
+ + variant="gridItem"
59
+ + direction={Direction.TOP_BOTTOM}
60
+ + title="System Diagram"
61
+ + height={700}
62
+ + relations={[
63
+ + RELATION_PART_OF,
64
+ + RELATION_HAS_PART,
65
+ + RELATION_API_CONSUMED_BY,
66
+ + RELATION_API_PROVIDED_BY,
67
+ + RELATION_CONSUMES_API,
68
+ + RELATION_PROVIDES_API,
69
+ + RELATION_DEPENDENCY_OF,
70
+ + RELATION_DEPENDS_ON,
71
+ + ]}
72
+ + unidirectional={false}
73
+ + />
74
+ </EntityLayout.Route>
75
+ ```
76
+
77
+ ```diff
78
+ const cicdContent = (
79
+ <Grid item md={6}>
80
+ <EntityAboutCard variant="gridItem" />
81
+ </Grid>
82
+ + <Grid item md={6} xs={12}>
83
+ + <EntityCatalogGraphCard variant="gridItem" height={400} />
84
+ + </Grid>
85
+ ```
86
+
87
+ Add the above component in `overviewContent`, `apiPage` , `systemPage` and domainPage` as well.
88
+
89
+ - 4aca2a5307: An example instance of a `<SearchFilter.Select />` with asynchronously loaded values was added to the composed `SearchPage.tsx`, allowing searches bound to the `techdocs` type to be filtered by entity name.
90
+
91
+ This is an entirely optional change; if you wish to adopt it, you can make the following (or similar) changes to your search page layout:
92
+
93
+ ```diff
94
+ --- a/packages/app/src/components/search/SearchPage.tsx
95
+ +++ b/packages/app/src/components/search/SearchPage.tsx
96
+ @@ -2,6 +2,10 @@ import React from 'react';
97
+ import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core';
98
+
99
+ import { CatalogResultListItem } from '@backstage/plugin-catalog';
100
+ +import {
101
+ + catalogApiRef,
102
+ + CATALOG_FILTER_EXISTS,
103
+ +} from '@backstage/plugin-catalog-react';
104
+ import { DocsResultListItem } from '@backstage/plugin-techdocs';
105
+
106
+ import {
107
+ @@ -10,6 +14,7 @@ import {
108
+ SearchResult,
109
+ SearchType,
110
+ DefaultResultListItem,
111
+ + useSearch,
112
+ } from '@backstage/plugin-search';
113
+ import {
114
+ CatalogIcon,
115
+ @@ -18,6 +23,7 @@ import {
116
+ Header,
117
+ Page,
118
+ } from '@backstage/core-components';
119
+ +import { useApi } from '@backstage/core-plugin-api';
120
+
121
+ const useStyles = makeStyles((theme: Theme) => ({
122
+ bar: {
123
+ @@ -36,6 +42,8 @@ const useStyles = makeStyles((theme: Theme) => ({
124
+
125
+ const SearchPage = () => {
126
+ const classes = useStyles();
127
+ + const { types } = useSearch();
128
+ + const catalogApi = useApi(catalogApiRef);
129
+
130
+ return (
131
+ <Page themeId="home">
132
+ @@ -65,6 +73,27 @@ const SearchPage = () => {
133
+ ]}
134
+ />
135
+ <Paper className={classes.filters}>
136
+ + {types.includes('techdocs') && (
137
+ + <SearchFilter.Select
138
+ + className={classes.filter}
139
+ + label="Entity"
140
+ + name="name"
141
+ + values={async () => {
142
+ + // Return a list of entities which are documented.
143
+ + const { items } = await catalogApi.getEntities({
144
+ + fields: ['metadata.name'],
145
+ + filter: {
146
+ + 'metadata.annotations.backstage.io/techdocs-ref':
147
+ + CATALOG_FILTER_EXISTS,
148
+ + },
149
+ + });
150
+ +
151
+ + const names = items.map(entity => entity.metadata.name);
152
+ + names.sort();
153
+ + return names;
154
+ + }}
155
+ + />
156
+ + )}
157
+ <SearchFilter.Select
158
+ className={classes.filter}
159
+ name="kind"
160
+ ```
161
+
162
+ - 1dbe63ec39: A `label` prop was added to `<SearchFilter.* />` components in order to allow
163
+ user-friendly label strings (as well as the option to omit a label). In order
164
+ to maintain labels on your existing filters, add a `label` prop to them in your
165
+ `SearchPage.tsx`.
166
+
167
+ ```diff
168
+ --- a/packages/app/src/components/search/SearchPage.tsx
169
+ +++ b/packages/app/src/components/search/SearchPage.tsx
170
+ @@ -96,11 +96,13 @@ const SearchPage = () => {
171
+ )}
172
+ <SearchFilter.Select
173
+ className={classes.filter}
174
+ + label="Kind"
175
+ name="kind"
176
+ values={['Component', 'Template']}
177
+ />
178
+ <SearchFilter.Checkbox
179
+ className={classes.filter}
180
+ + label="Lifecycle"
181
+ name="lifecycle"
182
+ values={['experimental', 'production']}
183
+ />
184
+ ```
185
+
186
+ ## 0.4.14
187
+
188
+ ### Patch Changes
189
+
190
+ - d4941024bc: Rebind external route for catalog import plugin from `scaffolderPlugin.routes.root` to `catalogImportPlugin.routes.importPage`.
191
+
192
+ To make this change to an existing app, make the following change to `packages/app/src/App.tsx`
193
+
194
+ ```diff
195
+ const App = createApp({
196
+ ...
197
+ bindRoutes({ bind }) {
198
+ ...
199
+ bind(apiDocsPlugin.externalRoutes, {
200
+ - createComponent: scaffolderPlugin.routes.root,
201
+ + registerApi: catalogImportPlugin.routes.importPage,
202
+ });
203
+ ...
204
+ },
205
+ });
206
+ ```
207
+
208
+ - b5402d6d72: Migrated the app template to React 17.
209
+
210
+ To apply this change to an existing app, make sure you have updated to the latest version of `@backstage/cli`, and make the following change to `packages/app/package.json`:
211
+
212
+ ```diff
213
+ "history": "^5.0.0",
214
+ - "react": "^16.13.1",
215
+ - "react-dom": "^16.13.1",
216
+ + "react": "^17.0.2",
217
+ + "react-dom": "^17.0.2",
218
+ "react-router": "6.0.0-beta.0",
219
+ ```
220
+
221
+ Since we have recently moved over all `react` and `react-dom` dependencies to `peerDependencies` of all packages, and included React 17 in the version range, this should be all you need to do. If you end up with duplicate React installations, first make sure that all of your plugins are up-to-date, including ones for example from `@roadiehq`. If that doesn't work, you may need to fall back to adding [Yarn resolutions](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) in the `package.json` of your project root:
222
+
223
+ ```diff
224
+ + "resolutions": {
225
+ + "react": "^17.0.2",
226
+ + "react-dom": "^17.0.2"
227
+ + },
228
+ ```
229
+
230
+ - 5e8d278f8e: Added an external route binding from the `org` plugin to the catalog index page.
231
+
232
+ This change is needed because `@backstage/plugin-org` now has a required external route that needs to be bound for the app to start.
233
+
234
+ To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`:
235
+
236
+ ```diff
237
+ import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
238
+ +import { orgPlugin } from '@backstage/plugin-org';
239
+ import { SearchPage } from '@backstage/plugin-search';
240
+ ```
241
+
242
+ And further down within the `createApp` call:
243
+
244
+ ```diff
245
+ bind(scaffolderPlugin.externalRoutes, {
246
+ registerComponent: catalogImportPlugin.routes.importPage,
247
+ });
248
+ + bind(orgPlugin.externalRoutes, {
249
+ + catalogIndex: catalogPlugin.routes.catalogIndex,
250
+ + });
251
+ },
252
+ ```
253
+
254
+ - fb08e2f285: Updated the configuration of the `app-backend` plugin to enable the static asset store by passing on `database` from the plugin environment to `createRouter`.
255
+
256
+ To apply this change to an existing app, make the following change to `packages/backend/src/plugins/app.ts`:
257
+
258
+ ```diff
259
+ export default async function createPlugin({
260
+ logger,
261
+ config,
262
+ + database,
263
+ }: PluginEnvironment): Promise<Router> {
264
+ return await createRouter({
265
+ logger,
266
+ config,
267
+ + database,
268
+ appPackageName: 'app',
269
+ });
270
+ }
271
+ ```
272
+
273
+ - 7ba416be78: You can now add `SidebarGroup`s to the current `Sidebar`. This will not affect how the current sidebar is displayed, but allows a customization on how the `MobileSidebar` on smaller screens will look like. A `SidebarGroup` will be displayed with the given icon in the `MobileSidebar`.
274
+
275
+ A `SidebarGroup` can either link to an existing page (e.g. `/search` or `/settings`) or wrap components, which will be displayed in a full-screen overlay menu (e.g. `Menu`).
276
+
277
+ ```diff
278
+ <Sidebar>
279
+ <SidebarLogo />
280
+ + <SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
281
+ <SidebarSearchModal />
282
+ + </SidebarGroup>
283
+ <SidebarDivider />
284
+ + <SidebarGroup label="Menu" icon={<MenuIcon />}>
285
+ <SidebarItem icon={HomeIcon} to="catalog" text="Home" />
286
+ <SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
287
+ <SidebarDivider />
288
+ <SidebarScrollWrapper>
289
+ <SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
290
+ </SidebarScrollWrapper>
291
+ + </SidebarGroup>
292
+ <SidebarSpace />
293
+ <SidebarDivider />
294
+ + <SidebarGroup
295
+ + label="Settings"
296
+ + icon={<UserSettingsSignInAvatar />}
297
+ + to="/settings"
298
+ + >
299
+ <SidebarSettings />
300
+ + </SidebarGroup>
301
+ </Sidebar>
302
+ ```
303
+
304
+ Additionally, you can order the groups differently in the `MobileSidebar` than in the usual `Sidebar` simply by giving a group a priority. The groups will be displayed in descending order from left to right.
305
+
306
+ ```diff
307
+ <SidebarGroup
308
+ label="Settings"
309
+ icon={<UserSettingsSignInAvatar />}
310
+ to="/settings"
311
+ + priority={1}
312
+ >
313
+ <SidebarSettings />
314
+ </SidebarGroup>
315
+ ```
316
+
317
+ If you decide against adding `SidebarGroup`s to your `Sidebar` the `MobileSidebar` will contain one default menu item, which will open a full-screen overlay menu displaying all the content of the current `Sidebar`.
318
+
319
+ More information on the `SidebarGroup` & the `MobileSidebar` component can be found in the changeset for the `core-components`.
320
+
321
+ - 08fa6a604a: The app template has been updated to add an explicit dependency on `typescript` in the root `package.json`. This is because it was removed as a dependency of `@backstage/cli` in order to decouple the TypeScript versioning in Backstage projects.
322
+
323
+ To apply this change in an existing app, add a `typescript` dependency to your `package.json` in the project root:
324
+
325
+ ```json
326
+ "dependencies": {
327
+ ...
328
+ "typescript": "~4.5.4",
329
+ }
330
+ ```
331
+
332
+ We recommend using a `~` version range since TypeScript releases do not adhere to semver.
333
+
334
+ It may be the case that you end up with errors if you upgrade the TypeScript version. This is because there was a change to TypeScript not long ago that defaulted the type of errors caught in `catch` blocks to `unknown`. You can work around this by adding `"useUnknownInCatchVariables": false` to the `"compilerOptions"` in your `tsconfig.json`:
335
+
336
+ ```json
337
+ "compilerOptions": {
338
+ ...
339
+ "useUnknownInCatchVariables": false
340
+ }
341
+ ```
342
+
343
+ Another option is to use the utilities from `@backstage/errors` to assert the type of errors caught in `catch` blocks:
344
+
345
+ ```ts
346
+ import { assertError, isError } from '@backstage/errors';
347
+
348
+ try {
349
+ ...
350
+ } catch (error) {
351
+ assertError(error);
352
+ ...
353
+ // OR
354
+ if (isError(error)) {
355
+ ...
356
+ }
357
+ }
358
+ ```
359
+
360
+ Yet another issue you might run into when upgrading TypeScript is incompatibilities in the types from `react-use`. The error you would run into looks something like this:
361
+
362
+ ```plain
363
+ node_modules/react-use/lib/usePermission.d.ts:1:54 - error TS2304: Cannot find name 'DevicePermissionDescriptor'.
364
+
365
+ 1 declare type PermissionDesc = PermissionDescriptor | DevicePermissionDescriptor | MidiPermissionDescriptor | PushPermissionDescriptor;
366
+ ```
367
+
368
+ If you encounter this error, the simplest fix is to replace full imports of `react-use` with more specific ones. For example, the following:
369
+
370
+ ```ts
371
+ import { useAsync } from 'react-use';
372
+ ```
373
+
374
+ Would be converted into this:
375
+
376
+ ```ts
377
+ import useAsync from 'react-use/lib/useAsync';
378
+ ```
379
+
380
+ ## 0.4.13
381
+
382
+ ### Patch Changes
383
+
384
+ - fb08e2f285: Updated the configuration of the `app-backend` plugin to enable the static asset store by passing on `database` from the plugin environment to `createRouter`.
385
+
386
+ To apply this change to an existing app, make the following change to `packages/backend/src/plugins/app.ts`:
387
+
388
+ ```diff
389
+ export default async function createPlugin({
390
+ logger,
391
+ config,
392
+ + database,
393
+ }: PluginEnvironment): Promise<Router> {
394
+ return await createRouter({
395
+ logger,
396
+ config,
397
+ + database,
398
+ appPackageName: 'app',
399
+ });
400
+ }
401
+ ```
402
+
403
+ - 7ba416be78: You can now add `SidebarGroup`s to the current `Sidebar`. This will not affect how the current sidebar is displayed, but allows a customization on how the `MobileSidebar` on smaller screens will look like. A `SidebarGroup` will be displayed with the given icon in the `MobileSidebar`.
404
+
405
+ A `SidebarGroup` can either link to an existing page (e.g. `/search` or `/settings`) or wrap components, which will be displayed in a full-screen overlay menu (e.g. `Menu`).
406
+
407
+ ```diff
408
+ <Sidebar>
409
+ <SidebarLogo />
410
+ + <SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
411
+ <SidebarSearchModal />
412
+ + </SidebarGroup>
413
+ <SidebarDivider />
414
+ + <SidebarGroup label="Menu" icon={<MenuIcon />}>
415
+ <SidebarItem icon={HomeIcon} to="catalog" text="Home" />
416
+ <SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
417
+ <SidebarDivider />
418
+ <SidebarScrollWrapper>
419
+ <SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
420
+ </SidebarScrollWrapper>
421
+ + </SidebarGroup>
422
+ <SidebarSpace />
423
+ <SidebarDivider />
424
+ + <SidebarGroup
425
+ + label="Settings"
426
+ + icon={<UserSettingsSignInAvatar />}
427
+ + to="/settings"
428
+ + >
429
+ <SidebarSettings />
430
+ + </SidebarGroup>
431
+ </Sidebar>
432
+ ```
433
+
434
+ Additionally, you can order the groups differently in the `MobileSidebar` than in the usual `Sidebar` simply by giving a group a priority. The groups will be displayed in descending order from left to right.
435
+
436
+ ```diff
437
+ <SidebarGroup
438
+ label="Settings"
439
+ icon={<UserSettingsSignInAvatar />}
440
+ to="/settings"
441
+ + priority={1}
442
+ >
443
+ <SidebarSettings />
444
+ </SidebarGroup>
445
+ ```
446
+
447
+ If you decide against adding `SidebarGroup`s to your `Sidebar` the `MobileSidebar` will contain one default menu item, which will open a full-screen overlay menu displaying all the content of the current `Sidebar`.
448
+
449
+ More information on the `SidebarGroup` & the `MobileSidebar` component can be found in the changeset for the `core-components`.
450
+
451
+ - 08fa6a604a: The app template has been updated to add an explicit dependency on `typescript` in the root `package.json`. This is because it was removed as a dependency of `@backstage/cli` in order to decouple the TypeScript versioning in Backstage projects.
452
+
453
+ To apply this change in an existing app, add a `typescript` dependency to your `package.json` in the project root:
454
+
455
+ ```json
456
+ "dependencies": {
457
+ ...
458
+ "typescript": "~4.5.4",
459
+ }
460
+ ```
461
+
462
+ We recommend using a `~` version range since TypeScript releases do not adhere to semver.
463
+
464
+ It may be the case that you end up with errors if you upgrade the TypeScript version. This is because there was a change to TypeScript not long ago that defaulted the type of errors caught in `catch` blocks to `unknown`. You can work around this by adding `"useUnknownInCatchVariables": false` to the `"compilerOptions"` in your `tsconfig.json`:
465
+
466
+ ```json
467
+ "compilerOptions": {
468
+ ...
469
+ "useUnknownInCatchVariables": false
470
+ }
471
+ ```
472
+
473
+ Another option is to use the utilities from `@backstage/errors` to assert the type of errors caught in `catch` blocks:
474
+
475
+ ```ts
476
+ import { assertError, isError } from '@backstage/errors';
477
+
478
+ try {
479
+ ...
480
+ } catch (error) {
481
+ assertError(error);
482
+ ...
483
+ // OR
484
+ if (isError(error)) {
485
+ ...
486
+ }
487
+ }
488
+ ```
489
+
490
+ - Updated dependencies
491
+ - @backstage/plugin-tech-radar@0.5.3-next.0
492
+ - @backstage/plugin-auth-backend@0.7.0-next.0
493
+ - @backstage/core-components@0.8.5-next.0
494
+ - @backstage/plugin-api-docs@0.6.23-next.0
495
+ - @backstage/plugin-catalog-backend@0.21.0-next.0
496
+ - @backstage/plugin-permission-common@0.4.0-next.0
497
+ - @backstage/cli@0.12.0-next.0
498
+ - @backstage/core-plugin-api@0.6.0-next.0
499
+ - @backstage/plugin-catalog@0.7.9-next.0
500
+ - @backstage/plugin-user-settings@0.3.17-next.0
501
+ - @backstage/backend-common@0.10.4-next.0
502
+ - @backstage/config@0.1.13-next.0
503
+ - @backstage/plugin-app-backend@0.3.22-next.0
504
+ - @backstage/core-app-api@0.5.0-next.0
505
+ - @backstage/plugin-catalog-import@0.7.10-next.0
506
+ - @backstage/plugin-scaffolder@0.11.19-next.0
507
+ - @backstage/plugin-search@0.5.6-next.0
508
+ - @backstage/plugin-techdocs@0.12.15-next.0
509
+ - @backstage/plugin-permission-node@0.4.0-next.0
510
+ - @backstage/catalog-model@0.9.10-next.0
511
+ - @backstage/integration-react@0.1.19-next.0
512
+ - @backstage/plugin-explore@0.3.26-next.0
513
+ - @backstage/plugin-github-actions@0.4.32-next.0
514
+ - @backstage/plugin-lighthouse@0.2.35-next.0
515
+ - @backstage/plugin-scaffolder-backend@0.15.21-next.0
516
+ - @backstage/backend-tasks@0.1.4-next.0
517
+ - @backstage/catalog-client@0.5.5-next.0
518
+ - @backstage/test-utils@0.2.3-next.0
519
+ - @backstage/plugin-proxy-backend@0.2.16-next.0
520
+ - @backstage/plugin-rollbar-backend@0.1.19-next.0
521
+ - @backstage/plugin-search-backend@0.3.1-next.0
522
+ - @backstage/plugin-techdocs-backend@0.12.4-next.0
523
+
524
+ ## 0.4.12
525
+
526
+ ### Patch Changes
527
+
528
+ - 5333451def: Cleaned up API exports
529
+ - cd529c4094: Add permissions to create-app's PluginEnvironment
530
+
531
+ `CatalogEnvironment` now has a `permissions` field, which means that a permission client must now be provided as part of `PluginEnvironment`. To apply these changes to an existing app, add the following to the `makeCreateEnv` function in `packages/backend/src/index.ts`:
532
+
533
+ ```diff
534
+ // packages/backend/src/index.ts
535
+
536
+ + import { ServerPermissionClient } from '@backstage/plugin-permission-node';
537
+
538
+ function makeCreateEnv(config: Config) {
539
+ ...
540
+ + const permissions = ServerPermissionClient.fromConfig(config, {
541
+ + discovery,
542
+ + tokenManager,
543
+ + });
544
+
545
+ root.info(`Created UrlReader ${reader}`);
546
+
547
+ return (plugin: string): PluginEnvironment => {
548
+ ...
549
+ return {
550
+ logger,
551
+ cache,
552
+ database,
553
+ config,
554
+ reader,
555
+ discovery,
556
+ tokenManager,
557
+ scheduler,
558
+ + permissions,
559
+ };
560
+ }
561
+ }
562
+ ```
563
+
564
+ And add a permissions field to the `PluginEnvironment` type in `packages/backend/src/types.ts`:
565
+
566
+ ```diff
567
+ // packages/backend/src/types.ts
568
+
569
+ + import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
570
+
571
+ export type PluginEnvironment = {
572
+ ...
573
+ + permissions: PermissionAuthorizer;
574
+ };
575
+ ```
576
+
577
+ [`@backstage/plugin-permission-common`](https://www.npmjs.com/package/@backstage/plugin-permission-common) and [`@backstage/plugin-permission-node`](https://www.npmjs.com/package/@backstage/plugin-permission-node) will need to be installed as dependencies:
578
+
579
+ ```diff
580
+ // packages/backend/package.json
581
+
582
+ + "@backstage/plugin-permission-common": "...",
583
+ + "@backstage/plugin-permission-node": "...",
584
+ ```
585
+
3
586
  ## 0.4.11
4
587
 
5
588
  ## 0.4.10