@things-factory/shell 10.1.40 → 10.1.44

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.
@@ -0,0 +1,15 @@
1
+ export type AfterCommitStep = {
2
+ name: string;
3
+ run: () => unknown | Promise<unknown>;
4
+ };
5
+ export type AfterCommit = AfterCommitStep[];
6
+ /** Adds a step. With no list (outside a transaction) it returns false, and the caller does it at once. */
7
+ export declare function whenCommitted(afterCommit: AfterCommit | undefined, name: string, run: AfterCommitStep['run']): boolean;
8
+ /** Runs the steps in order. A failing one is reported by name and the rest still run. */
9
+ export declare function runAfterCommit(afterCommit: AfterCommit, report: (message: string, error: unknown) => void): Promise<void>;
10
+ /**
11
+ * Opens the transaction, hands the body the list, and runs the list once committed. The directive and
12
+ * any code that opens a transaction by hand share it. transact has the shape of dataSource.transaction:
13
+ * if the body throws, it rolls back and throws.
14
+ */
15
+ export declare function runTransaction<T, TX>(transact: (work: (tx: TX) => Promise<T>) => Promise<T>, body: (tx: TX, afterCommit: AfterCommit) => Promise<T>, report: (message: string, error: unknown) => void): Promise<T>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /*
3
+ * What to do after the commit - a list @transaction hangs on the request it opens (ruling 2026-09-24).
4
+ *
5
+ * -- Why --------------------------------------------------------------------
6
+ * Something that cannot be undone, such as deleting a file from storage, done inside the transaction
7
+ * outlives a commit that then fails: the rows come back and the file does not. An attachment row is
8
+ * left pointing at nothing.
9
+ *
10
+ * -- Shape ------------------------------------------------------------------
11
+ * The directive puts an empty list on context.state.afterCommit. Code inside does not do the
12
+ * irreversible thing at once; it adds it to the list with a name. The moment the transaction returns
13
+ * its value is the moment after the commit, and the list is run then, in order. On a rollback the
14
+ * transaction throws, the list is never reached, and it is dropped.
15
+ *
16
+ * -- A failure afterwards does not fail the request -------------------------
17
+ * The commit has happened; there is nothing to roll back. A failing step is logged with its name, so
18
+ * what was left behind can be found, and the next step runs. One orphaned file is less harm than an
19
+ * attachment row with no file.
20
+ *
21
+ * -- Where there is no list -------------------------------------------------
22
+ * Outside the directive - a call with no transaction - there is no list and nothing to roll back, so
23
+ * the caller does it at once. A transaction opened by hand (getDataSource().transaction(...) called
24
+ * directly) has no list either, and the same hole remains there; such code gets the list by opening
25
+ * its transaction with runTransaction.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.whenCommitted = whenCommitted;
29
+ exports.runAfterCommit = runAfterCommit;
30
+ exports.runTransaction = runTransaction;
31
+ /** Adds a step. With no list (outside a transaction) it returns false, and the caller does it at once. */
32
+ function whenCommitted(afterCommit, name, run) {
33
+ if (!Array.isArray(afterCommit))
34
+ return false;
35
+ afterCommit.push({ name, run });
36
+ return true;
37
+ }
38
+ /** Runs the steps in order. A failing one is reported by name and the rest still run. */
39
+ async function runAfterCommit(afterCommit, report) {
40
+ for (const step of afterCommit) {
41
+ try {
42
+ await step.run();
43
+ }
44
+ catch (error) {
45
+ report(`after commit, "${step.name}" failed - left behind`, error);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Opens the transaction, hands the body the list, and runs the list once committed. The directive and
51
+ * any code that opens a transaction by hand share it. transact has the shape of dataSource.transaction:
52
+ * if the body throws, it rolls back and throws.
53
+ */
54
+ async function runTransaction(transact, body, report) {
55
+ const afterCommit = [];
56
+ const result = await transact(tx => body(tx, afterCommit));
57
+ await runAfterCommit(afterCommit, report);
58
+ return result;
59
+ }
60
+ //# sourceMappingURL=after-commit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"after-commit.js","sourceRoot":"","sources":["../../server/utils/after-commit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;AAMH,sCAIC;AAGD,wCAWC;AAOD,wCAWC;AArCD,0GAA0G;AAC1G,SAAgB,aAAa,CAAC,WAAoC,EAAE,IAAY,EAAE,GAA2B;IAC3G,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAA;IAC7C,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;IAC/B,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yFAAyF;AAClF,KAAK,UAAU,cAAc,CAClC,WAAwB,EACxB,MAAiD;IAEjD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAA;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,kBAAkB,IAAI,CAAC,IAAI,wBAAwB,EAAE,KAAK,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,cAAc,CAClC,QAAsD,EACtD,IAAsD,EACtD,MAAiD;IAEjD,MAAM,WAAW,GAAgB,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAA;IAE1D,MAAM,cAAc,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;IAEzC,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/*\n * What to do after the commit - a list @transaction hangs on the request it opens (ruling 2026-09-24).\n *\n * -- Why --------------------------------------------------------------------\n * Something that cannot be undone, such as deleting a file from storage, done inside the transaction\n * outlives a commit that then fails: the rows come back and the file does not. An attachment row is\n * left pointing at nothing.\n *\n * -- Shape ------------------------------------------------------------------\n * The directive puts an empty list on context.state.afterCommit. Code inside does not do the\n * irreversible thing at once; it adds it to the list with a name. The moment the transaction returns\n * its value is the moment after the commit, and the list is run then, in order. On a rollback the\n * transaction throws, the list is never reached, and it is dropped.\n *\n * -- A failure afterwards does not fail the request -------------------------\n * The commit has happened; there is nothing to roll back. A failing step is logged with its name, so\n * what was left behind can be found, and the next step runs. One orphaned file is less harm than an\n * attachment row with no file.\n *\n * -- Where there is no list -------------------------------------------------\n * Outside the directive - a call with no transaction - there is no list and nothing to roll back, so\n * the caller does it at once. A transaction opened by hand (getDataSource().transaction(...) called\n * directly) has no list either, and the same hole remains there; such code gets the list by opening\n * its transaction with runTransaction.\n */\n\nexport type AfterCommitStep = { name: string; run: () => unknown | Promise<unknown> }\nexport type AfterCommit = AfterCommitStep[]\n\n/** Adds a step. With no list (outside a transaction) it returns false, and the caller does it at once. */\nexport function whenCommitted(afterCommit: AfterCommit | undefined, name: string, run: AfterCommitStep['run']): boolean {\n if (!Array.isArray(afterCommit)) return false\n afterCommit.push({ name, run })\n return true\n}\n\n/** Runs the steps in order. A failing one is reported by name and the rest still run. */\nexport async function runAfterCommit(\n afterCommit: AfterCommit,\n report: (message: string, error: unknown) => void\n): Promise<void> {\n for (const step of afterCommit) {\n try {\n await step.run()\n } catch (error) {\n report(`after commit, \"${step.name}\" failed - left behind`, error)\n }\n }\n}\n\n/**\n * Opens the transaction, hands the body the list, and runs the list once committed. The directive and\n * any code that opens a transaction by hand share it. transact has the shape of dataSource.transaction:\n * if the body throws, it rolls back and throws.\n */\nexport async function runTransaction<T, TX>(\n transact: (work: (tx: TX) => Promise<T>) => Promise<T>,\n body: (tx: TX, afterCommit: AfterCommit) => Promise<T>,\n report: (message: string, error: unknown) => void\n): Promise<T> {\n const afterCommit: AfterCommit = []\n const result = await transact(tx => body(tx, afterCommit))\n\n await runAfterCommit(afterCommit, report)\n\n return result\n}\n"]}
@@ -10,3 +10,4 @@ export * from './list-param-adjuster.js';
10
10
  export * from './get-times-for-period.js';
11
11
  export * from './headless-pool/index.js';
12
12
  export * from './subscribe-auth.js';
13
+ export * from './after-commit.js';
@@ -13,4 +13,5 @@ tslib_1.__exportStar(require("./list-param-adjuster.js"), exports);
13
13
  tslib_1.__exportStar(require("./get-times-for-period.js"), exports);
14
14
  tslib_1.__exportStar(require("./headless-pool/index.js"), exports);
15
15
  tslib_1.__exportStar(require("./subscribe-auth.js"), exports);
16
+ tslib_1.__exportStar(require("./after-commit.js"), exports);
16
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/utils/index.ts"],"names":[],"mappings":";;;AAAA,0DAA+B;AAC/B,mEAAwC;AACxC,iEAAsC;AACtC,kEAAuC;AACvC,qEAA0C;AAC1C,gEAAqC;AACrC,kFAAuD;AACvD,6DAAkC;AAClC,mEAAwC;AACxC,oEAAyC;AACzC,mEAAwC;AACxC,8DAAmC","sourcesContent":["export * from './get-domain.js'\nexport * from './apply-domain-filter.js'\nexport * from './condition-builder.js'\nexport * from './list-query-builder.js'\nexport * from './list-params-converter.js'\nexport * from './publish-progress.js'\nexport * from './get-query-builder-from-list-params.js'\nexport * from './keyset-cursor.js'\nexport * from './list-param-adjuster.js'\nexport * from './get-times-for-period.js'\nexport * from './headless-pool/index.js'\nexport * from './subscribe-auth.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/utils/index.ts"],"names":[],"mappings":";;;AAAA,0DAA+B;AAC/B,mEAAwC;AACxC,iEAAsC;AACtC,kEAAuC;AACvC,qEAA0C;AAC1C,gEAAqC;AACrC,kFAAuD;AACvD,6DAAkC;AAClC,mEAAwC;AACxC,oEAAyC;AACzC,mEAAwC;AACxC,8DAAmC;AACnC,4DAAiC","sourcesContent":["export * from './get-domain.js'\nexport * from './apply-domain-filter.js'\nexport * from './condition-builder.js'\nexport * from './list-query-builder.js'\nexport * from './list-params-converter.js'\nexport * from './publish-progress.js'\nexport * from './get-query-builder-from-list-params.js'\nexport * from './keyset-cursor.js'\nexport * from './list-param-adjuster.js'\nexport * from './get-times-for-period.js'\nexport * from './headless-pool/index.js'\nexport * from './subscribe-auth.js'\nexport * from './after-commit.js'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.1.40",
3
+ "version": "10.1.44",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -126,5 +126,5 @@
126
126
  "pg": "^8.7.3",
127
127
  "sqlite3": "^5.0.8"
128
128
  },
129
- "gitHead": "51c5d804c14ad8fcc6d4707e6ef80c9deede2766"
129
+ "gitHead": "7c95c4e75e155e51c714409e865d96f317232c23"
130
130
  }
@@ -0,0 +1,200 @@
1
+ import { graphql } from 'graphql'
2
+ import gql from 'graphql-tag'
3
+ import { makeExecutableSchema } from '@graphql-tools/schema'
4
+
5
+ /**
6
+ * What waits for the commit (ADR-0091 decision 8).
7
+ *
8
+ * What this splits: after the commit against before it, a rollback against a commit, one failing step
9
+ * against the rest, and a call with a list against one without. The directive is driven through a real
10
+ * schema with the data source replaced by one that records when it commits.
11
+ */
12
+
13
+ const mockEvents: string[] = []
14
+ let mockFailCommit = false
15
+
16
+ jest.mock('../server/initializers/database', () => ({
17
+ getDataSource: () => ({
18
+ transaction: async (work: (tx: any) => Promise<any>) => {
19
+ const result = await work('tx')
20
+ if (mockFailCommit) throw new Error('commit failed')
21
+ mockEvents.push('commit')
22
+ return result
23
+ }
24
+ })
25
+ }))
26
+
27
+ jest.mock('@things-factory/env', () => ({ logger: { error: (message: string) => mockEvents.push(`logged: ${message}`) } }))
28
+
29
+ import { runAfterCommit, runTransaction, whenCommitted } from '../server/utils/after-commit'
30
+ import { transactionDirectiveResolver, transactionDirectiveTypeDefs } from '../server/service/directive-transaction/transaction'
31
+
32
+ beforeEach(() => {
33
+ mockEvents.length = 0
34
+ mockFailCommit = false
35
+ })
36
+
37
+ describe('the list', () => {
38
+ it('without a list there is nothing to wait for: the caller is told to do it now', () => {
39
+ expect(whenCommitted(undefined, 'x', () => {})).toBe(false)
40
+ })
41
+
42
+ it('with a list the step is added, not run', () => {
43
+ const list: any[] = []
44
+ let ran = false
45
+
46
+ expect(whenCommitted(list, 'x', () => (ran = true))).toBe(true)
47
+ expect(list.map(one => one.name)).toEqual(['x'])
48
+ expect(ran).toBe(false)
49
+ })
50
+
51
+ it('steps run in order, and a failing one is reported by name while the rest still run', async () => {
52
+ const reported: string[] = []
53
+
54
+ await runAfterCommit(
55
+ [
56
+ { name: 'first', run: () => mockEvents.push('first') },
57
+ { name: 'breaks', run: () => Promise.reject(new Error('storage down')) },
58
+ { name: 'third', run: () => mockEvents.push('third') }
59
+ ],
60
+ message => reported.push(message)
61
+ )
62
+
63
+ expect(mockEvents).toEqual(['first', 'third'])
64
+ expect(reported).toEqual(['after commit, "breaks" failed - left behind'])
65
+ })
66
+
67
+ it('an empty list does nothing', async () => {
68
+ await runAfterCommit([], () => mockEvents.push('reported'))
69
+ expect(mockEvents).toEqual([])
70
+ })
71
+ })
72
+
73
+ describe('runTransaction', () => {
74
+ const transact = async (work: (tx: any) => Promise<any>) => {
75
+ const result = await work('tx')
76
+ if (mockFailCommit) throw new Error('commit failed')
77
+ mockEvents.push('commit')
78
+ return result
79
+ }
80
+
81
+ it('runs the list after the commit, not before', async () => {
82
+ await runTransaction(
83
+ transact,
84
+ async (_tx, afterCommit) => {
85
+ whenCommitted(afterCommit, 'file', () => mockEvents.push('file'))
86
+ mockEvents.push('body')
87
+ return 1
88
+ },
89
+ () => {}
90
+ )
91
+
92
+ expect(mockEvents).toEqual(['body', 'commit', 'file'])
93
+ })
94
+
95
+ it('a body that throws rolls back and the list is dropped', async () => {
96
+ await expect(
97
+ runTransaction(
98
+ transact,
99
+ async (_tx, afterCommit) => {
100
+ whenCommitted(afterCommit, 'file', () => mockEvents.push('file'))
101
+ throw new Error('refused')
102
+ },
103
+ () => {}
104
+ )
105
+ ).rejects.toThrow('refused')
106
+
107
+ expect(mockEvents).not.toContain('file')
108
+ })
109
+
110
+ it('a commit that fails drops the list too', async () => {
111
+ mockFailCommit = true
112
+
113
+ await expect(
114
+ runTransaction(
115
+ transact,
116
+ async (_tx, afterCommit) => {
117
+ whenCommitted(afterCommit, 'file', () => mockEvents.push('file'))
118
+ return 1
119
+ },
120
+ () => {}
121
+ )
122
+ ).rejects.toThrow('commit failed')
123
+
124
+ expect(mockEvents).not.toContain('file')
125
+ })
126
+
127
+ it('a step that fails after the commit does not fail the request', async () => {
128
+ const result = await runTransaction(
129
+ transact,
130
+ async (_tx, afterCommit) => {
131
+ whenCommitted(afterCommit, 'file', () => Promise.reject(new Error('storage down')))
132
+ return 'done'
133
+ },
134
+ message => mockEvents.push(`reported: ${message}`)
135
+ )
136
+
137
+ expect(result).toBe('done')
138
+ expect(mockEvents).toEqual(['commit', 'reported: after commit, "file" failed - left behind'])
139
+ })
140
+ })
141
+
142
+ describe('the @transaction directive', () => {
143
+ function schemaWith(resolve: (context: any) => any) {
144
+ const schema = makeExecutableSchema({
145
+ typeDefs: [
146
+ transactionDirectiveTypeDefs,
147
+ gql`
148
+ type Query {
149
+ ok: Boolean
150
+ }
151
+ type Mutation {
152
+ save: String @transaction
153
+ }
154
+ `
155
+ ],
156
+ resolvers: { Mutation: { save: (_: any, __: any, context: any) => resolve(context) } }
157
+ })
158
+
159
+ return transactionDirectiveResolver(schema)
160
+ }
161
+
162
+ const context = () => ({ state: {}, t: (k: string) => k, app: { createContext: () => ({}) } })
163
+
164
+ it('hands the resolver a list and runs it after the commit', async () => {
165
+ const schema = schemaWith(context => {
166
+ whenCommitted(context.state.afterCommit, 'file', () => mockEvents.push('file'))
167
+ mockEvents.push('resolved')
168
+ return 'saved'
169
+ })
170
+
171
+ const result = await graphql({ schema, source: 'mutation { save }', contextValue: context() })
172
+
173
+ expect(result.errors).toBeUndefined()
174
+ expect(mockEvents).toEqual(['resolved', 'commit', 'file'])
175
+ })
176
+
177
+ it('a resolver that throws leaves the list unrun', async () => {
178
+ const schema = schemaWith(context => {
179
+ whenCommitted(context.state.afterCommit, 'file', () => mockEvents.push('file'))
180
+ throw new Error('refused')
181
+ })
182
+
183
+ const result = await graphql({ schema, source: 'mutation { save }', contextValue: context() })
184
+
185
+ expect(result.errors?.[0]?.message).toBe('refused')
186
+ expect(mockEvents).not.toContain('file')
187
+ })
188
+
189
+ it('a failing step after the commit is logged with the field it came from, and the mutation succeeds', async () => {
190
+ const schema = schemaWith(context => {
191
+ whenCommitted(context.state.afterCommit, 'file', () => Promise.reject(new Error('storage down')))
192
+ return 'saved'
193
+ })
194
+
195
+ const result = await graphql({ schema, source: 'mutation { save }', contextValue: context() })
196
+
197
+ expect(result.data?.save).toBe('saved')
198
+ expect(mockEvents).toEqual(['commit', 'logged: after commit, "file" failed - left behind (field save)'])
199
+ })
200
+ })
@@ -0,0 +1,133 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * A custom property read with no fallback must be defined somewhere.
6
+ *
7
+ * `var(--x)` with nothing defining `--x` is not an error anywhere: the declaration becomes invalid at
8
+ * computed-value time and the property falls back to its initial value. A hover colour turns into no
9
+ * hover, a padding into none, and every test stays green. Measured 2026-09-24 over operato's
10
+ * components and this repository's clients: 28 such reads, most of them typos - `--spacing-smail`,
11
+ * `--pading-default`, `--md-sys-on-color-secondary`, `--md-sys-color-on-s` - and Material names that
12
+ * do not exist (`--md-sys-color-primary-container-high`, `--md-sys-shape-corner-tiny`).
13
+ *
14
+ * Two scopes: this repository's clients from source, and the operato components as installed, since
15
+ * that is what an app runs. What defines a name: any declaration in this repository, in the installed
16
+ * @operato and @material/web packages, or a `setProperty('--x')`.
17
+ *
18
+ * ADR-0060 decision 1 (the machine catches a token nobody defines). The token inventory that found
19
+ * these is recorded in the ADR's status line.
20
+ */
21
+
22
+ const ROOT = path.resolve(__dirname, '../../..')
23
+ const PACKAGES = path.join(ROOT, 'packages')
24
+ const MODULES = path.join(ROOT, 'node_modules')
25
+
26
+ function* files(dir: string, exts: RegExp): Generator<string> {
27
+ let entries: string[]
28
+ try {
29
+ entries = readdirSync(dir)
30
+ } catch {
31
+ return
32
+ }
33
+ for (const entry of entries) {
34
+ if (entry === 'node_modules' || entry === '.git' || entry === 'coverage') continue
35
+ const full = path.join(dir, entry)
36
+ let stat
37
+ try {
38
+ stat = statSync(full)
39
+ } catch {
40
+ continue
41
+ }
42
+ if (stat.isDirectory()) yield* files(full, exts)
43
+ else if (exts.test(entry)) yield full
44
+ }
45
+ }
46
+
47
+ const DEFINES = /(--[a-z0-9-]+)\s*:/g
48
+ const SETS = /setProperty\(\s*['"`](--[a-z0-9-]+)/g
49
+ const BARE_READ = /var\(\s*(--[a-z0-9-]+)\s*\)/g
50
+
51
+ function definedNames(): Set<string> {
52
+ const names = new Set<string>()
53
+ const take = (file: string) => {
54
+ const text = readFileSync(file, 'utf-8')
55
+ for (const match of text.matchAll(DEFINES)) names.add(match[1])
56
+ for (const match of text.matchAll(SETS)) names.add(match[1])
57
+ }
58
+ for (const file of files(PACKAGES, /\.(ts|js|css|mjs|html)$/)) take(file)
59
+ for (const scope of ['@operato', '@material/web']) {
60
+ for (const file of files(path.join(MODULES, scope), /\.(js|css)$/)) take(file)
61
+ }
62
+ return names
63
+ }
64
+
65
+ function bareReadsOfUndefined(roots: string[], defined: Set<string>, skip: (file: string) => boolean): string[] {
66
+ const found = new Set<string>()
67
+ for (const root of roots) {
68
+ for (const file of files(root, /\.(ts|js|css)$/)) {
69
+ if (skip(file)) continue
70
+ for (const match of readFileSync(file, 'utf-8').matchAll(BARE_READ)) {
71
+ if (!defined.has(match[1])) found.add(match[1])
72
+ }
73
+ }
74
+ }
75
+ return [...found].sort()
76
+ }
77
+
78
+ /*
79
+ * Hooks: names a component reads so that a product's theme can set them, and none of ours does.
80
+ * fmsim sets `--button-active-border` (and operato's `--grid-record-focused-color`) in its own theme
81
+ * files, operato-pms sets `--content-container-max-width`. The rest are undefined in every codebase we
82
+ * checked - they draw nothing today, and deciding what they should draw is a design change, not a
83
+ * fix. A name leaves this list when something defines it or the read goes.
84
+ */
85
+ const HOOKS = [
86
+ '--button-active-border',
87
+ '--content-container-max-width',
88
+ '--form-input-width',
89
+ '--menu-tile-list-favorite-color',
90
+ '--menu-tile-list-item-margin'
91
+ ]
92
+
93
+ /*
94
+ * The installed operato components, as of @operato 10.19-10.22 (process is not installed here). The fixed typos leave this list when the
95
+ * next @operato release is installed - the test goes red then, and the line is deleted with it.
96
+ */
97
+ const OPERATO_KNOWN = [
98
+ '--button-radius',
99
+ '--calendar-monthly-color',
100
+ '--component-detail-border-color',
101
+ '--form-input-width',
102
+ '--grid-record-focused-color',
103
+ '--legend-background-color',
104
+ '--legend-border',
105
+ '--legend-border-radius',
106
+ '--legend-margin',
107
+ '--md-sys-color-on-primary-container-rgb',
108
+ '--md-sys-on-color-secondary',
109
+ '--md-sys-shape-corner-tiny',
110
+ '--md-sys-typescale-label-medium-weight',
111
+ '--opacity-light-dark-color',
112
+ '--pading-default',
113
+ '--record-view-edit-border',
114
+ '--record-view-grid-field-border',
115
+ '--spacing-smail'
116
+ ]
117
+
118
+ describe('custom properties read with no fallback are defined', () => {
119
+ const defined = definedNames()
120
+
121
+ it("★ in this repository's clients", () => {
122
+ const clients = readdirSync(PACKAGES).map(pkg => path.join(PACKAGES, pkg, 'client'))
123
+ const skip = (file: string) => /\.test\.|\/themes\/|\/dist/.test(file)
124
+
125
+ expect(bareReadsOfUndefined(clients, defined, skip)).toEqual(HOOKS)
126
+ })
127
+
128
+ it('★ in the operato components as installed', () => {
129
+ const skip = (file: string) => !/\/dist\//.test(file) || /\.(test|stories)\.|\/demo\/|\/test\//.test(file)
130
+
131
+ expect(bareReadsOfUndefined([path.join(MODULES, '@operato')], defined, skip)).toEqual(OPERATO_KNOWN)
132
+ })
133
+ })
@@ -159,5 +159,7 @@
159
159
  "title.lookup role": "lookup role",
160
160
  "title.select_item": "select item",
161
161
  "term.domain": "site",
162
- "field.roles": "roles"
162
+ "field.roles": "roles",
163
+ "label.released": "released",
164
+ "label.import": "import"
163
165
  }
@@ -159,5 +159,7 @@
159
159
  "title.lookup role": "ロール検索",
160
160
  "title.select_item": "アイテム選択",
161
161
  "term.domain": "事業場",
162
- "field.roles": "ロール"
162
+ "field.roles": "ロール",
163
+ "label.released": "公開済み",
164
+ "label.import": "インポート"
163
165
  }
@@ -159,5 +159,7 @@
159
159
  "title.lookup role": "역할 검색",
160
160
  "title.select_item": "아이템 선택",
161
161
  "term.domain": "사업장",
162
- "field.roles": "역할"
162
+ "field.roles": "역할",
163
+ "label.released": "발행됨",
164
+ "label.import": "가져오기"
163
165
  }
@@ -159,5 +159,7 @@
159
159
  "title.lookup role": "cari peranan",
160
160
  "title.select_item": "pilih item",
161
161
  "term.domain": "tapak",
162
- "field.roles": "peranan"
162
+ "field.roles": "peranan",
163
+ "label.released": "diterbitkan",
164
+ "label.import": "import"
163
165
  }
@@ -158,5 +158,8 @@
158
158
  "title.import event": "导入事件",
159
159
  "title.lookup role": "查找角色",
160
160
  "title.select_item": "选择项目",
161
- "field.roles": "角色"
161
+ "field.roles": "角色",
162
+ "term.domain": "站点",
163
+ "label.released": "已发布",
164
+ "label.import": "导入"
162
165
  }