@astrale-os/sdk 0.4.12 → 0.4.13
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/cli/run.d.ts +8 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +51 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/define/remote-function.d.ts +3 -1
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js.map +1 -1
- package/dist/dispatch/dispatcher.d.ts +10 -20
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +55 -54
- package/dist/dispatch/dispatcher.js.map +1 -1
- package/dist/dispatch/execute.d.ts +2 -1
- package/dist/dispatch/execute.d.ts.map +1 -1
- package/dist/dispatch/execute.js +1 -0
- package/dist/dispatch/execute.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/linter/analyze.d.ts +6 -0
- package/dist/linter/analyze.d.ts.map +1 -0
- package/dist/linter/analyze.js +12 -0
- package/dist/linter/analyze.js.map +1 -0
- package/dist/linter/diagnostic.d.ts +32 -0
- package/dist/linter/diagnostic.d.ts.map +1 -0
- package/dist/linter/diagnostic.js +35 -0
- package/dist/linter/diagnostic.js.map +1 -0
- package/dist/linter/index.d.ts +10 -0
- package/dist/linter/index.d.ts.map +1 -0
- package/dist/linter/index.js +6 -0
- package/dist/linter/index.js.map +1 -0
- package/dist/linter/lint.d.ts +8 -0
- package/dist/linter/lint.d.ts.map +1 -0
- package/dist/linter/lint.js +26 -0
- package/dist/linter/lint.js.map +1 -0
- package/dist/linter/oxlint/config.d.ts +14 -0
- package/dist/linter/oxlint/config.d.ts.map +1 -0
- package/dist/linter/oxlint/config.js +18 -0
- package/dist/linter/oxlint/config.js.map +1 -0
- package/dist/linter/oxlint/plugin.d.ts +13 -0
- package/dist/linter/oxlint/plugin.d.ts.map +1 -0
- package/dist/linter/oxlint/plugin.js +14 -0
- package/dist/linter/oxlint/plugin.js.map +1 -0
- package/dist/linter/oxlint/run.d.ts +7 -0
- package/dist/linter/oxlint/run.d.ts.map +1 -0
- package/dist/linter/oxlint/run.js +122 -0
- package/dist/linter/oxlint/run.js.map +1 -0
- package/dist/linter/preflight.d.ts +2 -0
- package/dist/linter/preflight.d.ts.map +1 -0
- package/dist/linter/preflight.js +162 -0
- package/dist/linter/preflight.js.map +1 -0
- package/dist/linter/project.d.ts +12 -0
- package/dist/linter/project.d.ts.map +1 -0
- package/dist/linter/project.js +88 -0
- package/dist/linter/project.js.map +1 -0
- package/dist/linter/report.d.ts +4 -0
- package/dist/linter/report.d.ts.map +1 -0
- package/dist/linter/report.js +22 -0
- package/dist/linter/report.js.map +1 -0
- package/dist/linter/rules/catalog.d.ts +56 -0
- package/dist/linter/rules/catalog.d.ts.map +1 -0
- package/dist/linter/rules/catalog.js +53 -0
- package/dist/linter/rules/catalog.js.map +1 -0
- package/dist/linter/rules/core-has-no-async.d.ts +3 -0
- package/dist/linter/rules/core-has-no-async.d.ts.map +1 -0
- package/dist/linter/rules/core-has-no-async.js +43 -0
- package/dist/linter/rules/core-has-no-async.js.map +1 -0
- package/dist/linter/rules/core-is-pure.d.ts +3 -0
- package/dist/linter/rules/core-is-pure.d.ts.map +1 -0
- package/dist/linter/rules/core-is-pure.js +74 -0
- package/dist/linter/rules/core-is-pure.js.map +1 -0
- package/dist/linter/rules/no-nested-steps.d.ts +3 -0
- package/dist/linter/rules/no-nested-steps.d.ts.map +1 -0
- package/dist/linter/rules/no-nested-steps.js +32 -0
- package/dist/linter/rules/no-nested-steps.js.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts +3 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.js +64 -0
- package/dist/linter/rules/no-read-inside-mutate.js.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts +3 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.js +23 -0
- package/dist/linter/rules/one-remote-definition-per-file.js.map +1 -0
- package/dist/linter/rules/oxlint-ast.d.ts +30 -0
- package/dist/linter/rules/oxlint-ast.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-ast.js +104 -0
- package/dist/linter/rules/oxlint-ast.js.map +1 -0
- package/dist/linter/rules/oxlint-rule.d.ts +45 -0
- package/dist/linter/rules/oxlint-rule.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-rule.js +2 -0
- package/dist/linter/rules/oxlint-rule.js.map +1 -0
- package/dist/linter/rules/project-rule.d.ts +10 -0
- package/dist/linter/rules/project-rule.d.ts.map +1 -0
- package/dist/linter/rules/project-rule.js +24 -0
- package/dist/linter/rules/project-rule.js.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts +3 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.js +23 -0
- package/dist/linter/rules/thin-schema-composition-root.js.map +1 -0
- package/dist/linter/source.d.ts +24 -0
- package/dist/linter/source.d.ts.map +1 -0
- package/dist/linter/source.js +279 -0
- package/dist/linter/source.js.map +1 -0
- package/dist/linter/suppression.d.ts +3 -0
- package/dist/linter/suppression.d.ts.map +1 -0
- package/dist/linter/suppression.js +11 -0
- package/dist/linter/suppression.js.map +1 -0
- package/dist/method/context.d.ts +3 -1
- package/dist/method/context.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.js +4 -4
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/create.js +1 -1
- package/dist/server/create.js.map +1 -1
- package/dist/server/start.d.ts.map +1 -1
- package/dist/server/start.js.map +1 -1
- package/package.json +27 -7
- package/src/cli/run.ts +53 -2
- package/src/define/remote-function.ts +3 -1
- package/src/dispatch/dispatcher.ts +62 -69
- package/src/dispatch/execute.ts +3 -1
- package/src/index.ts +2 -2
- package/src/linter/analyze.ts +17 -0
- package/src/linter/diagnostic.ts +68 -0
- package/src/linter/docs/DIAGNOSTICS.md +47 -0
- package/src/linter/docs/JUDGMENT.md +81 -0
- package/src/linter/docs/PRINCIPLES.md +97 -0
- package/src/linter/docs/README.md +38 -0
- package/src/linter/docs/RULE-AUTHORING.md +144 -0
- package/src/linter/docs/RULES.md +153 -0
- package/src/linter/index.ts +14 -0
- package/src/linter/lint.ts +32 -0
- package/src/linter/oxlint/astrale-ox.d.ts +5 -0
- package/src/linter/oxlint/config.ts +19 -0
- package/src/linter/oxlint/plugin.js +16 -0
- package/src/linter/oxlint/run.ts +162 -0
- package/src/linter/preflight.ts +181 -0
- package/src/linter/project.ts +108 -0
- package/src/linter/report.ts +25 -0
- package/src/linter/rules/catalog.ts +67 -0
- package/src/linter/rules/core-has-no-async.js +43 -0
- package/src/linter/rules/core-is-pure.ts +103 -0
- package/src/linter/rules/no-nested-steps.js +37 -0
- package/src/linter/rules/no-read-inside-mutate.js +72 -0
- package/src/linter/rules/one-remote-definition-per-file.ts +29 -0
- package/src/linter/rules/oxlint-ast.js +110 -0
- package/src/linter/rules/oxlint-rule.ts +35 -0
- package/src/linter/rules/project-rule.ts +37 -0
- package/src/linter/rules/thin-schema-composition-root.ts +26 -0
- package/src/linter/source.ts +323 -0
- package/src/linter/suppression.ts +11 -0
- package/src/method/context.ts +3 -1
- package/src/server/auxiliary-routes.ts +9 -4
- package/src/server/create.ts +1 -1
- package/src/server/start.ts +2 -1
- package/dist/defer/index.d.ts +0 -24
- package/dist/defer/index.d.ts.map +0 -1
- package/dist/defer/index.js +0 -42
- package/dist/defer/index.js.map +0 -1
- package/src/defer/index.ts +0 -64
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ruleCatalog, ruleId, ruleInfo } from './rules/catalog.js'
|
|
2
|
+
export type { ImplementedRuleSlug, RuleInfo, RuleOwner } from './rules/catalog.js'
|
|
3
|
+
export type {
|
|
4
|
+
DiagnosticLocation,
|
|
5
|
+
DiagnosticSeverity,
|
|
6
|
+
LintDiagnostic,
|
|
7
|
+
LintResult,
|
|
8
|
+
} from './diagnostic.js'
|
|
9
|
+
export { LinterToolError } from './diagnostic.js'
|
|
10
|
+
export { lintDomain } from './lint.js'
|
|
11
|
+
export type { LintDomainOptions } from './lint.js'
|
|
12
|
+
export { domainOxlintConfig } from './oxlint/config.js'
|
|
13
|
+
export { formatLintResult } from './report.js'
|
|
14
|
+
export type { LintReportFormat } from './report.js'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { performance } from 'node:perf_hooks'
|
|
3
|
+
|
|
4
|
+
import { analyzeProject } from './analyze.js'
|
|
5
|
+
import { dedupeDiagnostics, type LintResult } from './diagnostic.js'
|
|
6
|
+
import { runOxlint } from './oxlint/run.js'
|
|
7
|
+
import { assertSingleTypeUniverse } from './preflight.js'
|
|
8
|
+
import { discoverProject } from './project.js'
|
|
9
|
+
|
|
10
|
+
export type LintDomainOptions = {
|
|
11
|
+
root?: string
|
|
12
|
+
fix?: boolean
|
|
13
|
+
preflight?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function lintDomain(options: LintDomainOptions = {}): Promise<LintResult> {
|
|
17
|
+
const started = performance.now()
|
|
18
|
+
const root = resolve(options.root ?? process.cwd())
|
|
19
|
+
if (options.preflight !== false) await assertSingleTypeUniverse(root)
|
|
20
|
+
const [project, oxlint] = await Promise.all([
|
|
21
|
+
discoverProject(root),
|
|
22
|
+
runOxlint(root, options.fix ?? false),
|
|
23
|
+
])
|
|
24
|
+
const diagnostics = dedupeDiagnostics([...oxlint.diagnostics, ...analyzeProject(project)])
|
|
25
|
+
return {
|
|
26
|
+
root: project.root,
|
|
27
|
+
diagnostics,
|
|
28
|
+
files: Math.max(project.files.length, oxlint.files),
|
|
29
|
+
durationMs: Math.max(0, Math.round(performance.now() - started)),
|
|
30
|
+
exitCode: diagnostics.some((diagnostic) => diagnostic.severity === 'error') ? 1 : 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import base from '@astrale-os/ox/lint'
|
|
2
|
+
import { defineConfig } from 'oxlint'
|
|
3
|
+
|
|
4
|
+
export const domainOxlintConfig = defineConfig({
|
|
5
|
+
extends: [base],
|
|
6
|
+
jsPlugins: [
|
|
7
|
+
{
|
|
8
|
+
name: 'astrale',
|
|
9
|
+
specifier: '@astrale-os/sdk/linter/oxlint-plugin',
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
rules: {
|
|
13
|
+
'astrale/core-has-no-async': 'error',
|
|
14
|
+
'astrale/no-nested-steps': 'error',
|
|
15
|
+
'astrale/no-read-inside-mutate': 'error',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default domainOxlintConfig
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { coreHasNoAsync } from '../rules/core-has-no-async.js'
|
|
4
|
+
import { noNestedSteps } from '../rules/no-nested-steps.js'
|
|
5
|
+
import { noReadInsideMutate } from '../rules/no-read-inside-mutate.js'
|
|
6
|
+
|
|
7
|
+
const astraleOxlintPlugin = {
|
|
8
|
+
meta: { name: 'astrale', version: '0.1.0' },
|
|
9
|
+
rules: {
|
|
10
|
+
'core-has-no-async': coreHasNoAsync,
|
|
11
|
+
'no-nested-steps': noNestedSteps,
|
|
12
|
+
'no-read-inside-mutate': noReadInsideMutate,
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default astraleOxlintPlugin
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { LinterToolError, type DiagnosticSeverity, type LintDiagnostic } from '../diagnostic.js'
|
|
8
|
+
import { ruleInfo } from '../rules/catalog.js'
|
|
9
|
+
|
|
10
|
+
type OxlintOutput = {
|
|
11
|
+
diagnostics?: OxlintDiagnostic[]
|
|
12
|
+
number_of_files?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type OxlintDiagnostic = {
|
|
16
|
+
message?: string
|
|
17
|
+
code?: string
|
|
18
|
+
severity?: string
|
|
19
|
+
url?: string
|
|
20
|
+
help?: string
|
|
21
|
+
filename?: string
|
|
22
|
+
labels?: Array<{
|
|
23
|
+
span?: { offset?: number; length?: number; line?: number; column?: number }
|
|
24
|
+
}>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type OxlintResult = {
|
|
28
|
+
diagnostics: LintDiagnostic[]
|
|
29
|
+
files: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runOxlint(root: string, fix: boolean): Promise<OxlintResult> {
|
|
33
|
+
const packageJson = join(root, 'package.json')
|
|
34
|
+
const projectRequire = createRequire(packageJson)
|
|
35
|
+
const sdkRequire = createRequire(import.meta.url)
|
|
36
|
+
const oxlintPackage = resolvePackage('oxlint/package.json', projectRequire, sdkRequire)
|
|
37
|
+
const bin = join(dirname(oxlintPackage), 'bin', 'oxlint')
|
|
38
|
+
const config = projectConfig(root) ?? internalConfigPath()
|
|
39
|
+
const args = [bin, '.', '--format=json', '--no-error-on-unmatched-pattern', '--config', config]
|
|
40
|
+
if (fix) args.push('--fix')
|
|
41
|
+
|
|
42
|
+
const child = spawn(process.execPath, args, {
|
|
43
|
+
cwd: root,
|
|
44
|
+
env: process.env,
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
})
|
|
47
|
+
let stdout = ''
|
|
48
|
+
let stderr = ''
|
|
49
|
+
child.stdout.setEncoding('utf8')
|
|
50
|
+
child.stderr.setEncoding('utf8')
|
|
51
|
+
child.stdout.on('data', (chunk: string) => (stdout += chunk))
|
|
52
|
+
child.stderr.on('data', (chunk: string) => (stderr += chunk))
|
|
53
|
+
const status = await new Promise<number | null>((resolveStatus, reject) => {
|
|
54
|
+
child.once('error', reject)
|
|
55
|
+
child.once('close', resolveStatus)
|
|
56
|
+
}).catch((error: unknown) => {
|
|
57
|
+
throw new LinterToolError(`Could not start Oxlint at ${bin}.`, { cause: error })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
let output: OxlintOutput
|
|
61
|
+
try {
|
|
62
|
+
output = JSON.parse(stdout) as OxlintOutput
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const detail =
|
|
65
|
+
stderr.trim() || stdout.trim() || `Oxlint exited with status ${status ?? 'unknown'}`
|
|
66
|
+
throw new LinterToolError(`Oxlint did not produce a diagnostic report.\n${detail}`, {
|
|
67
|
+
cause: error,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
if (status !== 0 && status !== 1) {
|
|
71
|
+
throw new LinterToolError(
|
|
72
|
+
`Oxlint failed with status ${status ?? 'unknown'}.${stderr.trim() ? `\n${stderr.trim()}` : ''}`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
diagnostics: (output.diagnostics ?? []).map((diagnostic) =>
|
|
78
|
+
normalizeDiagnostic(root, diagnostic),
|
|
79
|
+
),
|
|
80
|
+
files: output.number_of_files ?? 0,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvePackage(
|
|
85
|
+
id: string,
|
|
86
|
+
projectRequire: NodeJS.Require,
|
|
87
|
+
sdkRequire: NodeJS.Require,
|
|
88
|
+
): string {
|
|
89
|
+
try {
|
|
90
|
+
return projectRequire.resolve(id)
|
|
91
|
+
} catch (projectError) {
|
|
92
|
+
try {
|
|
93
|
+
return sdkRequire.resolve(id)
|
|
94
|
+
} catch {
|
|
95
|
+
throw new LinterToolError(
|
|
96
|
+
'Oxlint is not installed. Add the scaffolded Oxlint dev dependency and run your package manager install.',
|
|
97
|
+
{ cause: projectError },
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function projectConfig(root: string): string | undefined {
|
|
104
|
+
for (const name of [
|
|
105
|
+
'oxlint.config.ts',
|
|
106
|
+
'oxlint.config.js',
|
|
107
|
+
'oxlint.config.mjs',
|
|
108
|
+
'oxlint.config.cjs',
|
|
109
|
+
'.oxlintrc.json',
|
|
110
|
+
'.oxlintrc.jsonc',
|
|
111
|
+
]) {
|
|
112
|
+
const path = join(root, name)
|
|
113
|
+
if (existsSync(path)) return path
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function internalConfigPath(): string {
|
|
119
|
+
const current = fileURLToPath(import.meta.url)
|
|
120
|
+
const extension = current.endsWith('.ts') ? '.ts' : '.js'
|
|
121
|
+
return join(dirname(current), `config${extension}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeDiagnostic(root: string, diagnostic: OxlintDiagnostic): LintDiagnostic {
|
|
125
|
+
const id = normalizeCode(diagnostic.code ?? 'oxlint/unknown')
|
|
126
|
+
const info = ruleInfo(id)
|
|
127
|
+
const span = diagnostic.labels?.[0]?.span
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
severity: normalizeSeverity(diagnostic.severity),
|
|
131
|
+
message: diagnostic.message ?? info?.message ?? 'Oxlint reported a violation.',
|
|
132
|
+
...(diagnostic.help || info?.help ? { help: diagnostic.help ?? info?.help } : {}),
|
|
133
|
+
...(diagnostic.url || info?.url ? { url: diagnostic.url ?? info?.url } : {}),
|
|
134
|
+
owner: info?.owner ?? 'generic',
|
|
135
|
+
...(diagnostic.filename
|
|
136
|
+
? {
|
|
137
|
+
location: {
|
|
138
|
+
path: relativePath(root, diagnostic.filename),
|
|
139
|
+
line: span?.line ?? 1,
|
|
140
|
+
column: span?.column ?? 1,
|
|
141
|
+
...(span?.offset !== undefined ? { offset: span.offset } : {}),
|
|
142
|
+
...(span?.length !== undefined ? { length: span.length } : {}),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeCode(code: string): string {
|
|
150
|
+
const match = /^([^()]+)\(([^()]+)\)$/.exec(code)
|
|
151
|
+
return match ? `${match[1]}/${match[2]}` : code
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeSeverity(severity: string | undefined): DiagnosticSeverity {
|
|
155
|
+
return severity === 'error' ? 'error' : 'warning'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function relativePath(root: string, filename: string): string {
|
|
159
|
+
const absolute = resolve(root, filename)
|
|
160
|
+
const path = relative(root, absolute)
|
|
161
|
+
return sep === '/' ? path : path.split(sep).join('/')
|
|
162
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile, realpath } from 'node:fs/promises'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { LinterToolError } from './diagnostic.js'
|
|
8
|
+
|
|
9
|
+
const brandedPackages = [
|
|
10
|
+
'@astrale-os/kernel-client',
|
|
11
|
+
'@astrale-os/kernel-core',
|
|
12
|
+
'@astrale-os/kernel-dsl',
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
const conventionalMembers = ['client', 'frontend', 'schema', 'worker'] as const
|
|
16
|
+
|
|
17
|
+
type Resolution = {
|
|
18
|
+
consumer: string
|
|
19
|
+
path: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ConsumerPackageJson = {
|
|
23
|
+
name?: string
|
|
24
|
+
dependencies?: Record<string, string>
|
|
25
|
+
optionalDependencies?: Record<string, string>
|
|
26
|
+
workspaces?: string[] | { packages?: string[] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function assertSingleTypeUniverse(root: string): Promise<void> {
|
|
30
|
+
const manifests = await collectConsumerManifests(root)
|
|
31
|
+
const consumers = [
|
|
32
|
+
...manifests.map((path) => ({ label: relative(root, dirname(path)) || '.', path })),
|
|
33
|
+
{ label: '@astrale-os/sdk/linter', path: fileURLPackageJson() },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for (const packageName of brandedPackages) {
|
|
37
|
+
const resolutions: Resolution[] = []
|
|
38
|
+
for (const consumer of consumers) {
|
|
39
|
+
const resolved = await resolvePackageRoot(packageName, consumer.path)
|
|
40
|
+
if (resolved) resolutions.push({ consumer: consumer.label, path: resolved })
|
|
41
|
+
}
|
|
42
|
+
const paths = new Set(resolutions.map((resolution) => resolution.path))
|
|
43
|
+
if (paths.size <= 1) continue
|
|
44
|
+
const detail = formatResolutions(resolutions)
|
|
45
|
+
throw new LinterToolError(
|
|
46
|
+
`Multiple physical installations of ${packageName} would create incompatible Astrale types.\n` +
|
|
47
|
+
`${detail}\nReinstall the workspace so every member resolves one compatible package instance.`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatResolutions(resolutions: readonly Resolution[]): string {
|
|
53
|
+
const byPath = new Map<string, string[]>()
|
|
54
|
+
for (const resolution of resolutions) {
|
|
55
|
+
const consumers = byPath.get(resolution.path) ?? []
|
|
56
|
+
consumers.push(resolution.consumer)
|
|
57
|
+
byPath.set(resolution.path, consumers)
|
|
58
|
+
}
|
|
59
|
+
return [...byPath]
|
|
60
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
61
|
+
.map(([path, consumers]) => {
|
|
62
|
+
const sorted = [...new Set(consumers)].sort()
|
|
63
|
+
const shown = sorted.slice(0, 4).join(', ')
|
|
64
|
+
const omitted = sorted.length > 4 ? ` (+${sorted.length - 4} more)` : ''
|
|
65
|
+
return ` ${path}\n from: ${shown}${omitted}`
|
|
66
|
+
})
|
|
67
|
+
.join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function collectConsumerManifests(root: string): Promise<string[]> {
|
|
71
|
+
const rootManifest = join(root, 'package.json')
|
|
72
|
+
if (!existsSync(rootManifest)) return []
|
|
73
|
+
|
|
74
|
+
const manifests = new Set([rootManifest])
|
|
75
|
+
const packageJson = await readPackageJson(rootManifest)
|
|
76
|
+
const workspaces = Array.isArray(packageJson?.workspaces)
|
|
77
|
+
? packageJson.workspaces
|
|
78
|
+
: packageJson?.workspaces?.packages
|
|
79
|
+
|
|
80
|
+
for (const workspace of workspaces ?? []) {
|
|
81
|
+
if (typeof workspace !== 'string' || hasGlob(workspace)) continue
|
|
82
|
+
addManifestWithinRoot(manifests, root, workspace)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const member of conventionalMembers) addManifestWithinRoot(manifests, root, member)
|
|
86
|
+
await addDomainContractDependencyManifests(manifests)
|
|
87
|
+
return [...manifests].sort()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readPackageJson(manifest: string): Promise<ConsumerPackageJson | undefined> {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(await readFile(manifest, 'utf8'))
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function addDomainContractDependencyManifests(manifests: Set<string>): Promise<void> {
|
|
99
|
+
const pending = [...manifests]
|
|
100
|
+
for (let index = 0; index < pending.length; index++) {
|
|
101
|
+
const consumer = pending[index]!
|
|
102
|
+
const packageJson = await readPackageJson(consumer)
|
|
103
|
+
const dependencies = {
|
|
104
|
+
...packageJson?.optionalDependencies,
|
|
105
|
+
...packageJson?.dependencies,
|
|
106
|
+
}
|
|
107
|
+
for (const packageName of Object.keys(dependencies).sort()) {
|
|
108
|
+
if (!isDomainContractPackage(packageName)) continue
|
|
109
|
+
const manifest = await resolveDependencyManifest(packageName, consumer)
|
|
110
|
+
if (!manifest || manifests.has(manifest)) continue
|
|
111
|
+
manifests.add(manifest)
|
|
112
|
+
pending.push(manifest)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function resolveDependencyManifest(
|
|
118
|
+
packageName: string,
|
|
119
|
+
consumerManifest: string,
|
|
120
|
+
): Promise<string | undefined> {
|
|
121
|
+
const require = createRequire(consumerManifest)
|
|
122
|
+
try {
|
|
123
|
+
return await realpath(require.resolve(`${packageName}/package.json`))
|
|
124
|
+
} catch {
|
|
125
|
+
try {
|
|
126
|
+
let directory = dirname(await realpath(require.resolve(packageName)))
|
|
127
|
+
for (;;) {
|
|
128
|
+
const manifest = join(directory, 'package.json')
|
|
129
|
+
if (existsSync(manifest)) {
|
|
130
|
+
const packageJson = await readPackageJson(manifest)
|
|
131
|
+
if (packageJson?.name === packageName) return await realpath(manifest)
|
|
132
|
+
}
|
|
133
|
+
const parent = dirname(directory)
|
|
134
|
+
if (parent === directory) return undefined
|
|
135
|
+
directory = parent
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addManifestWithinRoot(manifests: Set<string>, root: string, member: string): void {
|
|
144
|
+
const manifest = resolve(root, member, 'package.json')
|
|
145
|
+
const parent = relative(root, manifest)
|
|
146
|
+
if (parent.startsWith('..') || parent === '') return
|
|
147
|
+
if (existsSync(manifest)) manifests.add(manifest)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasGlob(path: string): boolean {
|
|
151
|
+
return /[*?[\]{}!]/u.test(path)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isDomainContractPackage(packageName: string): boolean {
|
|
155
|
+
return packageName.startsWith('@astrale-domains/')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function resolvePackageRoot(
|
|
159
|
+
packageName: (typeof brandedPackages)[number],
|
|
160
|
+
manifest: string,
|
|
161
|
+
): Promise<string | undefined> {
|
|
162
|
+
if (!existsSync(manifest)) return undefined
|
|
163
|
+
try {
|
|
164
|
+
const resolved = createRequire(manifest).resolve(`${packageName}/package.json`)
|
|
165
|
+
return await realpath(dirname(resolved))
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fileURLPackageJson(): string {
|
|
172
|
+
let directory = dirname(fileURLToPath(import.meta.url))
|
|
173
|
+
for (;;) {
|
|
174
|
+
const candidate = join(directory, 'package.json')
|
|
175
|
+
if (existsSync(candidate)) return candidate
|
|
176
|
+
const parent = dirname(directory)
|
|
177
|
+
if (parent === directory) break
|
|
178
|
+
directory = parent
|
|
179
|
+
}
|
|
180
|
+
return join(directory, 'package.json')
|
|
181
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readdir, readFile, realpath } from 'node:fs/promises'
|
|
2
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { scanSource, type SourceFile } from './source.js'
|
|
5
|
+
|
|
6
|
+
const ignoredDirectories = new Set([
|
|
7
|
+
'.astrale',
|
|
8
|
+
'.dist',
|
|
9
|
+
'.domain-studio',
|
|
10
|
+
'.git',
|
|
11
|
+
'.output',
|
|
12
|
+
'.turbo',
|
|
13
|
+
'.wrangler',
|
|
14
|
+
'coverage',
|
|
15
|
+
'dist',
|
|
16
|
+
'dist-client',
|
|
17
|
+
'node_modules',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const sourceExtensions = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx'])
|
|
21
|
+
|
|
22
|
+
export type DomainProject = {
|
|
23
|
+
root: string
|
|
24
|
+
files: readonly SourceFile[]
|
|
25
|
+
filesByPath: ReadonlyMap<string, SourceFile>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function discoverProject(root: string): Promise<DomainProject> {
|
|
29
|
+
const projectRoot = await realpath(resolve(root))
|
|
30
|
+
const paths: string[] = []
|
|
31
|
+
await collectSourcePaths(projectRoot, paths)
|
|
32
|
+
paths.sort()
|
|
33
|
+
|
|
34
|
+
const files = await Promise.all(
|
|
35
|
+
paths.map(async (path) => {
|
|
36
|
+
const text = await readFile(path, 'utf8')
|
|
37
|
+
return scanSource(path, normalizePath(relative(projectRoot, path)), text)
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
return {
|
|
41
|
+
root: projectRoot,
|
|
42
|
+
files,
|
|
43
|
+
filesByPath: new Map(files.map((file) => [file.path, file])),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collectSourcePaths(directory: string, result: string[]): Promise<void> {
|
|
48
|
+
const entries = await readdir(directory, { withFileTypes: true })
|
|
49
|
+
await Promise.all(
|
|
50
|
+
entries.map(async (entry) => {
|
|
51
|
+
if (entry.isSymbolicLink()) return
|
|
52
|
+
const path = join(directory, entry.name)
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
if (!ignoredDirectories.has(entry.name)) await collectSourcePaths(path, result)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (!entry.isFile() || !sourceExtensions.has(extname(entry.name))) return
|
|
58
|
+
if (entry.name.endsWith('.d.ts') || entry.name.includes('.gen.')) return
|
|
59
|
+
result.push(path)
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizePath(path: string): string {
|
|
65
|
+
return sep === '/' ? path : path.split(sep).join('/')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isProductionSource(relativePath: string): boolean {
|
|
69
|
+
const normalized = normalizePath(relativePath)
|
|
70
|
+
return (
|
|
71
|
+
!normalized.split('/').includes('__tests__') &&
|
|
72
|
+
!/\.(?:test|spec|bench|perf)\.[cm]?[jt]sx?$/.test(normalized) &&
|
|
73
|
+
!normalized.includes('.gen.')
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isProductionCore(relativePath: string): boolean {
|
|
78
|
+
const normalized = normalizePath(relativePath)
|
|
79
|
+
return (normalized === 'core' || normalized.startsWith('core/')) && isProductionSource(normalized)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveProjectImport(
|
|
83
|
+
project: DomainProject,
|
|
84
|
+
importer: SourceFile,
|
|
85
|
+
specifier: string,
|
|
86
|
+
): SourceFile | undefined {
|
|
87
|
+
if (!specifier.startsWith('.')) return undefined
|
|
88
|
+
const unresolved = resolve(dirname(importer.path), specifier)
|
|
89
|
+
for (const candidate of importCandidates(unresolved)) {
|
|
90
|
+
const file = project.filesByPath.get(candidate)
|
|
91
|
+
if (file) return file
|
|
92
|
+
}
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function importCandidates(path: string): string[] {
|
|
97
|
+
const extension = extname(path)
|
|
98
|
+
const candidates = [path]
|
|
99
|
+
if (extension === '.js' || extension === '.jsx' || extension === '.mjs' || extension === '.cjs') {
|
|
100
|
+
const stem = path.slice(0, -extension.length)
|
|
101
|
+
candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`, `${stem}.cts`)
|
|
102
|
+
}
|
|
103
|
+
if (!extension) {
|
|
104
|
+
candidates.push(`${path}.ts`, `${path}.tsx`, `${path}.mts`, `${path}.cts`)
|
|
105
|
+
candidates.push(join(path, 'index.ts'), join(path, 'index.tsx'))
|
|
106
|
+
}
|
|
107
|
+
return candidates.map((candidate) => (isAbsolute(candidate) ? candidate : resolve(candidate)))
|
|
108
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LintResult } from './diagnostic.js'
|
|
2
|
+
|
|
3
|
+
export type LintReportFormat = 'json' | 'stylish'
|
|
4
|
+
|
|
5
|
+
export function formatLintResult(result: LintResult, format: LintReportFormat = 'stylish'): string {
|
|
6
|
+
if (format === 'json') return `${JSON.stringify(result, null, 2)}\n`
|
|
7
|
+
const lines: string[] = []
|
|
8
|
+
for (const diagnostic of result.diagnostics) {
|
|
9
|
+
const location = diagnostic.location
|
|
10
|
+
? `${diagnostic.location.path}:${diagnostic.location.line}:${diagnostic.location.column}`
|
|
11
|
+
: result.root
|
|
12
|
+
lines.push(`${location} ${diagnostic.severity} ${diagnostic.id} ${diagnostic.message}`)
|
|
13
|
+
if (diagnostic.help) lines.push(` ${diagnostic.help}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length
|
|
17
|
+
const warnings = result.diagnostics.length - errors
|
|
18
|
+
if (lines.length > 0) lines.push('')
|
|
19
|
+
lines.push(
|
|
20
|
+
errors === 0
|
|
21
|
+
? `\u2713 ${result.files} files linted in ${result.durationMs}ms${warnings ? ` (${warnings} warnings)` : ''}`
|
|
22
|
+
: `\u2717 ${errors} ${errors === 1 ? 'error' : 'errors'}, ${warnings} ${warnings === 1 ? 'warning' : 'warnings'} in ${result.durationMs}ms`,
|
|
23
|
+
)
|
|
24
|
+
return `${lines.join('\n')}\n`
|
|
25
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type RuleOwner = 'analyzer' | 'oxlint' | 'sdk'
|
|
2
|
+
|
|
3
|
+
export type RuleInfo = {
|
|
4
|
+
slug: string
|
|
5
|
+
owner: RuleOwner
|
|
6
|
+
message: string
|
|
7
|
+
help: string
|
|
8
|
+
url: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ruleDocsUrl = (slug: string) => `https://docs.astrale.ai/linter/rules/${slug}`
|
|
12
|
+
|
|
13
|
+
export const ruleCatalog = {
|
|
14
|
+
'thin-schema-composition-root': {
|
|
15
|
+
slug: 'thin-schema-composition-root',
|
|
16
|
+
owner: 'analyzer',
|
|
17
|
+
message: 'The schema composition root defines a schema member inline.',
|
|
18
|
+
help: 'Move this member definition to its bounded-context module and import it here.',
|
|
19
|
+
url: ruleDocsUrl('thin-schema-composition-root'),
|
|
20
|
+
},
|
|
21
|
+
'one-remote-definition-per-file': {
|
|
22
|
+
slug: 'one-remote-definition-per-file',
|
|
23
|
+
owner: 'analyzer',
|
|
24
|
+
message: 'This file defines more than one Astrale remote callable.',
|
|
25
|
+
help: 'Keep one remoteMethod or defineRemoteFunction definition per implementation file.',
|
|
26
|
+
url: ruleDocsUrl('one-remote-definition-per-file'),
|
|
27
|
+
},
|
|
28
|
+
'core-is-pure': {
|
|
29
|
+
slug: 'core-is-pure',
|
|
30
|
+
owner: 'analyzer',
|
|
31
|
+
message: 'Pure core code depends on an effectful or runtime-only module.',
|
|
32
|
+
help: 'Move I/O orchestration to runtime or integrations and pass pure data into core.',
|
|
33
|
+
url: ruleDocsUrl('core-is-pure'),
|
|
34
|
+
},
|
|
35
|
+
'core-has-no-async': {
|
|
36
|
+
slug: 'core-has-no-async',
|
|
37
|
+
owner: 'oxlint',
|
|
38
|
+
message: 'Production core code contains asynchronous syntax.',
|
|
39
|
+
help: 'Keep core deterministic and synchronous; perform asynchronous work in runtime or integrations.',
|
|
40
|
+
url: ruleDocsUrl('core-has-no-async'),
|
|
41
|
+
},
|
|
42
|
+
'no-nested-steps': {
|
|
43
|
+
slug: 'no-nested-steps',
|
|
44
|
+
owner: 'oxlint',
|
|
45
|
+
message: 'A durable step directly contains another durable step.',
|
|
46
|
+
help: 'Give each stage one step boundary and call helper-owned steps outside another step callback.',
|
|
47
|
+
url: ruleDocsUrl('no-nested-steps'),
|
|
48
|
+
},
|
|
49
|
+
'no-read-inside-mutate': {
|
|
50
|
+
slug: 'no-read-inside-mutate',
|
|
51
|
+
owner: 'oxlint',
|
|
52
|
+
message: 'A graph mutation builder performs a read or is asynchronous.',
|
|
53
|
+
help: 'Read before mutate, then keep the mutation callback synchronous and limited to patch construction.',
|
|
54
|
+
url: ruleDocsUrl('no-read-inside-mutate'),
|
|
55
|
+
},
|
|
56
|
+
} as const satisfies Record<string, RuleInfo>
|
|
57
|
+
|
|
58
|
+
export type ImplementedRuleSlug = keyof typeof ruleCatalog
|
|
59
|
+
|
|
60
|
+
export function ruleId(slug: ImplementedRuleSlug): `astrale/${ImplementedRuleSlug}` {
|
|
61
|
+
return `astrale/${slug}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ruleInfo(id: string): RuleInfo | undefined {
|
|
65
|
+
const slug = id.startsWith('astrale/') ? id.slice('astrale/'.length) : id
|
|
66
|
+
return ruleCatalog[slug as ImplementedRuleSlug]
|
|
67
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { isProductionCoreFile, nearestFunction, reportUnlessSuppressed } from './oxlint-ast.js'
|
|
4
|
+
|
|
5
|
+
const id = 'astrale/core-has-no-async'
|
|
6
|
+
|
|
7
|
+
/** @type {import('./oxlint-rule.js').OxlintRule} */
|
|
8
|
+
export const coreHasNoAsync = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Production core code contains asynchronous syntax.',
|
|
13
|
+
url: 'https://docs.astrale.ai/linter/rules/core-has-no-async',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
asyncFunction: 'Production core functions must be synchronous.',
|
|
17
|
+
topLevelAwait: 'Production core modules must not use top-level await.',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
if (!isProductionCoreFile(context)) return {}
|
|
23
|
+
/** @param {import('./oxlint-rule.js').AstNode} node */
|
|
24
|
+
const reportFunction = (node) => {
|
|
25
|
+
if (node.async) reportUnlessSuppressed(context, node, id, 'asyncFunction')
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
FunctionDeclaration: reportFunction,
|
|
29
|
+
FunctionExpression: reportFunction,
|
|
30
|
+
ArrowFunctionExpression: reportFunction,
|
|
31
|
+
AwaitExpression(node) {
|
|
32
|
+
if (!nearestFunction(node.parent)) {
|
|
33
|
+
reportUnlessSuppressed(context, node, id, 'topLevelAwait')
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
ForOfStatement(node) {
|
|
37
|
+
if (node.await && !nearestFunction(node.parent)) {
|
|
38
|
+
reportUnlessSuppressed(context, node, id, 'topLevelAwait')
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|