mikoshi-construct 0.1.3 → 0.3.0
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/README.md +34 -5
- package/dist/cli.js +2060 -525
- package/package.json +15 -10
- package/templates/ai/claude/_claude/agents/architect.md +7 -11
- package/templates/ai/claude/_claude/agents/harness.md +7 -11
- package/templates/ai/claude/_claude/agents/implementer.md +6 -10
- package/templates/ai/claude/_claude/commands/plan.md +2 -0
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +43 -8
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +103 -30
- package/templates/ai/cursor/_cursor/rules/construct.mdc +1 -1
- package/templates/ai/shared/_claude/commands/construct-discover.md +25 -4
- package/templates/base/architecture/decisions/README.md +26 -0
- package/templates/base/architecture/principles.md +3 -1
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +22 -6
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +99 -25
- package/templates/presets/node-backend/baseline/eslint.config.mjs +11 -3
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +73 -18
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +12 -3
- package/templates/presets/node-frontend/sample/scripts/tests/lint/syntax-policy.test.ts +78 -0
|
@@ -2,35 +2,109 @@ import { ESLint } from 'eslint'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
const SAMPLES = {
|
|
6
|
+
plainModule: 'export function label(name: string): string {\n return name.trim()\n}\n',
|
|
7
|
+
processMember: 'export const level = process.env.LOG_LEVEL\n',
|
|
8
|
+
processDestructured: 'const { env } = process\n\nexport const level = env.LOG_LEVEL\n',
|
|
9
|
+
processViaGlobalThis: 'export const level = globalThis.process.env.LOG_LEVEL\n',
|
|
10
|
+
processAliased: 'const runtime = process\n\nexport const level = runtime.env.LOG_LEVEL\n',
|
|
11
|
+
processArgv: 'export const args = process.argv.slice(2)\n',
|
|
12
|
+
rawRequestMember: 'export function name(req: { body: { name: string } }): string {\n return req.body.name\n}\n',
|
|
13
|
+
rawRequestDestructured: 'export function name(req: { body: { name: string } }): string {\n const { body } = req\n return body.name\n}\n',
|
|
14
|
+
rawRequestAliased: 'export function name(req: { body: { name: string } }): string {\n const request = req\n return request.body.name\n}\n',
|
|
15
|
+
rawSqlCall: 'export const rows = sql.raw(\'select 1\')\n',
|
|
16
|
+
rawSqlTagged: 'export const rows = sql.raw`select 1`\n',
|
|
17
|
+
rawSqlComputed: 'export const rows = sql[\'raw\'](\'select 1\')\n',
|
|
18
|
+
sharedStaticImport: 'import type { components } from \'{{scope}}/shared\'\n\nexport type Contract = components\n',
|
|
19
|
+
sharedDynamicImport: 'export async function load(): Promise<unknown> {\n return import(\'{{scope}}/shared\')\n}\n',
|
|
20
|
+
sharedRequire: 'export const shared: unknown = require(\'{{scope}}/shared\')\n',
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
type SampleName = keyof typeof SAMPLES
|
|
24
|
+
|
|
25
|
+
const PROCESS_FORMS: SampleName[] = ['processMember', 'processDestructured', 'processViaGlobalThis', 'processAliased', 'processArgv']
|
|
26
|
+
const PROCESS_ENV_FORMS: SampleName[] = ['processMember', 'processDestructured', 'processViaGlobalThis']
|
|
27
|
+
const RAW_REQUEST_FORMS: SampleName[] = ['rawRequestMember', 'rawRequestDestructured', 'rawRequestAliased']
|
|
28
|
+
const RAW_SQL_FORMS: SampleName[] = ['rawSqlCall', 'rawSqlTagged', 'rawSqlComputed']
|
|
29
|
+
const SHARED_FORMS: SampleName[] = ['sharedStaticImport', 'sharedDynamicImport', 'sharedRequire']
|
|
18
30
|
|
|
19
|
-
const ROLES: Array<{ role: string, file: string,
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
31
|
+
const ROLES: Array<{ role: string, file: string, reported: SampleName[], exempt: SampleName[] }> = [
|
|
32
|
+
{
|
|
33
|
+
role: 'a service',
|
|
34
|
+
file: 'apps/api/src/things/things.service.ts',
|
|
35
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS, ...RAW_REQUEST_FORMS, ...SHARED_FORMS],
|
|
36
|
+
exempt: [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
role: 'a controller',
|
|
40
|
+
file: 'apps/api/src/things/things.controller.ts',
|
|
41
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS, ...SHARED_FORMS],
|
|
42
|
+
exempt: RAW_REQUEST_FORMS,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
role: 'a middleware',
|
|
46
|
+
file: 'apps/api/src/http/error-handler.middleware.ts',
|
|
47
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS, ...SHARED_FORMS],
|
|
48
|
+
exempt: RAW_REQUEST_FORMS,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
role: 'the contract types gateway',
|
|
52
|
+
file: 'apps/api/src/contracts/types.ts',
|
|
53
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
54
|
+
exempt: SHARED_FORMS,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
role: 'the app config',
|
|
58
|
+
file: 'apps/api/src/config.ts',
|
|
59
|
+
reported: [...RAW_SQL_FORMS, ...SHARED_FORMS],
|
|
60
|
+
exempt: [...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
role: 'the process entry',
|
|
64
|
+
file: 'apps/api/src/server.ts',
|
|
65
|
+
reported: [...RAW_SQL_FORMS, ...SHARED_FORMS],
|
|
66
|
+
exempt: [...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
role: 'a package module',
|
|
70
|
+
file: 'packages/shared/src/index.ts',
|
|
71
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_ENV_FORMS],
|
|
72
|
+
exempt: ['processArgv', ...RAW_REQUEST_FORMS, ...SHARED_FORMS],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
role: 'a package config',
|
|
76
|
+
file: 'packages/shared/src/shared.config.ts',
|
|
77
|
+
reported: RAW_SQL_FORMS,
|
|
78
|
+
exempt: [...PROCESS_FORMS, ...RAW_REQUEST_FORMS, ...SHARED_FORMS],
|
|
79
|
+
},
|
|
28
80
|
]
|
|
29
81
|
|
|
82
|
+
const eslint = new ESLint({
|
|
83
|
+
cwd: REPO_ROOT,
|
|
84
|
+
ruleFilter: ({ ruleId }) => ruleId === 'no-restricted-syntax',
|
|
85
|
+
overrideConfig: { languageOptions: { parserOptions: { projectService: false } } },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
async function restrictionReports(file: string, sample: SampleName): Promise<string[]> {
|
|
89
|
+
const [result] = await eslint.lintText(SAMPLES[sample], { filePath: file })
|
|
90
|
+
const fatal = result.messages.find(message => message.fatal)
|
|
91
|
+
if (fatal)
|
|
92
|
+
throw new Error(`${file} (${sample}): ${fatal.message}`)
|
|
93
|
+
return result.messages
|
|
94
|
+
.filter(message => message.ruleId === 'no-restricted-syntax')
|
|
95
|
+
.map(message => message.message)
|
|
96
|
+
}
|
|
97
|
+
|
|
30
98
|
describe('syntax policy by file role', () => {
|
|
31
|
-
for (const { role, file,
|
|
32
|
-
it(`
|
|
33
|
-
|
|
99
|
+
for (const { role, file, reported, exempt } of ROLES) {
|
|
100
|
+
it(`reports every restricted form in ${role}`, async () => {
|
|
101
|
+
for (const sample of reported)
|
|
102
|
+
expect(await restrictionReports(file, sample), sample).not.toEqual([])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it(`reports nothing ${role} may legitimately write`, async () => {
|
|
106
|
+
for (const sample of ['plainModule' as SampleName, ...exempt])
|
|
107
|
+
expect(await restrictionReports(file, sample), sample).toEqual([])
|
|
34
108
|
})
|
|
35
109
|
}
|
|
36
110
|
})
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import antfu from '@antfu/eslint-config'
|
|
2
2
|
|
|
3
3
|
const PROCESS_MEMBER = 'MemberExpression[object.name="process"]'
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const PROCESS_VIA_GLOBAL_THIS = 'MemberExpression[object.name="globalThis"]:matches([property.name="process"], [property.value="process"])'
|
|
5
|
+
const PROCESS_BINDING = 'VariableDeclarator[init.name="process"]'
|
|
6
|
+
const PROCESS = `:matches(${PROCESS_MEMBER}, ${PROCESS_VIA_GLOBAL_THIS}, ${PROCESS_BINDING})`
|
|
7
|
+
|
|
8
|
+
const RAW_REQUEST_MEMBER = 'MemberExpression[object.name="req"][property.name=/^(body|query|params)$/]'
|
|
9
|
+
const RAW_REQUEST_DESTRUCTURED = 'VariableDeclarator[init.name="req"] > ObjectPattern > Property[key.name=/^(body|query|params)$/]'
|
|
10
|
+
const RAW_REQUEST_ALIASED = 'VariableDeclarator[id.type="Identifier"][init.name="req"]'
|
|
11
|
+
const RAW_REQUEST_DATA = `:matches(${RAW_REQUEST_MEMBER}, ${RAW_REQUEST_DESTRUCTURED}, ${RAW_REQUEST_ALIASED})`
|
|
12
|
+
|
|
13
|
+
const RAW_SQL = 'MemberExpression[object.name="sql"]:matches([property.name="raw"], [property.value="raw"])'
|
|
6
14
|
|
|
7
15
|
const NO_PROCESS_OUTSIDE_CONFIG = {
|
|
8
|
-
selector:
|
|
16
|
+
selector: PROCESS,
|
|
9
17
|
message: 'src touches process only in config.ts: read configuration through readConfig() and pass the value on',
|
|
10
18
|
}
|
|
11
19
|
const NO_RAW_REQUEST_DATA = {
|
|
@@ -2,30 +2,85 @@ import { ESLint } from 'eslint'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
const SAMPLES = {
|
|
6
|
+
plainModule: 'export function label(name: string): string {\n return name.trim()\n}\n',
|
|
7
|
+
processMember: 'export const level = process.env.LOG_LEVEL\n',
|
|
8
|
+
processDestructured: 'const { env } = process\n\nexport const level = env.LOG_LEVEL\n',
|
|
9
|
+
processViaGlobalThis: 'export const level = globalThis.process.env.LOG_LEVEL\n',
|
|
10
|
+
processAliased: 'const runtime = process\n\nexport const level = runtime.env.LOG_LEVEL\n',
|
|
11
|
+
rawRequestMember: 'export function name(req: { body: { name: string } }): string {\n return req.body.name\n}\n',
|
|
12
|
+
rawRequestDestructured: 'export function name(req: { body: { name: string } }): string {\n const { body } = req\n return body.name\n}\n',
|
|
13
|
+
rawRequestAliased: 'export function name(req: { body: { name: string } }): string {\n const request = req\n return request.body.name\n}\n',
|
|
14
|
+
rawSqlCall: 'export const rows = sql.raw(\'select 1\')\n',
|
|
15
|
+
rawSqlTagged: 'export const rows = sql.raw`select 1`\n',
|
|
16
|
+
rawSqlComputed: 'export const rows = sql[\'raw\'](\'select 1\')\n',
|
|
17
|
+
} as const
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
type SampleName = keyof typeof SAMPLES
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return restrictions.map(restriction => restriction.selector).sort()
|
|
15
|
-
}
|
|
21
|
+
const PROCESS_FORMS: SampleName[] = ['processMember', 'processDestructured', 'processViaGlobalThis', 'processAliased']
|
|
22
|
+
const RAW_REQUEST_FORMS: SampleName[] = ['rawRequestMember', 'rawRequestDestructured', 'rawRequestAliased']
|
|
23
|
+
const RAW_SQL_FORMS: SampleName[] = ['rawSqlCall', 'rawSqlTagged', 'rawSqlComputed']
|
|
16
24
|
|
|
17
|
-
const ROLES: Array<{ role: string, file: string,
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const ROLES: Array<{ role: string, file: string, reported: SampleName[], exempt: SampleName[] }> = [
|
|
26
|
+
{
|
|
27
|
+
role: 'a service',
|
|
28
|
+
file: 'src/things/things.service.ts',
|
|
29
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
30
|
+
exempt: [],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
role: 'a controller',
|
|
34
|
+
file: 'src/things/things.controller.ts',
|
|
35
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS],
|
|
36
|
+
exempt: RAW_REQUEST_FORMS,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
role: 'a middleware',
|
|
40
|
+
file: 'src/http/error-handler.middleware.ts',
|
|
41
|
+
reported: [...RAW_SQL_FORMS, ...PROCESS_FORMS],
|
|
42
|
+
exempt: RAW_REQUEST_FORMS,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
role: 'the app config',
|
|
46
|
+
file: 'src/config.ts',
|
|
47
|
+
reported: RAW_SQL_FORMS,
|
|
48
|
+
exempt: [...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
role: 'the process entry',
|
|
52
|
+
file: 'src/server.ts',
|
|
53
|
+
reported: RAW_SQL_FORMS,
|
|
54
|
+
exempt: [...PROCESS_FORMS, ...RAW_REQUEST_FORMS],
|
|
55
|
+
},
|
|
23
56
|
]
|
|
24
57
|
|
|
58
|
+
const eslint = new ESLint({
|
|
59
|
+
cwd: REPO_ROOT,
|
|
60
|
+
ruleFilter: ({ ruleId }) => ruleId === 'no-restricted-syntax',
|
|
61
|
+
overrideConfig: { languageOptions: { parserOptions: { projectService: false } } },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
async function restrictionReports(file: string, sample: SampleName): Promise<string[]> {
|
|
65
|
+
const [result] = await eslint.lintText(SAMPLES[sample], { filePath: file })
|
|
66
|
+
const fatal = result.messages.find(message => message.fatal)
|
|
67
|
+
if (fatal)
|
|
68
|
+
throw new Error(`${file} (${sample}): ${fatal.message}`)
|
|
69
|
+
return result.messages
|
|
70
|
+
.filter(message => message.ruleId === 'no-restricted-syntax')
|
|
71
|
+
.map(message => message.message)
|
|
72
|
+
}
|
|
73
|
+
|
|
25
74
|
describe('syntax policy by file role', () => {
|
|
26
|
-
for (const { role, file,
|
|
27
|
-
it(`
|
|
28
|
-
|
|
75
|
+
for (const { role, file, reported, exempt } of ROLES) {
|
|
76
|
+
it(`reports every restricted form in ${role}`, async () => {
|
|
77
|
+
for (const sample of reported)
|
|
78
|
+
expect(await restrictionReports(file, sample), sample).not.toEqual([])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it(`reports nothing ${role} may legitimately write`, async () => {
|
|
82
|
+
for (const sample of ['plainModule' as SampleName, ...exempt])
|
|
83
|
+
expect(await restrictionReports(file, sample), sample).toEqual([])
|
|
29
84
|
})
|
|
30
85
|
}
|
|
31
86
|
})
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import antfu from '@antfu/eslint-config'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const CLASS_LIST_ACCESS = ':matches([callee.object.property.name="classList"], [callee.object.property.value="classList"])'
|
|
4
|
+
const CLASS_LIST_METHOD = ':matches([callee.property.name=/^(add|remove|toggle|replace)$/], [callee.property.value=/^(add|remove|toggle|replace)$/])'
|
|
5
|
+
const CLASS_LIST_CALL = `CallExpression${CLASS_LIST_ACCESS}${CLASS_LIST_METHOD}`
|
|
6
|
+
const CLASS_LIST_BINDING = 'VariableDeclarator:matches([init.property.name="classList"], [init.property.value="classList"])'
|
|
7
|
+
const CLASS_LIST_MUTATION = `:matches(${CLASS_LIST_CALL}, ${CLASS_LIST_BINDING})`
|
|
8
|
+
|
|
9
|
+
const STYLE_ACCESS = ':matches([left.object.property.name="style"], [left.object.property.value="style"])'
|
|
10
|
+
const INLINE_STYLE_WRITE = `AssignmentExpression${STYLE_ACCESS}`
|
|
11
|
+
const STYLE_METHOD = ':matches([callee.property.name=/^(setProperty|removeProperty)$/], [callee.property.value=/^(setProperty|removeProperty)$/])'
|
|
12
|
+
const INLINE_STYLE_CALL = `CallExpression:matches([callee.object.property.name="style"], [callee.object.property.value="style"])${STYLE_METHOD}`
|
|
13
|
+
const STYLE_BINDING = 'VariableDeclarator:matches([init.property.name="style"], [init.property.value="style"])'
|
|
14
|
+
const INLINE_STYLE_PROPERTY = `:matches(${INLINE_STYLE_CALL}, ${STYLE_BINDING})`
|
|
6
15
|
|
|
7
16
|
const NO_STATE_CLASSES = {
|
|
8
17
|
selector: CLASS_LIST_MUTATION,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ESLint } from 'eslint'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
|
+
|
|
5
|
+
const SAMPLES = {
|
|
6
|
+
plainModule: 'export function open(element: HTMLElement): void {\n element.dataset.state = \'open\'\n}\n',
|
|
7
|
+
classListCall: 'export function open(element: HTMLElement): void {\n element.classList.add(\'is-open\')\n}\n',
|
|
8
|
+
classListComputed: 'export function open(element: HTMLElement): void {\n element.classList[\'add\'](\'is-open\')\n}\n',
|
|
9
|
+
classListOnComputedElement: 'export function open(element: HTMLElement): void {\n element[\'classList\'].add(\'is-open\')\n}\n',
|
|
10
|
+
classListBound: 'export function open(element: HTMLElement): void {\n const classes = element.classList\n classes.add(\'is-open\')\n}\n',
|
|
11
|
+
inlineStyleWrite: 'export function open(element: HTMLElement): void {\n element.style.display = \'block\'\n}\n',
|
|
12
|
+
inlineStyleWriteComputed: 'export function open(element: HTMLElement): void {\n element[\'style\'].display = \'block\'\n}\n',
|
|
13
|
+
inlineStyleProperty: 'export function open(element: HTMLElement): void {\n element.style.setProperty(\'--open\', \'1\')\n}\n',
|
|
14
|
+
inlineStylePropertyComputed: 'export function open(element: HTMLElement): void {\n element.style[\'setProperty\'](\'--open\', \'1\')\n}\n',
|
|
15
|
+
inlineStyleBound: 'export function open(element: HTMLElement): void {\n const style = element.style\n style.display = \'block\'\n}\n',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
type SampleName = keyof typeof SAMPLES
|
|
19
|
+
|
|
20
|
+
const CLASS_LIST_FORMS: SampleName[] = ['classListCall', 'classListComputed', 'classListOnComputedElement', 'classListBound']
|
|
21
|
+
const INLINE_STYLE_FORMS: SampleName[] = ['inlineStyleWrite', 'inlineStyleWriteComputed', 'inlineStyleProperty', 'inlineStylePropertyComputed', 'inlineStyleBound']
|
|
22
|
+
|
|
23
|
+
const ROLES: Array<{ role: string, file: string, reported: SampleName[], exempt: SampleName[] }> = [
|
|
24
|
+
{
|
|
25
|
+
role: 'an application module',
|
|
26
|
+
file: 'src/app.ts',
|
|
27
|
+
reported: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
28
|
+
exempt: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
role: 'the entry point',
|
|
32
|
+
file: 'src/main.ts',
|
|
33
|
+
reported: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
34
|
+
exempt: [],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
role: 'a test',
|
|
38
|
+
file: 'tests/app.test.ts',
|
|
39
|
+
reported: [],
|
|
40
|
+
exempt: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
role: 'a repository script',
|
|
44
|
+
file: 'scripts/composition/check.ts',
|
|
45
|
+
reported: [],
|
|
46
|
+
exempt: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const eslint = new ESLint({
|
|
51
|
+
cwd: REPO_ROOT,
|
|
52
|
+
ruleFilter: ({ ruleId }) => ruleId === 'no-restricted-syntax',
|
|
53
|
+
overrideConfig: { languageOptions: { parserOptions: { projectService: false } } },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
async function restrictionReports(file: string, sample: SampleName): Promise<string[]> {
|
|
57
|
+
const [result] = await eslint.lintText(SAMPLES[sample], { filePath: file })
|
|
58
|
+
const fatal = result.messages.find(message => message.fatal)
|
|
59
|
+
if (fatal)
|
|
60
|
+
throw new Error(`${file} (${sample}): ${fatal.message}`)
|
|
61
|
+
return result.messages
|
|
62
|
+
.filter(message => message.ruleId === 'no-restricted-syntax')
|
|
63
|
+
.map(message => message.message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('styling policy by file role', () => {
|
|
67
|
+
for (const { role, file, reported, exempt } of ROLES) {
|
|
68
|
+
it(`reports every restricted form in ${role}`, async () => {
|
|
69
|
+
for (const sample of reported)
|
|
70
|
+
expect(await restrictionReports(file, sample), sample).not.toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it(`reports nothing ${role} may legitimately write`, async () => {
|
|
74
|
+
for (const sample of ['plainModule' as SampleName, ...exempt])
|
|
75
|
+
expect(await restrictionReports(file, sample), sample).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
})
|