declapract-typescript-ehmpathy 0.47.47 → 0.47.49
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/practices/cicd-common/best-practice/.github/workflows/.test.yml +8 -0
- package/dist/practices/directory-structure-src/bad-practices/data-dir/src/<star><star>/data/<star><star>/<star>.declapract.ts +19 -0
- package/dist/practices/directory-structure-src/bad-practices/domain-dir/src/<star><star>/domain/<star><star>/<star>.declapract.ts +17 -0
- package/dist/practices/directory-structure-src/bad-practices/logic-dir/src/{logic → <star><star>/logic}/<star><star>/<star>.declapract.ts +3 -3
- package/dist/practices/directory-structure-src/bad-practices/model-dir/src/{model/<star><star>/<star>.ts.declapract.ts → <star><star>/model/<star><star>/<star>.declapract.ts} +2 -2
- package/dist/practices/directory-structure-src/bad-practices/nonpublished-modules-dir/src/__nonpublished_modules__/<star><star>/<star>.declapract.ts +17 -0
- package/dist/practices/directory-structure-src/bad-practices/old-import-paths/src/<star><star>/<star>.ts.declapract.ts +9 -1
- package/dist/practices/directory-structure-src/bad-practices/services-dir/src/<star><star>/services/<star><star>/<star>.declapract.ts +16 -0
- package/dist/practices/tests/best-practice/package.json +6 -6
- package/package.json +11 -11
- package/dist/practices/directory-structure-src/bad-practices/data-dir/src/data/<star><star>/<star>.ts.declapract.ts +0 -19
- package/dist/practices/directory-structure-src/bad-practices/domain-dir/src/domain/<star><star>/<star>.ts.declapract.ts +0 -17
- package/dist/practices/directory-structure-src/bad-practices/services-dir/src/services/<star><star>/<star>.ts.declapract.ts +0 -3
|
@@ -220,12 +220,14 @@ jobs:
|
|
|
220
220
|
- name: test:integration (explicit ${{ matrix.shard.index }})
|
|
221
221
|
if: matrix.shard.type == 'explicit'
|
|
222
222
|
run: |
|
|
223
|
+
set -eu
|
|
223
224
|
patterns='${{ join(matrix.shard.patterns, '|') }}'
|
|
224
225
|
THOROUGH=true npm run test:integration -- --testPathPatterns="$patterns" --json --outputFile=jest-results.json
|
|
225
226
|
|
|
226
227
|
- name: test:integration (dynamic ${{ matrix.shard.shard }}/${{ matrix.shard.total }})
|
|
227
228
|
if: matrix.shard.type == 'dynamic'
|
|
228
229
|
run: |
|
|
230
|
+
set -eu
|
|
229
231
|
exclude='${{ needs.enshard.outputs.integration-patterns }}'
|
|
230
232
|
if [[ -n "$exclude" ]]; then
|
|
231
233
|
THOROUGH=true npm run test:integration -- --testPathIgnorePatterns="$exclude" --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
@@ -236,6 +238,7 @@ jobs:
|
|
|
236
238
|
- name: report slow tests
|
|
237
239
|
if: always() && hashFiles('jest-results.json') != ''
|
|
238
240
|
run: |
|
|
241
|
+
set -eu
|
|
239
242
|
jq -r '.testResults[] | "\(.name):\(.perfStats.runtime)"' jest-results.json | while IFS=: read -r name runtime; do
|
|
240
243
|
seconds=$((runtime / 1000))
|
|
241
244
|
if [[ $seconds -ge 30 ]]; then
|
|
@@ -286,12 +289,14 @@ jobs:
|
|
|
286
289
|
- name: test:acceptance (explicit ${{ matrix.shard.index }})
|
|
287
290
|
if: matrix.shard.type == 'explicit'
|
|
288
291
|
run: |
|
|
292
|
+
set -eu
|
|
289
293
|
patterns='${{ join(matrix.shard.patterns, '|') }}'
|
|
290
294
|
THOROUGH=true npm run test:acceptance -- --testPathPatterns="$patterns" --json --outputFile=jest-results.json
|
|
291
295
|
|
|
292
296
|
- name: test:acceptance (dynamic ${{ matrix.shard.shard }}/${{ matrix.shard.total }})
|
|
293
297
|
if: matrix.shard.type == 'dynamic'
|
|
294
298
|
run: |
|
|
299
|
+
set -eu
|
|
295
300
|
exclude='${{ needs.enshard.outputs.acceptance-patterns }}'
|
|
296
301
|
if [[ -n "$exclude" ]]; then
|
|
297
302
|
THOROUGH=true npm run test:acceptance -- --testPathIgnorePatterns="$exclude" --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
@@ -302,6 +307,7 @@ jobs:
|
|
|
302
307
|
- name: report slow tests
|
|
303
308
|
if: always() && hashFiles('jest-results.json') != ''
|
|
304
309
|
run: |
|
|
310
|
+
set -eu
|
|
305
311
|
jq -r '.testResults[] | "\(.name):\(.perfStats.runtime)"' jest-results.json | while IFS=: read -r name runtime; do
|
|
306
312
|
seconds=$((runtime / 1000))
|
|
307
313
|
if [[ $seconds -ge 30 ]]; then
|
|
@@ -319,6 +325,7 @@ jobs:
|
|
|
319
325
|
steps:
|
|
320
326
|
- name: report shard results
|
|
321
327
|
run: |
|
|
328
|
+
set -eu
|
|
322
329
|
if [[ "${{ needs.test-shards-integration.result }}" == "success" ]]; then
|
|
323
330
|
echo "👌 all integration test shards passed"
|
|
324
331
|
else
|
|
@@ -334,6 +341,7 @@ jobs:
|
|
|
334
341
|
steps:
|
|
335
342
|
- name: report shard results
|
|
336
343
|
run: |
|
|
344
|
+
set -eu
|
|
337
345
|
if [[ "${{ needs.test-shards-acceptance.result }}" == "success" ]]; then
|
|
338
346
|
echo "👌 all acceptance test shards passed"
|
|
339
347
|
else
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
// if files exist in any /data/, this is a bad practice
|
|
4
|
+
export const check = FileCheckType.EXISTS;
|
|
5
|
+
|
|
6
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
7
|
+
// move any /data/dao/* to /access/daos/*
|
|
8
|
+
// move any /data/clients/* to /access/sdks/*
|
|
9
|
+
// move other /data/* to /access/*
|
|
10
|
+
const newPath = context.relativeFilePath
|
|
11
|
+
.replace(/\/data\/dao\//g, '/access/daos/')
|
|
12
|
+
.replace(/\/data\/clients\//g, '/access/sdks/')
|
|
13
|
+
.replace(/\/data\//g, '/access/');
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
contents: contents ?? null,
|
|
17
|
+
relativeFilePath: newPath,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
// if files exist in any /domain/, this is a bad practice
|
|
4
|
+
export const check = FileCheckType.EXISTS;
|
|
5
|
+
|
|
6
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
7
|
+
// move any /domain/objects/* to /domain.objects/*
|
|
8
|
+
// move any /domain/* to /domain.objects/*
|
|
9
|
+
const newPath = context.relativeFilePath
|
|
10
|
+
.replace(/\/domain\/objects\//g, '/domain.objects/')
|
|
11
|
+
.replace(/\/domain\//g, '/domain.objects/');
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
contents: contents ?? null,
|
|
15
|
+
relativeFilePath: newPath,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -3,13 +3,13 @@ import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
|
3
3
|
export const check = FileCheckType.EXISTS;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* .what = moves any file from
|
|
6
|
+
* .what = moves any file from any /logic/ to /domain.operations/
|
|
7
7
|
* .why = ensures all files are moved, not just .ts files — prevents silent data loss for hidden paths
|
|
8
8
|
*/
|
|
9
9
|
export const fix: FileFixFunction = (contents, context) => {
|
|
10
10
|
const newPath = context.relativeFilePath.replace(
|
|
11
|
-
|
|
12
|
-
'
|
|
11
|
+
/\/logic\//g,
|
|
12
|
+
'/domain.operations/',
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
return {
|
|
@@ -10,7 +10,7 @@ export const fix: FileFixFunction = (contents, context) => {
|
|
|
10
10
|
"export * from './';",
|
|
11
11
|
) ?? null,
|
|
12
12
|
relativeFilePath: context.relativeFilePath
|
|
13
|
-
.replace(
|
|
14
|
-
.replace(
|
|
13
|
+
.replace(/\/model\/domainObjects\//g, '/domain.objects/')
|
|
14
|
+
.replace(/\/model\//g, '/domain.objects/'),
|
|
15
15
|
};
|
|
16
16
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
// if files exist in __nonpublished_modules__/, this is a bad practice
|
|
4
|
+
export const check = FileCheckType.EXISTS;
|
|
5
|
+
|
|
6
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
7
|
+
// move any /__nonpublished_modules__/ to /_topublish/
|
|
8
|
+
const newPath = context.relativeFilePath.replace(
|
|
9
|
+
/__nonpublished_modules__\//g,
|
|
10
|
+
'_topublish/',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
contents: contents ?? null,
|
|
15
|
+
relativeFilePath: newPath,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -8,6 +8,9 @@ const OLD_PATH_PATTERNS = [
|
|
|
8
8
|
/from\s+['"][^'"]*\/domain\/objects/,
|
|
9
9
|
/from\s+['"][^'"]*\/domain\//,
|
|
10
10
|
/from\s+['"][^'"]*\/logic\//,
|
|
11
|
+
/from\s+['"][^'"]*\/model\/domainObjects/,
|
|
12
|
+
/from\s+['"][^'"]*\/model\//,
|
|
13
|
+
/from\s+['"][^'"]*\/services\//,
|
|
11
14
|
];
|
|
12
15
|
|
|
13
16
|
export const check: FileCheckFunction = (contents) => {
|
|
@@ -35,7 +38,12 @@ export const fix: FileFixFunction = (contents) => {
|
|
|
35
38
|
.replace(/(['"])([^'"]*?)\/domain\/objects\b/g, '$1$2/domain.objects')
|
|
36
39
|
.replace(/(['"])([^'"]*?)\/domain\//g, '$1$2/domain.objects/')
|
|
37
40
|
// logic layer fixes
|
|
38
|
-
.replace(/(['"])([^'"]*?)\/logic\//g, '$1$2/domain.operations/')
|
|
41
|
+
.replace(/(['"])([^'"]*?)\/logic\//g, '$1$2/domain.operations/')
|
|
42
|
+
// model layer fixes (old structure before domain.objects)
|
|
43
|
+
.replace(/(['"])([^'"]*?)\/model\/domainObjects\b/g, '$1$2/domain.objects')
|
|
44
|
+
.replace(/(['"])([^'"]*?)\/model\//g, '$1$2/domain.objects/')
|
|
45
|
+
// services layer fixes (services = business logic, not service clients)
|
|
46
|
+
.replace(/(['"])([^'"]*?)\/services\//g, '$1$2/domain.operations/');
|
|
39
47
|
|
|
40
48
|
return { contents: fixed };
|
|
41
49
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.EXISTS; // if a file exists with this path pattern, then it's bad practice
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
6
|
+
// move any /services/* to /domain.operations/* (services = business logic)
|
|
7
|
+
const newPath = context.relativeFilePath.replace(
|
|
8
|
+
/\/services\//g,
|
|
9
|
+
'/domain.operations/',
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
contents: contents ?? null,
|
|
14
|
+
relativeFilePath: newPath,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"@swc/jest": "@declapract{check.minVersion('0.2.39')}"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test:auth": "[ \"$ECHO\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
13
|
-
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
14
|
-
"test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
15
|
-
"test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
16
|
-
"test": "eval $(ECHO=true npm run --silent test:auth) && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
17
|
-
"test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')"
|
|
12
|
+
"test:auth": "[ \"${ECHO:-}\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
13
|
+
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
14
|
+
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
15
|
+
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
16
|
+
"test": "set -eu && eval $(ECHO=true npm run --silent test:auth) && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
17
|
+
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "declapract-typescript-ehmpathy",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "declapract best practices declarations for typescript",
|
|
5
|
-
"version": "0.47.
|
|
5
|
+
"version": "0.47.49",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"repository": "ehmpathy/declapract-typescript-ehmpathy",
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
"test:lint:biome": "npm run biome:nested:hide && biome check --diagnostic-level=error; EXIT=$?; npm run biome:nested:restore; exit $EXIT",
|
|
34
34
|
"test:lint:biome:all": "npm run biome:nested:hide && biome check; EXIT=$?; npm run biome:nested:restore; exit $EXIT",
|
|
35
35
|
"test:lint": "npm run test:lint:biome && npm run test:lint:deps",
|
|
36
|
-
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
37
|
-
"test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
38
|
-
"test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
39
|
-
"test": "npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && test:validate",
|
|
40
|
-
"test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests",
|
|
36
|
+
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
37
|
+
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
38
|
+
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
39
|
+
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && test:validate",
|
|
40
|
+
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
41
41
|
"prepush": "npm run test && npm run build",
|
|
42
42
|
"prepublish": "npm run build",
|
|
43
43
|
"preversion": "npm run prepush",
|
|
44
44
|
"postversion": "git push origin HEAD --tags --no-verify",
|
|
45
45
|
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
46
46
|
"prepare:rhachet": "npx rhachet init --hooks --roles behaver mechanic reviewer",
|
|
47
|
-
"prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi"
|
|
47
|
+
"prepare": "if [ -e .git ] && [ -z \"${CI:-}\" ]; then npm run prepare:husky && npm run prepare:rhachet; fi"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"domain-objects": "0.31.9",
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"esbuild-register": "3.6.0",
|
|
76
76
|
"husky": "8.0.3",
|
|
77
77
|
"jest": "30.2.0",
|
|
78
|
-
"rhachet": "1.
|
|
78
|
+
"rhachet": "1.37.16",
|
|
79
79
|
"rhachet-brains-anthropic": "0.3.3",
|
|
80
|
-
"rhachet-roles-bhrain": "0.
|
|
81
|
-
"rhachet-roles-bhuild": "0.
|
|
82
|
-
"rhachet-roles-ehmpathy": "1.
|
|
80
|
+
"rhachet-roles-bhrain": "0.20.0",
|
|
81
|
+
"rhachet-roles-bhuild": "0.14.2",
|
|
82
|
+
"rhachet-roles-ehmpathy": "1.27.16",
|
|
83
83
|
"tsc-alias": "1.8.10",
|
|
84
84
|
"tsx": "4.20.6",
|
|
85
85
|
"type-fns": "0.8.1",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
-
|
|
3
|
-
// if files exist in src/data/, this is a bad practice
|
|
4
|
-
export const check = FileCheckType.EXISTS;
|
|
5
|
-
|
|
6
|
-
export const fix: FileFixFunction = (contents, context) => {
|
|
7
|
-
// move src/data/dao/* to src/access/daos/*
|
|
8
|
-
// move src/data/clients/* to src/access/sdks/*
|
|
9
|
-
// move other src/data/* to src/access/*
|
|
10
|
-
const newPath = context.relativeFilePath
|
|
11
|
-
.replace('src/data/dao/', 'src/access/daos/')
|
|
12
|
-
.replace('src/data/clients/', 'src/access/sdks/')
|
|
13
|
-
.replace('src/data/', 'src/access/');
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
contents,
|
|
17
|
-
relativeFilePath: newPath,
|
|
18
|
-
};
|
|
19
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
-
|
|
3
|
-
// if files exist in src/domain/, this is a bad practice
|
|
4
|
-
export const check = FileCheckType.EXISTS;
|
|
5
|
-
|
|
6
|
-
export const fix: FileFixFunction = (contents, context) => {
|
|
7
|
-
// move src/domain/objects/* to src/domain.objects/*
|
|
8
|
-
// move src/domain/* to src/domain.objects/*
|
|
9
|
-
const newPath = context.relativeFilePath
|
|
10
|
-
.replace('src/domain/objects/', 'src/domain.objects/')
|
|
11
|
-
.replace('src/domain/', 'src/domain.objects/');
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
contents,
|
|
15
|
-
relativeFilePath: newPath,
|
|
16
|
-
};
|
|
17
|
-
};
|