@orkestrel/scaffold 0.0.81 → 0.0.82

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.
Files changed (35) hide show
  1. package/dist/bin/main.js +167 -13
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/AGENTS.md +5 -3
  4. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +2 -2
  5. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +41 -38
  6. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +3 -3
  7. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +3 -3
  8. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +5 -5
  9. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +3 -3
  10. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +9 -9
  11. package/dist/host/claude/rules/application.md +20 -6
  12. package/dist/host/claude/rules/architecture.md +2 -2
  13. package/dist/host/claude/rules/browser.md +9 -0
  14. package/dist/host/claude/rules/documentation.md +2 -1
  15. package/dist/host/claude/rules/styles.md +35 -12
  16. package/dist/host/claude/rules/tests.md +15 -10
  17. package/dist/host/claude/rules/workspace.md +128 -98
  18. package/dist/host/claude/skills/orkestrel-journey/SKILL.md +1 -1
  19. package/dist/host/configs/helpers.ts +157 -9
  20. package/dist/host/configs/policy.ts +64 -61
  21. package/dist/host/dotfiles/oxlintrc.json +132 -16
  22. package/dist/host/dotfiles/prettierignore +1 -1
  23. package/dist/host/guides/README.md +9 -5
  24. package/dist/host/guides/scaffold.md +475 -115
  25. package/dist/host/manifest.json +26 -26
  26. package/dist/host/tests/config.test.ts +996 -39
  27. package/dist/host/tests/policy.test.ts +11 -0
  28. package/dist/host/tests/setupPolicy.ts +396 -22
  29. package/dist/src/core/index.cjs +1261 -159
  30. package/dist/src/core/index.cjs.map +1 -1
  31. package/dist/src/core/index.d.cts +297 -35
  32. package/dist/src/core/index.d.ts +297 -35
  33. package/dist/src/core/index.js +1241 -160
  34. package/dist/src/core/index.js.map +1 -1
  35. package/package.json +1 -1
@@ -2,6 +2,7 @@ import type { Plugin, Rolldown } from 'vite'
2
2
  import { parseSync, transformWithOxc, Visitor } from 'vite'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { spawnSync } from 'node:child_process'
5
+ import { createHash } from 'node:crypto'
5
6
  import { createRequire, isBuiltin } from 'node:module'
6
7
  import { tmpdir } from 'node:os'
7
8
  import {
@@ -55,6 +56,69 @@ export const WORKSPACE_ROOT = realpathSync.native(
55
56
  resolvePath(dirname(fileURLToPath(import.meta.url)), '..'),
56
57
  )
57
58
 
59
+ /**
60
+ * Resolves a Vite mode to a declared application.
61
+ * @param mode - The requested mode; undefined, development, production, and test select browser.
62
+ * @param applications - The declared application factories.
63
+ * @returns The selected application name.
64
+ * @throws Thrown when the selected application is not declared.
65
+ * @example
66
+ * ```ts
67
+ * resolveApplication('test', { browser: true }) // 'browser'
68
+ * ```
69
+ */
70
+ export function resolveApplication<Application extends string>(
71
+ mode: string | undefined,
72
+ applications: Readonly<Record<Application, unknown>>,
73
+ ): Application {
74
+ const selected =
75
+ mode === undefined || mode === 'development' || mode === 'production' || mode === 'test'
76
+ ? 'browser'
77
+ : mode
78
+ for (const name in applications) {
79
+ if (Object.hasOwn(applications, name) && name === selected) return name
80
+ }
81
+ throw new Error(`The application mode "${selected}" is not declared.`)
82
+ }
83
+
84
+ /**
85
+ * Computes the SHA-256 digest of a page with its build stamp line removed.
86
+ * @param text - The page text, with or without its stamp line.
87
+ * @returns The lowercase hexadecimal digest.
88
+ * @example
89
+ * ```ts
90
+ * computeStamp('') // 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
91
+ * computeStamp('abc') // 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
92
+ * ```
93
+ */
94
+ export function computeStamp(text: string): string {
95
+ return createHash('sha256')
96
+ .update(text.replace(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu, ''))
97
+ .digest('hex')
98
+ }
99
+
100
+ /**
101
+ * Stamps a final inlined page with its content digest on a separate line.
102
+ * @param html - The final page with a head closing on its own line.
103
+ * @returns The page with exactly one build stamp line.
104
+ * @throws Thrown when a stamp is malformed or repeated, or the head closing line is absent.
105
+ * @example
106
+ * ```ts
107
+ * computeStamp(stampPage('abc\n</head>\n')) === computeStamp('abc\n</head>\n') // true
108
+ * ```
109
+ */
110
+ export function stampPage(html: string): string {
111
+ const stamps = html.match(/<meta\b[^>]*\bname=["']build-id["'][^>]*>/gu) ?? []
112
+ const lines = html.match(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu) ?? []
113
+ if (stamps.length > 1 || stamps.length !== lines.length) {
114
+ throw new Error('A showcase page must carry at most one well-formed build stamp line.')
115
+ }
116
+ const text = html.replace(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu, '')
117
+ const close = /^[\t ]*<\/head>[\t ]*(?:\r?\n|$)/mu.exec(text)
118
+ if (close === null) throw new Error('A showcase page must close its head on its own line.')
119
+ return `${text.slice(0, close.index)}\t\t<meta name="build-id" content="${computeStamp(text)}" />\n${text.slice(close.index)}`
120
+ }
121
+
58
122
  export function fileSystemPath(pathname: string): string {
59
123
  if (!pathname.startsWith('/@fs/')) return pathname
60
124
  const candidate = pathname.slice('/@fs/'.length)
@@ -132,7 +196,7 @@ export function isWorkspaceBoundaryModule(id: string): boolean {
132
196
  } catch {
133
197
  return false
134
198
  }
135
- const rootRelative = /^\/(?:app|src)\/(?:core|browser|server)\//.test(candidate)
199
+ const rootRelative = /^\/(?:app|src)\/(?:core|browser|server|vue)\//.test(candidate)
136
200
  const absoluteCandidate = rootRelative
137
201
  ? resolvePath(WORKSPACE_ROOT, candidate.slice(1))
138
202
  : isAbsolute(candidate)
@@ -143,7 +207,7 @@ export function isWorkspaceBoundaryModule(id: string): boolean {
143
207
  relativeId !== '..' &&
144
208
  !relativeId.startsWith('../') &&
145
209
  !isAbsolute(relativeId) &&
146
- /^(?:app|src)\/(?:core|browser|server)\//.test(relativeId)
210
+ /^(?:app|src)\/(?:core|browser|server|vue)\//.test(relativeId)
147
211
  )
148
212
  }
149
213
 
@@ -312,9 +376,62 @@ export function isStylesheetPath(path: string): boolean {
312
376
  return /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:[?#]|$)/.test(path)
313
377
  }
314
378
 
379
+ /**
380
+ * Defines the manifest peers, refused packages, and published sibling entries for a build.
381
+ * @example Refuse framework imports until a peer is declared
382
+ * ```ts
383
+ * const options: ExternalOptions = { peers: [], refused: ['vue', '@vue/'], siblings: [] }
384
+ * ```
385
+ */
386
+ export interface ExternalOptions {
387
+ readonly peers: readonly string[]
388
+ readonly refused: readonly string[]
389
+ readonly siblings: readonly string[]
390
+ }
391
+
392
+ /**
393
+ * Resolves whether a build leaves a module external under its manifest and face boundaries.
394
+ * @param id - The module identifier supplied by the bundler.
395
+ * @param options - The peer names, refused package names or scope prefixes, and sibling entries.
396
+ * @returns True if the module stays external; false if the build bundles it.
397
+ * @throws Thrown when an import names a refused scope or a refused package without an admitting peer.
398
+ * @remarks
399
+ * A peer admits its own subpaths. A refused scope remains refused even when a package inside it
400
+ * is a peer. Node builtins and fleet packages retain their published identities. Workspace aliases
401
+ * resolve before a sibling entry can be recognized.
402
+ * @example Externalize a declared peer
403
+ * ```ts
404
+ * resolveExternal('vue/runtime-dom', { peers: ['vue'], refused: ['vue', '@vue/'], siblings: [] })
405
+ * // true
406
+ * ```
407
+ */
408
+ export function resolveExternal(id: string, options: ExternalOptions): boolean {
409
+ const peer = options.peers.some((name) => id === name || id.startsWith(`${name}/`))
410
+ const refused = options.refused.find((name) =>
411
+ name.endsWith('/') ? id.startsWith(name) : id === name || id.startsWith(`${name}/`),
412
+ )
413
+ if (refused?.endsWith('/')) {
414
+ throw new Error(
415
+ `[orkestrel-build] The import ${id} is refused; import from ${refused.slice(1, -1)} instead of the ${refused} implementation scope.`,
416
+ )
417
+ }
418
+ if (refused !== undefined && !peer) {
419
+ throw new Error(
420
+ `[orkestrel-build] The import ${id} is refused; declare its public package in peerDependencies with peerDependenciesMeta marking it optional.`,
421
+ )
422
+ }
423
+ if (id.startsWith('@src/') || id.startsWith('@app/')) return false
424
+ return (
425
+ id.startsWith('node:') ||
426
+ id.startsWith('@orkestrel/') ||
427
+ peer ||
428
+ options.siblings.some((sibling) => sibling.replaceAll('\\', '/') === id.replaceAll('\\', '/'))
429
+ )
430
+ }
431
+
315
432
  export function environmentPathError(owner: string, target: string): string | undefined {
316
433
  const targetApplication = target.startsWith('app/')
317
- const targetBrowser = target.startsWith('app/browser/') || target.startsWith('src/browser/')
434
+ const targetBrowser = /^(?:app|src)\/(?:browser|vue)\//.test(target)
318
435
  const targetServer = target.startsWith('app/server/') || target.startsWith('src/server/')
319
436
  const stylesheet = isStylesheetPath(target)
320
437
  if (owner.startsWith('src/') && targetApplication) {
@@ -323,7 +440,7 @@ export function environmentPathError(owner: string, target: string): string | un
323
440
  if (owner.endsWith('/core') && (stylesheet || targetBrowser || targetServer)) {
324
441
  return 'Core modules must remain host-independent'
325
442
  }
326
- if (owner.endsWith('/browser') && targetServer) {
443
+ if ((owner.endsWith('/browser') || owner.endsWith('/vue')) && targetServer) {
327
444
  return 'Browser modules cannot depend on Node or server-only modules'
328
445
  }
329
446
  if (owner.endsWith('/server') && (stylesheet || targetBrowser)) {
@@ -346,7 +463,7 @@ export function environmentSourceError(owner: string, source: string): string |
346
463
  !/^file:/i.test(sourcePath) &&
347
464
  !/^[A-Za-z]:\//.test(sourcePath)
348
465
  const browserPackage =
349
- /^(?:(?:vue|vite)(?:[/?#]|$)|@(?:vue|vitejs)\/|@(?:app|src)\/browser(?:[/?#]|$)|@orkestrel\/[^/]+\/browser(?:[/?#]|$))/.test(
466
+ /^(?:(?:vue|vite)(?:[/?#]|$)|@(?:vue|vitejs)\/|@(?:app|src)\/(?:browser|vue)(?:[/?#]|$)|@orkestrel\/[^/]+\/(?:browser|vue)(?:[/?#]|$))/.test(
350
467
  normalizedSource,
351
468
  )
352
469
  const serverPackage =
@@ -361,7 +478,7 @@ export function environmentSourceError(owner: string, source: string): string |
361
478
  if (owner.endsWith('/core') && (builtin || browserPackage || serverPackage || stylesheet)) {
362
479
  return 'Core modules must remain host-independent'
363
480
  }
364
- if (owner.endsWith('/browser') && (builtin || serverPackage)) {
481
+ if ((owner.endsWith('/browser') || owner.endsWith('/vue')) && (builtin || serverPackage)) {
365
482
  return 'Browser modules cannot depend on Node or server-only modules'
366
483
  }
367
484
  if (owner.endsWith('/server') && (browserPackage || stylesheet)) {
@@ -623,6 +740,26 @@ export function rewriteCoreSpecifier(content: string): string {
623
740
  return content.replaceAll(/(?:\.\.\/)+core\/index\.[jt]s/g, name).replaceAll('@src/core', name)
624
741
  }
625
742
 
743
+ /**
744
+ * Rewrites browser specifiers in declarations to the workspace package's browser export.
745
+ * @param content - The finished declaration roll-up.
746
+ * @returns The roll-up with browser aliases and relative entry paths replaced.
747
+ * @throws Thrown when the workspace manifest names no package.
748
+ * @example Rewrite a browser alias
749
+ * ```ts
750
+ * rewriteBrowserSpecifier("from '@src/browser'")
751
+ * ```
752
+ */
753
+ export function rewriteBrowserSpecifier(content: string): string {
754
+ const name = packageManifestName(WORKSPACE_ROOT)
755
+ if (name === undefined) {
756
+ throw new Error('[orkestrel-declaration-rollup] The workspace manifest names no package')
757
+ }
758
+ return content
759
+ .replaceAll(/(?:\.\.\/)+browser\/index\.[jt]s/g, `${name}/browser`)
760
+ .replaceAll('@src/browser', `${name}/browser`)
761
+ }
762
+
626
763
  /**
627
764
  * Rolls one published face's declarations into the single file that face ships.
628
765
  *
@@ -843,7 +980,15 @@ export async function environmentAssetSources(
843
980
  }
844
981
 
845
982
  export function environmentBoundary(
846
- owner: 'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server',
983
+ owner:
984
+ | 'src/core'
985
+ | 'src/browser'
986
+ | 'src/server'
987
+ | 'src/vue'
988
+ | 'app/core'
989
+ | 'app/browser'
990
+ | 'app/server'
991
+ | 'app/vue',
847
992
  ): Plugin {
848
993
  const trustedPackageRoots = new Set<string>()
849
994
  let environmentRoot = WORKSPACE_ROOT
@@ -867,7 +1012,10 @@ export function environmentBoundary(
867
1012
  if (
868
1013
  importerPackageRoot === undefined &&
869
1014
  ((layer !== 'app' && layer !== 'src') ||
870
- (environment !== 'core' && environment !== 'browser' && environment !== 'server'))
1015
+ (environment !== 'core' &&
1016
+ environment !== 'browser' &&
1017
+ environment !== 'server' &&
1018
+ environment !== 'vue'))
871
1019
  ) {
872
1020
  return null
873
1021
  }
@@ -1037,7 +1185,7 @@ export function environmentBoundary(
1037
1185
  if (pathError !== undefined) this.error(pathError)
1038
1186
  }
1039
1187
  const environmentModule =
1040
- target !== undefined && /^(?:app|src)\/(?:core|browser|server)\//.test(target)
1188
+ target !== undefined && /^(?:app|src)\/(?:core|browser|server|vue)\//.test(target)
1041
1189
  if (!environmentModule && importerPackageRoot === undefined) return null
1042
1190
  for (const source of await environmentAssetSources(code, id)) {
1043
1191
  const normalizedSource = source.replaceAll('\\', '/')
@@ -20,6 +20,7 @@ export interface PolicyExpression extends PolicyNode {
20
20
  readonly callee?: PolicyExpression
21
21
  readonly argument?: PolicyExpression | null
22
22
  readonly arguments?: readonly PolicyExpression[]
23
+ readonly elements?: ReadonlyArray<PolicyExpression | null>
23
24
  readonly body?: PolicyExpression | readonly PolicyExpression[]
24
25
  readonly quasis?: readonly PolicyExpression[]
25
26
  readonly expressions?: readonly PolicyExpression[]
@@ -514,39 +515,65 @@ export function isPolicyDeclaredFunction(node: PolicyExpression): boolean {
514
515
  return node.type === 'FunctionDeclaration' || node.type === 'TSDeclareFunction'
515
516
  }
516
517
 
517
- /** Reports whether a policy function is anonymous. */
518
- export function isPolicyAnonymous(node: PolicyExpression): boolean {
519
- return node.type === 'ArrowFunctionExpression' || node.id === null
520
- }
521
-
522
- /** Returns the outermost parenthesized expression holding a policy function. */
518
+ /** Returns a policy function's position through parentheses, object values, and array elements. */
523
519
  export function functionToPolicyPosition(node: PolicyExpression): PolicyExpression {
524
520
  let position = node
525
- while (position.parent?.type === 'ParenthesizedExpression') {
526
- position = position.parent
521
+ while (position.parent !== undefined && position.parent !== null) {
522
+ const parent = position.parent
523
+ if (parent.type === 'ParenthesizedExpression') {
524
+ position = parent
525
+ continue
526
+ }
527
+ if (
528
+ parent.type === 'Property' &&
529
+ parent.kind === 'init' &&
530
+ parent.method === false &&
531
+ parent.computed !== true &&
532
+ parent.value === position &&
533
+ parent.parent?.type === 'ObjectExpression'
534
+ ) {
535
+ position = parent.parent
536
+ continue
537
+ }
538
+ if (parent.type === 'ArrayExpression' && parent.elements?.includes(position) === true) {
539
+ position = parent
540
+ continue
541
+ }
542
+ break
527
543
  }
528
544
  return position
529
545
  }
530
546
 
531
- /** Reports whether a policy function is an anonymous callback passed directly as an argument. */
547
+ /** Reports whether a literal position is admitted as an argument, return, or arrow body. */
548
+ export function isPolicyPosition(position: PolicyExpression): boolean {
549
+ const parent = position.parent
550
+ return (
551
+ ((parent?.type === 'CallExpression' || parent?.type === 'NewExpression') &&
552
+ parent.arguments?.includes(position) === true) ||
553
+ (parent?.type === 'ReturnStatement' && parent.argument === position) ||
554
+ (parent?.type === 'ArrowFunctionExpression' && parent.body === position)
555
+ )
556
+ }
557
+
558
+ /** Reports whether a function expression is passed as an argument through its literal position. */
532
559
  export function isPolicyCallback(node: PolicyExpression): boolean {
533
- if (!isPolicyAnonymous(node)) return false
560
+ if (node.type !== 'ArrowFunctionExpression' && node.type !== 'FunctionExpression') return false
534
561
  const position = functionToPolicyPosition(node)
535
562
  const parent = position.parent
536
563
  return (
537
564
  (parent?.type === 'CallExpression' || parent?.type === 'NewExpression') &&
538
- parent.arguments?.includes(position) === true
565
+ isPolicyPosition(position)
539
566
  )
540
567
  }
541
568
 
542
- /** Reports whether a policy function is an anonymous function returned directly as a result. */
569
+ /** Reports whether a function expression is returned as a result through its literal position. */
543
570
  export function isPolicyResult(node: PolicyExpression): boolean {
544
- if (!isPolicyAnonymous(node)) return false
571
+ if (node.type !== 'ArrowFunctionExpression' && node.type !== 'FunctionExpression') return false
545
572
  const position = functionToPolicyPosition(node)
546
573
  const parent = position.parent
547
574
  return (
548
- (parent?.type === 'ReturnStatement' && parent.argument === position) ||
549
- (parent?.type === 'ArrowFunctionExpression' && parent.body === position)
575
+ (parent?.type === 'ReturnStatement' || parent?.type === 'ArrowFunctionExpression') &&
576
+ isPolicyPosition(position)
550
577
  )
551
578
  }
552
579
 
@@ -581,42 +608,6 @@ export function hasPolicyFunctionAncestor(node: PolicyExpression): boolean {
581
608
  return method
582
609
  }
583
610
 
584
- /** Reports whether an arrow is the policy plugin's sanctioned visitor-table delegation. */
585
- export function isPolicyVisitor(node: PolicyExpression): boolean {
586
- const body = expressionToPolicyBody(node)
587
- if (
588
- node.type !== 'ArrowFunctionExpression' ||
589
- node.expression !== true ||
590
- body?.type !== 'CallExpression' ||
591
- body.callee?.type !== 'Identifier' ||
592
- typeof body.callee.name !== 'string' ||
593
- !body.callee.name.startsWith('report')
594
- ) {
595
- return false
596
- }
597
- const property = node.parent
598
- const object = property?.parent
599
- const returned = object?.parent
600
- const block = returned?.parent
601
- const create = block?.parent
602
- const definition = create?.parent
603
- return (
604
- property?.type === 'Property' &&
605
- property.method === false &&
606
- property.value === node &&
607
- object?.type === 'ObjectExpression' &&
608
- returned?.type === 'ReturnStatement' &&
609
- returned.argument === object &&
610
- block?.type === 'BlockStatement' &&
611
- create?.type === 'FunctionExpression' &&
612
- definition?.type === 'Property' &&
613
- definition.method === true &&
614
- definition.value === create &&
615
- definition.key?.type === 'Identifier' &&
616
- definition.key.name === 'create'
617
- )
618
- }
619
-
620
611
  /**
621
612
  * Returns the module-scope statement that owns a policy function, or `undefined` when none does.
622
613
  *
@@ -849,15 +840,17 @@ export function reportComments(context: PolicyContext): void {
849
840
 
850
841
  /** Reports function syntax nested inside another function body. */
851
842
  export function reportNested(context: PolicyContext, node: PolicyExpression): void {
852
- if (
853
- !hasPolicyFunctionAncestor(node) ||
854
- isPolicyMethod(node) ||
855
- isPolicyCallback(node) ||
856
- isPolicyResult(node) ||
857
- isPolicyVisitor(node)
858
- ) {
843
+ if (!hasPolicyFunctionAncestor(node) || isPolicyCallback(node) || isPolicyResult(node)) {
859
844
  return
860
845
  }
846
+ if (isPolicyMethod(node)) {
847
+ if (node.parent?.type === 'MethodDefinition') return
848
+ const object = node.parent?.parent
849
+ if (object?.type === 'ObjectExpression') {
850
+ const position = functionToPolicyPosition(object)
851
+ if (isPolicyPosition(position)) return
852
+ }
853
+ }
861
854
  context.report({ node, messageId: 'nested' })
862
855
  }
863
856
 
@@ -969,7 +962,16 @@ export function reportFunction(context: PolicyContext, node: PolicyExpression):
969
962
  return
970
963
  }
971
964
  if (region.type !== 'VariableDeclaration') return
972
- if (isPolicyCallback(node) || isPolicyResult(node)) return
965
+ let position = node
966
+ while (position.parent?.type === 'ParenthesizedExpression') position = position.parent
967
+ // Module placement admits direct callbacks; a literal member still belongs in a function file.
968
+ if (
969
+ (node.type === 'ArrowFunctionExpression' || node.id === null) &&
970
+ functionToPolicyPosition(node) === position &&
971
+ (isPolicyCallback(node) || isPolicyResult(node))
972
+ ) {
973
+ return
974
+ }
973
975
  context.report({ node, messageId: 'function' })
974
976
  }
975
977
 
@@ -1059,11 +1061,12 @@ export const NESTED_RULE: PolicyRuleInterface = {
1059
1061
  meta: {
1060
1062
  type: 'problem',
1061
1063
  docs: {
1062
- description: 'Disallow function declarations and assignments inside another function body.',
1064
+ description:
1065
+ 'Disallow nested functions except callbacks passed as arguments or returned as results, directly or as members of object or array literals in those positions.',
1063
1066
  },
1064
1067
  messages: {
1065
1068
  nested:
1066
- 'Extract the function to module scope or make instance-bound work a method; only direct anonymous callbacks and returned anonymous functions may stay in a function body.',
1069
+ 'Extract the function to module scope or make instance-bound work a method; only a callback passed as an argument or returned as the result, directly or as a member of an object or array literal in that position, may stay in a function body.',
1067
1070
  },
1068
1071
  },
1069
1072
  create(context) {